diff --git a/pype/tools/pyblish_pype/__init__.py b/pype/tools/pyblish_pype/__init__.py
new file mode 100644
index 0000000000..ef507005a5
--- /dev/null
+++ b/pype/tools/pyblish_pype/__init__.py
@@ -0,0 +1,13 @@
+from .version import version, version_info, __version__
+
+# This must be run prior to importing the application, due to the
+# application requiring a discovered copy of Qt bindings.
+
+from .app import show
+
+__all__ = [
+ 'show',
+ 'version',
+ 'version_info',
+ '__version__'
+]
diff --git a/pype/tools/pyblish_pype/__main__.py b/pype/tools/pyblish_pype/__main__.py
new file mode 100644
index 0000000000..5fc1b44a35
--- /dev/null
+++ b/pype/tools/pyblish_pype/__main__.py
@@ -0,0 +1,19 @@
+from .app import show
+
+
+if __name__ == '__main__':
+ import argparse
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--debug", action="store_true")
+
+ args = parser.parse_args()
+
+ if args.debug:
+ from . import mock
+ import pyblish.api
+
+ for Plugin in mock.plugins:
+ pyblish.api.register_plugin(Plugin)
+
+ show()
diff --git a/pype/tools/pyblish_pype/app.css b/pype/tools/pyblish_pype/app.css
new file mode 100644
index 0000000000..b52d9efec8
--- /dev/null
+++ b/pype/tools/pyblish_pype/app.css
@@ -0,0 +1,493 @@
+/* Global CSS */
+
+* {
+ outline: none;
+ color: #ddd;
+ font-family: "Open Sans";
+ font-style: normal;
+}
+
+/* General CSS */
+
+QWidget {
+ background: #555;
+ background-position: center center;
+ background-repeat: no-repeat;
+ font-size: 12px;
+}
+
+QMenu {
+ background-color: #555; /* sets background of the menu */
+ border: 1px solid #222;
+}
+
+QMenu::item {
+ /* sets background of menu item. set this to something non-transparent
+ if you want menu color and menu item color to be different */
+ background-color: transparent;
+ padding: 5px;
+ padding-left: 30px;
+}
+
+QMenu::item:selected { /* when user selects item using mouse or keyboard */
+ background-color: #666;
+}
+
+QDialog {
+ min-width: 300;
+ background: "#555";
+}
+
+QListView {
+ border: 0px;
+ background: "transparent"
+}
+
+QTreeView {
+ border: 0px;
+ background: "transparent"
+}
+
+QPushButton {
+ width: 27px;
+ height: 27px;
+ background: #555;
+ border: 1px solid #aaa;
+ border-radius: 4px;
+ font-family: "FontAwesome";
+ font-size: 11pt;
+ color: white;
+ padding: 0px;
+}
+
+QPushButton:pressed {
+ background: "#777";
+}
+
+QPushButton:hover {
+ color: white;
+ background: "#666";
+}
+
+QPushButton:disabled {
+ color: rgba(255, 255, 255, 50);
+}
+
+QTextEdit, QLineEdit {
+ background: #555;
+ border: 1px solid #333;
+ font-size: 9pt;
+ color: #fff;
+}
+
+QCheckBox {
+ min-width: 17px;
+ max-width: 17px;
+ border: 1px solid #222;
+ background: transparent;
+}
+
+QCheckBox::indicator {
+ width: 15px;
+ height: 15px;
+ /*background: #444;*/
+ background: transparent;
+ border: 1px solid #555;
+}
+
+QCheckBox::indicator:checked {
+ background: #222;
+}
+
+QComboBox {
+ background: #444;
+ color: #EEE;
+ font-size: 8pt;
+ border: 1px solid #333;
+ padding: 0px;
+}
+
+QComboBox[combolist="true"]::drop-down {
+ background: transparent;
+}
+
+QComboBox[combolist="true"]::down-arrow {
+ max-width: 0px;
+ width: 1px;
+}
+
+QComboBox[combolist="true"] QAbstractItemView {
+ background: #555;
+}
+
+QScrollBar:vertical {
+ border: none;
+ background: transparent;
+ width: 6px;
+ margin: 0;
+}
+
+QScrollBar::handle:vertical {
+ background: #333;
+ border-radius: 3px;
+ min-height: 20px;
+}
+
+QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
+ height: 0px;
+}
+
+QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
+ border: 1px solid #444;
+ width: 3px;
+ height: 3px;
+ background: white;
+}
+
+QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
+ background: none;
+}
+
+QToolTip {
+ color: #eee;
+ background-color: #555;
+ border: none;
+ padding: 5px;
+}
+
+QLabel {
+ border-radius: 0px;
+}
+
+QToolButton {
+ background-color: transparent;
+ margin: 0px;
+ padding: 0px;
+ border-radius: 0px;
+ border: none;
+}
+
+/* Specific CSS */
+#PerspectiveToggleBtn {
+ border-bottom: 3px solid lightblue;
+ border-top: 0px;
+ border-radius: 0px;
+ border-right: 1px solid #232323;
+ border-left: 0px;
+ font-size: 26pt;
+ font-family: "FontAwesome";
+}
+
+#Terminal QComboBox::drop-down {
+ width: 60px;
+}
+
+#Header {
+ background: #555;
+ border: 1px solid #444;
+ padding: 0px;
+ margin: 0px;
+}
+
+#Header QRadioButton {
+ border: 3px solid "transparent";
+ border-right: 1px solid #333;
+ left: 2px;
+}
+
+#Header QRadioButton::indicator {
+ width: 65px;
+ height: 40px;
+ background-repeat: no-repeat;
+ background-position: center center;
+ image: none;
+}
+
+#Header QRadioButton:hover {
+ background-color: rgba(255, 255, 255, 10);
+}
+
+#Header QRadioButton:checked {
+ background-color: rgba(255, 255, 255, 20);
+ border-bottom: 3px solid "lightblue";
+}
+
+#Body {
+ padding: 0px;
+ border: 1px solid #333;
+ background: #444;
+}
+
+#Body QWidget {
+ background: #444;
+}
+
+#Header #ArtistTab {
+ background-image: url("img/tab-home.png");
+}
+
+#Header #TerminalTab {
+ background-image: url("img/tab-terminal.png");
+}
+
+#Header #OverviewTab {
+ background-image: url("img/tab-overview.png");
+}
+
+#ButtonWithMenu {
+ background: #555;
+ border: 1px solid #fff;
+ border-radius: 4px;
+ font-family: "FontAwesome";
+ font-size: 11pt;
+ color: white;
+}
+
+#ButtonWithMenu:pressed {
+ background: #777;
+}
+
+#ButtonWithMenu:hover {
+ color: white;
+ background: #666;
+}
+#ButtonWithMenu:disabled {
+ background: #666;
+ color: #999;
+ border: 1px solid #999;
+}
+
+#FooterSpacer, #FooterInfo, #HeaderSpacer {
+ background: transparent;
+}
+
+#Footer {
+ background: #555;
+ min-height: 43px;
+}
+
+#Footer[success="1"] {
+ background: #458056
+}
+
+#Footer[success="0"] {
+ background-color: #AA5050
+}
+
+#Footer QPushButton {
+ background: #555;
+ border: 1px solid #aaa;
+ border-radius: 4px;
+ font-family: "FontAwesome";
+ font-size: 11pt;
+ color: white;
+ padding: 0px;
+}
+
+#Footer QPushButton:pressed:hover {
+ color: #3784c5;
+ background: #444;
+}
+
+#Footer QPushButton:hover {
+ background: #505050;
+ border: 2px solid #3784c5;
+}
+
+#Footer QPushButton:disabled {
+ border: 1px solid #888;
+ background: #666;
+ color: #999;
+}
+
+#ClosingPlaceholder {
+ background: rgba(0, 0, 0, 50);
+}
+
+#CommentIntentWidget {
+ background: transparent;
+}
+
+#CommentBox, #CommentPlaceholder {
+ font-family: "Open Sans";
+ font-size: 8pt;
+ padding: 5px;
+ background: #444;
+}
+
+#CommentBox {
+ selection-background-color: #222;
+}
+
+#CommentBox:disabled, #CommentPlaceholder:disabled, #IntentBox:disabled {
+ background: #555;
+}
+
+#CommentPlaceholder {
+ color: #888
+}
+
+#IntentBox {
+ background: #444;
+ font-size: 8pt;
+ padding: 5px;
+ min-width: 75px;
+ color: #EEE;
+}
+
+#IntentBox::drop-down:button {
+ border: 0px;
+ background: transparent;
+}
+
+#IntentBox::down-arrow {
+ image: url("/img/down_arrow.png");
+}
+
+#IntentBox::down-arrow:disabled {
+ image: url();
+}
+
+#TerminalView {
+ background-color: transparent;
+}
+
+#TerminalView:item {
+ background-color: transparent;
+}
+
+#TerminalView:hover {
+ background-color: transparent;
+}
+
+#TerminalView:selected {
+ background-color: transparent;
+}
+
+#TerminalView:item:hover {
+ color: #ffffff;
+}
+
+#TerminalView:item:selected {
+ color: #eeeeee;
+}
+
+#TerminalView QTextEdit {
+ padding:3px;
+ color: #aaa;
+ border-radius: 7px;
+ border-color: #222;
+ border-style: solid;
+ border-width: 2px;
+ background-color: #333;
+}
+
+#TerminalView QTextEdit:hover {
+ background-color: #353535;
+}
+
+#TerminalView QTextEdit:selected {
+ background-color: #303030;
+}
+
+#ExpandableWidgetContent {
+ border: none;
+ background-color: #232323;
+ color:#eeeeee;
+}
+
+#EllidableLabel {
+ font-size: 16pt;
+ font-weight: normal;
+}
+
+#PerspectiveScrollContent {
+ border: 1px solid #333;
+ border-radius: 0px;
+}
+
+#PerspectiveWidgetContent{
+ padding: 0px;
+}
+
+#PerspectiveLabel {
+ background-color: transparent;
+ border: none;
+}
+
+#PerspectiveIndicator {
+ font-size: 16pt;
+ font-weight: normal;
+ padding: 5px;
+ background-color: #ffffff;
+ color: #333333;
+}
+
+#PerspectiveIndicator[state="warning"] {
+ background-color: #ff9900;
+ color: #ffffff;
+}
+
+#PerspectiveIndicator[state="active"] {
+ background-color: #99CEEE;
+ color: #ffffff;
+}
+
+#PerspectiveIndicator[state="error"] {
+ background-color: #cc4a4a;
+ color: #ffffff;
+}
+
+#PerspectiveIndicator[state="ok"] {
+ background-color: #69a567;
+ color: #ffffff;
+}
+
+#ExpandableHeader {
+ background-color: transparent;
+ margin: 0px;
+ padding: 0px;
+ border-radius: 0px;
+ border: none;
+}
+
+#ExpandableHeader QWidget {
+ color: #ddd;
+}
+
+#ExpandableHeader QWidget:hover {
+ color: #fff;
+}
+
+#TerminalFilerBtn {
+ /* font: %(font_size_pt)spt; */
+ font-family: "FontAwesome";
+ text-align: center;
+ background-color: transparent;
+ border-width: 1px;
+ border-color: #777777;
+ border-style: none;
+ padding: 0px;
+ border-radius: 3px;
+}
+
+#TerminalFilerBtn[type="info"]:checked {color: rgb(255, 255, 255);}
+#TerminalFilerBtn[type="info"] {color: rgba(255, 255, 255, 63);}
+
+#TerminalFilerBtn[type="error"]:checked {color: rgb(255, 74, 74);}
+#TerminalFilerBtn[type="error"] {color: rgba(255, 74, 74, 63);}
+
+#TerminalFilerBtn[type="log_debug"]:checked {color: rgb(255, 102, 232);}
+#TerminalFilerBtn[type="log_debug"] {color: rgba(255, 102, 232, 63);}
+
+#TerminalFilerBtn[type="log_info"]:checked {color: rgb(102, 171, 255);}
+#TerminalFilerBtn[type="log_info"] {color: rgba(102, 171, 255, 63);}
+
+#TerminalFilerBtn[type="log_warning"]:checked {color: rgb(255, 186, 102);}
+#TerminalFilerBtn[type="log_warning"] {color: rgba(255, 186, 102, 63);}
+
+#TerminalFilerBtn[type="log_error"]:checked {color: rgb(255, 77, 88);}
+#TerminalFilerBtn[type="log_error"] {color: rgba(255, 77, 88, 63);}
+
+#TerminalFilerBtn[type="log_critical"]:checked {color: rgb(255, 79, 117);}
+#TerminalFilerBtn[type="log_critical"] {color: rgba(255, 79, 117, 63);}
diff --git a/pype/tools/pyblish_pype/app.py b/pype/tools/pyblish_pype/app.py
new file mode 100644
index 0000000000..8b77d2f93d
--- /dev/null
+++ b/pype/tools/pyblish_pype/app.py
@@ -0,0 +1,104 @@
+from __future__ import print_function
+
+import contextlib
+import os
+import sys
+
+from . import compat, control, settings, util, window
+from .vendor.Qt import QtCore, QtGui, QtWidgets
+
+self = sys.modules[__name__]
+
+# Maintain reference to currently opened window
+self._window = None
+
+
+@contextlib.contextmanager
+def application():
+ app = QtWidgets.QApplication.instance()
+
+ if not app:
+ print("Starting new QApplication..")
+ app = QtWidgets.QApplication(sys.argv)
+ yield app
+ app.exec_()
+ else:
+ print("Using existing QApplication..")
+ yield app
+ if os.environ.get("PYBLISH_GUI_ALWAYS_EXEC"):
+ app.exec_()
+
+
+def install_translator(app):
+ translator = QtCore.QTranslator(app)
+ translator.load(QtCore.QLocale.system(), "i18n/",
+ directory=util.root)
+ app.installTranslator(translator)
+ print("Installed translator")
+
+
+def install_fonts():
+ database = QtGui.QFontDatabase()
+ fonts = [
+ "opensans/OpenSans-Bold.ttf",
+ "opensans/OpenSans-BoldItalic.ttf",
+ "opensans/OpenSans-ExtraBold.ttf",
+ "opensans/OpenSans-ExtraBoldItalic.ttf",
+ "opensans/OpenSans-Italic.ttf",
+ "opensans/OpenSans-Light.ttf",
+ "opensans/OpenSans-LightItalic.ttf",
+ "opensans/OpenSans-Regular.ttf",
+ "opensans/OpenSans-Semibold.ttf",
+ "opensans/OpenSans-SemiboldItalic.ttf",
+ "fontawesome/fontawesome-webfont.ttf"
+ ]
+
+ for font in fonts:
+ path = util.get_asset("font", font)
+
+ # TODO(marcus): Check if they are already installed first.
+ # In hosts, this will be called each time the GUI is shown,
+ # potentially installing a font each time.
+ if database.addApplicationFont(path) < 0:
+ sys.stderr.write("Could not install %s\n" % path)
+ else:
+ sys.stdout.write("Installed %s\n" % font)
+
+
+def on_destroyed():
+ """Remove internal reference to window on window destroyed"""
+ self._window = None
+
+
+def show(parent=None):
+ with open(util.get_asset("app.css")) as f:
+ css = f.read()
+
+ # Make relative paths absolute
+ root = util.get_asset("").replace("\\", "/")
+ css = css.replace("url(\"", "url(\"%s" % root)
+
+ with application() as app:
+ compat.init()
+
+ install_fonts()
+ install_translator(app)
+
+ ctrl = control.Controller()
+
+ if self._window is None:
+ self._window = window.Window(ctrl, parent)
+ self._window.destroyed.connect(on_destroyed)
+
+ self._window.show()
+ self._window.activateWindow()
+ self._window.resize(*settings.WindowSize)
+ self._window.setWindowTitle(settings.WindowTitle)
+
+ font = QtGui.QFont("Open Sans", 8, QtGui.QFont.Normal)
+ self._window.setFont(font)
+ self._window.setStyleSheet(css)
+
+ self._window.reset()
+
+ return self._window
diff --git a/pype/tools/pyblish_pype/awesome.py b/pype/tools/pyblish_pype/awesome.py
new file mode 100644
index 0000000000..c70f5b1064
--- /dev/null
+++ b/pype/tools/pyblish_pype/awesome.py
@@ -0,0 +1,733 @@
+
+tags = {
+ "500px": u"\uf26e",
+ "adjust": u"\uf042",
+ "adn": u"\uf170",
+ "align-center": u"\uf037",
+ "align-justify": u"\uf039",
+ "align-left": u"\uf036",
+ "align-right": u"\uf038",
+ "amazon": u"\uf270",
+ "ambulance": u"\uf0f9",
+ "american-sign-language-interpreting": u"\uf2a3",
+ "anchor": u"\uf13d",
+ "android": u"\uf17b",
+ "angellist": u"\uf209",
+ "angle-double-down": u"\uf103",
+ "angle-double-left": u"\uf100",
+ "angle-double-right": u"\uf101",
+ "angle-double-up": u"\uf102",
+ "angle-down": u"\uf107",
+ "angle-left": u"\uf104",
+ "angle-right": u"\uf105",
+ "angle-up": u"\uf106",
+ "apple": u"\uf179",
+ "archive": u"\uf187",
+ "area-chart": u"\uf1fe",
+ "arrow-circle-down": u"\uf0ab",
+ "arrow-circle-left": u"\uf0a8",
+ "arrow-circle-o-down": u"\uf01a",
+ "arrow-circle-o-left": u"\uf190",
+ "arrow-circle-o-right": u"\uf18e",
+ "arrow-circle-o-up": u"\uf01b",
+ "arrow-circle-right": u"\uf0a9",
+ "arrow-circle-up": u"\uf0aa",
+ "arrow-down": u"\uf063",
+ "arrow-left": u"\uf060",
+ "arrow-right": u"\uf061",
+ "arrow-up": u"\uf062",
+ "arrows": u"\uf047",
+ "arrows-alt": u"\uf0b2",
+ "arrows-h": u"\uf07e",
+ "arrows-v": u"\uf07d",
+ "asl-interpreting (alias)": u"\uf2a3",
+ "assistive-listening-systems": u"\uf2a2",
+ "asterisk": u"\uf069",
+ "at": u"\uf1fa",
+ "audio-description": u"\uf29e",
+ "automobile (alias)": u"\uf1b9",
+ "backward": u"\uf04a",
+ "balance-scale": u"\uf24e",
+ "ban": u"\uf05e",
+ "bank (alias)": u"\uf19c",
+ "bar-chart": u"\uf080",
+ "bar-chart-o (alias)": u"\uf080",
+ "barcode": u"\uf02a",
+ "bars": u"\uf0c9",
+ "battery-0 (alias)": u"\uf244",
+ "battery-1 (alias)": u"\uf243",
+ "battery-2 (alias)": u"\uf242",
+ "battery-3 (alias)": u"\uf241",
+ "battery-4 (alias)": u"\uf240",
+ "battery-empty": u"\uf244",
+ "battery-full": u"\uf240",
+ "battery-half": u"\uf242",
+ "battery-quarter": u"\uf243",
+ "battery-three-quarters": u"\uf241",
+ "bed": u"\uf236",
+ "beer": u"\uf0fc",
+ "behance": u"\uf1b4",
+ "behance-square": u"\uf1b5",
+ "bell": u"\uf0f3",
+ "bell-o": u"\uf0a2",
+ "bell-slash": u"\uf1f6",
+ "bell-slash-o": u"\uf1f7",
+ "bicycle": u"\uf206",
+ "binoculars": u"\uf1e5",
+ "birthday-cake": u"\uf1fd",
+ "bitbucket": u"\uf171",
+ "bitbucket-square": u"\uf172",
+ "bitcoin (alias)": u"\uf15a",
+ "black-tie": u"\uf27e",
+ "blind": u"\uf29d",
+ "bluetooth": u"\uf293",
+ "bluetooth-b": u"\uf294",
+ "bold": u"\uf032",
+ "bolt": u"\uf0e7",
+ "bomb": u"\uf1e2",
+ "book": u"\uf02d",
+ "bookmark": u"\uf02e",
+ "bookmark-o": u"\uf097",
+ "braille": u"\uf2a1",
+ "briefcase": u"\uf0b1",
+ "btc": u"\uf15a",
+ "bug": u"\uf188",
+ "building": u"\uf1ad",
+ "building-o": u"\uf0f7",
+ "bullhorn": u"\uf0a1",
+ "bullseye": u"\uf140",
+ "bus": u"\uf207",
+ "buysellads": u"\uf20d",
+ "cab (alias)": u"\uf1ba",
+ "calculator": u"\uf1ec",
+ "calendar": u"\uf073",
+ "calendar-check-o": u"\uf274",
+ "calendar-minus-o": u"\uf272",
+ "calendar-o": u"\uf133",
+ "calendar-plus-o": u"\uf271",
+ "calendar-times-o": u"\uf273",
+ "camera": u"\uf030",
+ "camera-retro": u"\uf083",
+ "car": u"\uf1b9",
+ "caret-down": u"\uf0d7",
+ "caret-left": u"\uf0d9",
+ "caret-right": u"\uf0da",
+ "caret-square-o-down": u"\uf150",
+ "caret-square-o-left": u"\uf191",
+ "caret-square-o-right": u"\uf152",
+ "caret-square-o-up": u"\uf151",
+ "caret-up": u"\uf0d8",
+ "cart-arrow-down": u"\uf218",
+ "cart-plus": u"\uf217",
+ "cc": u"\uf20a",
+ "cc-amex": u"\uf1f3",
+ "cc-diners-club": u"\uf24c",
+ "cc-discover": u"\uf1f2",
+ "cc-jcb": u"\uf24b",
+ "cc-mastercard": u"\uf1f1",
+ "cc-paypal": u"\uf1f4",
+ "cc-stripe": u"\uf1f5",
+ "cc-visa": u"\uf1f0",
+ "certificate": u"\uf0a3",
+ "chain (alias)": u"\uf0c1",
+ "chain-broken": u"\uf127",
+ "check": u"\uf00c",
+ "check-circle": u"\uf058",
+ "check-circle-o": u"\uf05d",
+ "check-square": u"\uf14a",
+ "check-square-o": u"\uf046",
+ "chevron-circle-down": u"\uf13a",
+ "chevron-circle-left": u"\uf137",
+ "chevron-circle-right": u"\uf138",
+ "chevron-circle-up": u"\uf139",
+ "chevron-down": u"\uf078",
+ "chevron-left": u"\uf053",
+ "chevron-right": u"\uf054",
+ "chevron-up": u"\uf077",
+ "child": u"\uf1ae",
+ "chrome": u"\uf268",
+ "circle": u"\uf111",
+ "circle-o": u"\uf10c",
+ "circle-o-notch": u"\uf1ce",
+ "circle-thin": u"\uf1db",
+ "clipboard": u"\uf0ea",
+ "clock-o": u"\uf017",
+ "clone": u"\uf24d",
+ "close (alias)": u"\uf00d",
+ "cloud": u"\uf0c2",
+ "cloud-download": u"\uf0ed",
+ "cloud-upload": u"\uf0ee",
+ "cny (alias)": u"\uf157",
+ "code": u"\uf121",
+ "code-fork": u"\uf126",
+ "codepen": u"\uf1cb",
+ "codiepie": u"\uf284",
+ "coffee": u"\uf0f4",
+ "cog": u"\uf013",
+ "cogs": u"\uf085",
+ "columns": u"\uf0db",
+ "comment": u"\uf075",
+ "comment-o": u"\uf0e5",
+ "commenting": u"\uf27a",
+ "commenting-o": u"\uf27b",
+ "comments": u"\uf086",
+ "comments-o": u"\uf0e6",
+ "compass": u"\uf14e",
+ "compress": u"\uf066",
+ "connectdevelop": u"\uf20e",
+ "contao": u"\uf26d",
+ "copy (alias)": u"\uf0c5",
+ "copyright": u"\uf1f9",
+ "creative-commons": u"\uf25e",
+ "credit-card": u"\uf09d",
+ "credit-card-alt": u"\uf283",
+ "crop": u"\uf125",
+ "crosshairs": u"\uf05b",
+ "css3": u"\uf13c",
+ "cube": u"\uf1b2",
+ "cubes": u"\uf1b3",
+ "cut (alias)": u"\uf0c4",
+ "cutlery": u"\uf0f5",
+ "dashboard (alias)": u"\uf0e4",
+ "dashcube": u"\uf210",
+ "database": u"\uf1c0",
+ "deaf": u"\uf2a4",
+ "deafness (alias)": u"\uf2a4",
+ "dedent (alias)": u"\uf03b",
+ "delicious": u"\uf1a5",
+ "desktop": u"\uf108",
+ "deviantart": u"\uf1bd",
+ "diamond": u"\uf219",
+ "digg": u"\uf1a6",
+ "dollar (alias)": u"\uf155",
+ "dot-circle-o": u"\uf192",
+ "download": u"\uf019",
+ "dribbble": u"\uf17d",
+ "dropbox": u"\uf16b",
+ "drupal": u"\uf1a9",
+ "edge": u"\uf282",
+ "edit (alias)": u"\uf044",
+ "eject": u"\uf052",
+ "ellipsis-h": u"\uf141",
+ "ellipsis-v": u"\uf142",
+ "empire": u"\uf1d1",
+ "envelope": u"\uf0e0",
+ "envelope-o": u"\uf003",
+ "envelope-square": u"\uf199",
+ "envira": u"\uf299",
+ "eraser": u"\uf12d",
+ "eur": u"\uf153",
+ "euro (alias)": u"\uf153",
+ "exchange": u"\uf0ec",
+ "exclamation": u"\uf12a",
+ "exclamation-circle": u"\uf06a",
+ "exclamation-triangle": u"\uf071",
+ "expand": u"\uf065",
+ "expeditedssl": u"\uf23e",
+ "external-link": u"\uf08e",
+ "external-link-square": u"\uf14c",
+ "eye": u"\uf06e",
+ "eye-slash": u"\uf070",
+ "eyedropper": u"\uf1fb",
+ "fa (alias)": u"\uf2b4",
+ "facebook": u"\uf09a",
+ "facebook-f (alias)": u"\uf09a",
+ "facebook-official": u"\uf230",
+ "facebook-square": u"\uf082",
+ "fast-backward": u"\uf049",
+ "fast-forward": u"\uf050",
+ "fax": u"\uf1ac",
+ "feed (alias)": u"\uf09e",
+ "female": u"\uf182",
+ "fighter-jet": u"\uf0fb",
+ "file": u"\uf15b",
+ "file-archive-o": u"\uf1c6",
+ "file-audio-o": u"\uf1c7",
+ "file-code-o": u"\uf1c9",
+ "file-excel-o": u"\uf1c3",
+ "file-image-o": u"\uf1c5",
+ "file-movie-o (alias)": u"\uf1c8",
+ "file-o": u"\uf016",
+ "file-pdf-o": u"\uf1c1",
+ "file-photo-o (alias)": u"\uf1c5",
+ "file-picture-o (alias)": u"\uf1c5",
+ "file-powerpoint-o": u"\uf1c4",
+ "file-sound-o (alias)": u"\uf1c7",
+ "file-text": u"\uf15c",
+ "file-text-o": u"\uf0f6",
+ "file-video-o": u"\uf1c8",
+ "file-word-o": u"\uf1c2",
+ "file-zip-o (alias)": u"\uf1c6",
+ "files-o": u"\uf0c5",
+ "film": u"\uf008",
+ "filter": u"\uf0b0",
+ "fire": u"\uf06d",
+ "fire-extinguisher": u"\uf134",
+ "firefox": u"\uf269",
+ "first-order": u"\uf2b0",
+ "flag": u"\uf024",
+ "flag-checkered": u"\uf11e",
+ "flag-o": u"\uf11d",
+ "flash (alias)": u"\uf0e7",
+ "flask": u"\uf0c3",
+ "flickr": u"\uf16e",
+ "floppy-o": u"\uf0c7",
+ "folder": u"\uf07b",
+ "folder-o": u"\uf114",
+ "folder-open": u"\uf07c",
+ "folder-open-o": u"\uf115",
+ "font": u"\uf031",
+ "font-awesome": u"\uf2b4",
+ "fonticons": u"\uf280",
+ "fort-awesome": u"\uf286",
+ "forumbee": u"\uf211",
+ "forward": u"\uf04e",
+ "foursquare": u"\uf180",
+ "frown-o": u"\uf119",
+ "futbol-o": u"\uf1e3",
+ "gamepad": u"\uf11b",
+ "gavel": u"\uf0e3",
+ "gbp": u"\uf154",
+ "ge (alias)": u"\uf1d1",
+ "gear (alias)": u"\uf013",
+ "gears (alias)": u"\uf085",
+ "genderless": u"\uf22d",
+ "get-pocket": u"\uf265",
+ "gg": u"\uf260",
+ "gg-circle": u"\uf261",
+ "gift": u"\uf06b",
+ "git": u"\uf1d3",
+ "git-square": u"\uf1d2",
+ "github": u"\uf09b",
+ "github-alt": u"\uf113",
+ "github-square": u"\uf092",
+ "gitlab": u"\uf296",
+ "gittip (alias)": u"\uf184",
+ "glass": u"\uf000",
+ "glide": u"\uf2a5",
+ "glide-g": u"\uf2a6",
+ "globe": u"\uf0ac",
+ "google": u"\uf1a0",
+ "google-plus": u"\uf0d5",
+ "google-plus-circle (alias)": u"\uf2b3",
+ "google-plus-official": u"\uf2b3",
+ "google-plus-square": u"\uf0d4",
+ "google-wallet": u"\uf1ee",
+ "graduation-cap": u"\uf19d",
+ "gratipay": u"\uf184",
+ "group (alias)": u"\uf0c0",
+ "h-square": u"\uf0fd",
+ "hacker-news": u"\uf1d4",
+ "hand-grab-o (alias)": u"\uf255",
+ "hand-lizard-o": u"\uf258",
+ "hand-o-down": u"\uf0a7",
+ "hand-o-left": u"\uf0a5",
+ "hand-o-right": u"\uf0a4",
+ "hand-o-up": u"\uf0a6",
+ "hand-paper-o": u"\uf256",
+ "hand-peace-o": u"\uf25b",
+ "hand-pointer-o": u"\uf25a",
+ "hand-rock-o": u"\uf255",
+ "hand-scissors-o": u"\uf257",
+ "hand-spock-o": u"\uf259",
+ "hand-stop-o (alias)": u"\uf256",
+ "hard-of-hearing (alias)": u"\uf2a4",
+ "hashtag": u"\uf292",
+ "hdd-o": u"\uf0a0",
+ "header": u"\uf1dc",
+ "headphones": u"\uf025",
+ "heart": u"\uf004",
+ "heart-o": u"\uf08a",
+ "heartbeat": u"\uf21e",
+ "history": u"\uf1da",
+ "home": u"\uf015",
+ "hospital-o": u"\uf0f8",
+ "hotel (alias)": u"\uf236",
+ "hourglass": u"\uf254",
+ "hourglass-1 (alias)": u"\uf251",
+ "hourglass-2 (alias)": u"\uf252",
+ "hourglass-3 (alias)": u"\uf253",
+ "hourglass-end": u"\uf253",
+ "hourglass-half": u"\uf252",
+ "hourglass-o": u"\uf250",
+ "hourglass-start": u"\uf251",
+ "houzz": u"\uf27c",
+ "html5": u"\uf13b",
+ "i-cursor": u"\uf246",
+ "ils": u"\uf20b",
+ "image (alias)": u"\uf03e",
+ "inbox": u"\uf01c",
+ "indent": u"\uf03c",
+ "industry": u"\uf275",
+ "info": u"\uf129",
+ "info-circle": u"\uf05a",
+ "inr": u"\uf156",
+ "instagram": u"\uf16d",
+ "institution (alias)": u"\uf19c",
+ "internet-explorer": u"\uf26b",
+ "intersex (alias)": u"\uf224",
+ "ioxhost": u"\uf208",
+ "italic": u"\uf033",
+ "joomla": u"\uf1aa",
+ "jpy": u"\uf157",
+ "jsfiddle": u"\uf1cc",
+ "key": u"\uf084",
+ "keyboard-o": u"\uf11c",
+ "krw": u"\uf159",
+ "language": u"\uf1ab",
+ "laptop": u"\uf109",
+ "lastfm": u"\uf202",
+ "lastfm-square": u"\uf203",
+ "leaf": u"\uf06c",
+ "leanpub": u"\uf212",
+ "legal (alias)": u"\uf0e3",
+ "lemon-o": u"\uf094",
+ "level-down": u"\uf149",
+ "level-up": u"\uf148",
+ "life-bouy (alias)": u"\uf1cd",
+ "life-buoy (alias)": u"\uf1cd",
+ "life-ring": u"\uf1cd",
+ "life-saver (alias)": u"\uf1cd",
+ "lightbulb-o": u"\uf0eb",
+ "line-chart": u"\uf201",
+ "link": u"\uf0c1",
+ "linkedin": u"\uf0e1",
+ "linkedin-square": u"\uf08c",
+ "linux": u"\uf17c",
+ "list": u"\uf03a",
+ "list-alt": u"\uf022",
+ "list-ol": u"\uf0cb",
+ "list-ul": u"\uf0ca",
+ "location-arrow": u"\uf124",
+ "lock": u"\uf023",
+ "long-arrow-down": u"\uf175",
+ "long-arrow-left": u"\uf177",
+ "long-arrow-right": u"\uf178",
+ "long-arrow-up": u"\uf176",
+ "low-vision": u"\uf2a8",
+ "magic": u"\uf0d0",
+ "magnet": u"\uf076",
+ "mail-forward (alias)": u"\uf064",
+ "mail-reply (alias)": u"\uf112",
+ "mail-reply-all (alias)": u"\uf122",
+ "male": u"\uf183",
+ "map": u"\uf279",
+ "map-marker": u"\uf041",
+ "map-o": u"\uf278",
+ "map-pin": u"\uf276",
+ "map-signs": u"\uf277",
+ "mars": u"\uf222",
+ "mars-double": u"\uf227",
+ "mars-stroke": u"\uf229",
+ "mars-stroke-h": u"\uf22b",
+ "mars-stroke-v": u"\uf22a",
+ "maxcdn": u"\uf136",
+ "meanpath": u"\uf20c",
+ "medium": u"\uf23a",
+ "medkit": u"\uf0fa",
+ "meh-o": u"\uf11a",
+ "mercury": u"\uf223",
+ "microphone": u"\uf130",
+ "microphone-slash": u"\uf131",
+ "minus": u"\uf068",
+ "minus-circle": u"\uf056",
+ "minus-square": u"\uf146",
+ "minus-square-o": u"\uf147",
+ "mixcloud": u"\uf289",
+ "mobile": u"\uf10b",
+ "mobile-phone (alias)": u"\uf10b",
+ "modx": u"\uf285",
+ "money": u"\uf0d6",
+ "moon-o": u"\uf186",
+ "mortar-board (alias)": u"\uf19d",
+ "motorcycle": u"\uf21c",
+ "mouse-pointer": u"\uf245",
+ "music": u"\uf001",
+ "navicon (alias)": u"\uf0c9",
+ "neuter": u"\uf22c",
+ "newspaper-o": u"\uf1ea",
+ "object-group": u"\uf247",
+ "object-ungroup": u"\uf248",
+ "odnoklassniki": u"\uf263",
+ "odnoklassniki-square": u"\uf264",
+ "opencart": u"\uf23d",
+ "openid": u"\uf19b",
+ "opera": u"\uf26a",
+ "optin-monster": u"\uf23c",
+ "outdent": u"\uf03b",
+ "pagelines": u"\uf18c",
+ "paint-brush": u"\uf1fc",
+ "paper-plane": u"\uf1d8",
+ "paper-plane-o": u"\uf1d9",
+ "paperclip": u"\uf0c6",
+ "paragraph": u"\uf1dd",
+ "paste (alias)": u"\uf0ea",
+ "pause": u"\uf04c",
+ "pause-circle": u"\uf28b",
+ "pause-circle-o": u"\uf28c",
+ "paw": u"\uf1b0",
+ "paypal": u"\uf1ed",
+ "pencil": u"\uf040",
+ "pencil-square": u"\uf14b",
+ "pencil-square-o": u"\uf044",
+ "percent": u"\uf295",
+ "phone": u"\uf095",
+ "phone-square": u"\uf098",
+ "photo (alias)": u"\uf03e",
+ "picture-o": u"\uf03e",
+ "pie-chart": u"\uf200",
+ "pied-piper": u"\uf2ae",
+ "pied-piper-alt": u"\uf1a8",
+ "pied-piper-pp": u"\uf1a7",
+ "pinterest": u"\uf0d2",
+ "pinterest-p": u"\uf231",
+ "pinterest-square": u"\uf0d3",
+ "plane": u"\uf072",
+ "play": u"\uf04b",
+ "play-circle": u"\uf144",
+ "play-circle-o": u"\uf01d",
+ "plug": u"\uf1e6",
+ "plus": u"\uf067",
+ "plus-circle": u"\uf055",
+ "plus-square": u"\uf0fe",
+ "plus-square-o": u"\uf196",
+ "power-off": u"\uf011",
+ "print": u"\uf02f",
+ "product-hunt": u"\uf288",
+ "puzzle-piece": u"\uf12e",
+ "qq": u"\uf1d6",
+ "qrcode": u"\uf029",
+ "question": u"\uf128",
+ "question-circle": u"\uf059",
+ "question-circle-o": u"\uf29c",
+ "quote-left": u"\uf10d",
+ "quote-right": u"\uf10e",
+ "ra (alias)": u"\uf1d0",
+ "random": u"\uf074",
+ "rebel": u"\uf1d0",
+ "recycle": u"\uf1b8",
+ "reddit": u"\uf1a1",
+ "reddit-alien": u"\uf281",
+ "reddit-square": u"\uf1a2",
+ "refresh": u"\uf021",
+ "registered": u"\uf25d",
+ "remove (alias)": u"\uf00d",
+ "renren": u"\uf18b",
+ "reorder (alias)": u"\uf0c9",
+ "repeat": u"\uf01e",
+ "reply": u"\uf112",
+ "reply-all": u"\uf122",
+ "resistance (alias)": u"\uf1d0",
+ "retweet": u"\uf079",
+ "rmb (alias)": u"\uf157",
+ "road": u"\uf018",
+ "rocket": u"\uf135",
+ "rotate-left (alias)": u"\uf0e2",
+ "rotate-right (alias)": u"\uf01e",
+ "rouble (alias)": u"\uf158",
+ "rss": u"\uf09e",
+ "rss-square": u"\uf143",
+ "rub": u"\uf158",
+ "ruble (alias)": u"\uf158",
+ "rupee (alias)": u"\uf156",
+ "safari": u"\uf267",
+ "save (alias)": u"\uf0c7",
+ "scissors": u"\uf0c4",
+ "scribd": u"\uf28a",
+ "search": u"\uf002",
+ "search-minus": u"\uf010",
+ "search-plus": u"\uf00e",
+ "sellsy": u"\uf213",
+ "send (alias)": u"\uf1d8",
+ "send-o (alias)": u"\uf1d9",
+ "server": u"\uf233",
+ "share": u"\uf064",
+ "share-alt": u"\uf1e0",
+ "share-alt-square": u"\uf1e1",
+ "share-square": u"\uf14d",
+ "share-square-o": u"\uf045",
+ "shekel (alias)": u"\uf20b",
+ "sheqel (alias)": u"\uf20b",
+ "shield": u"\uf132",
+ "ship": u"\uf21a",
+ "shirtsinbulk": u"\uf214",
+ "shopping-bag": u"\uf290",
+ "shopping-basket": u"\uf291",
+ "shopping-cart": u"\uf07a",
+ "sign-in": u"\uf090",
+ "sign-language": u"\uf2a7",
+ "sign-out": u"\uf08b",
+ "signal": u"\uf012",
+ "signing (alias)": u"\uf2a7",
+ "simplybuilt": u"\uf215",
+ "sitemap": u"\uf0e8",
+ "skyatlas": u"\uf216",
+ "skype": u"\uf17e",
+ "slack": u"\uf198",
+ "sliders": u"\uf1de",
+ "slideshare": u"\uf1e7",
+ "smile-o": u"\uf118",
+ "snapchat": u"\uf2ab",
+ "snapchat-ghost": u"\uf2ac",
+ "snapchat-square": u"\uf2ad",
+ "soccer-ball-o (alias)": u"\uf1e3",
+ "sort": u"\uf0dc",
+ "sort-alpha-asc": u"\uf15d",
+ "sort-alpha-desc": u"\uf15e",
+ "sort-amount-asc": u"\uf160",
+ "sort-amount-desc": u"\uf161",
+ "sort-asc": u"\uf0de",
+ "sort-desc": u"\uf0dd",
+ "sort-down (alias)": u"\uf0dd",
+ "sort-numeric-asc": u"\uf162",
+ "sort-numeric-desc": u"\uf163",
+ "sort-up (alias)": u"\uf0de",
+ "soundcloud": u"\uf1be",
+ "space-shuttle": u"\uf197",
+ "spinner": u"\uf110",
+ "spoon": u"\uf1b1",
+ "spotify": u"\uf1bc",
+ "square": u"\uf0c8",
+ "square-o": u"\uf096",
+ "stack-exchange": u"\uf18d",
+ "stack-overflow": u"\uf16c",
+ "star": u"\uf005",
+ "star-half": u"\uf089",
+ "star-half-empty (alias)": u"\uf123",
+ "star-half-full (alias)": u"\uf123",
+ "star-half-o": u"\uf123",
+ "star-o": u"\uf006",
+ "steam": u"\uf1b6",
+ "steam-square": u"\uf1b7",
+ "step-backward": u"\uf048",
+ "step-forward": u"\uf051",
+ "stethoscope": u"\uf0f1",
+ "sticky-note": u"\uf249",
+ "sticky-note-o": u"\uf24a",
+ "stop": u"\uf04d",
+ "stop-circle": u"\uf28d",
+ "stop-circle-o": u"\uf28e",
+ "street-view": u"\uf21d",
+ "strikethrough": u"\uf0cc",
+ "stumbleupon": u"\uf1a4",
+ "stumbleupon-circle": u"\uf1a3",
+ "subscript": u"\uf12c",
+ "subway": u"\uf239",
+ "suitcase": u"\uf0f2",
+ "sun-o": u"\uf185",
+ "superscript": u"\uf12b",
+ "support (alias)": u"\uf1cd",
+ "table": u"\uf0ce",
+ "tablet": u"\uf10a",
+ "tachometer": u"\uf0e4",
+ "tag": u"\uf02b",
+ "tags": u"\uf02c",
+ "tasks": u"\uf0ae",
+ "taxi": u"\uf1ba",
+ "television": u"\uf26c",
+ "tencent-weibo": u"\uf1d5",
+ "terminal": u"\uf120",
+ "text-height": u"\uf034",
+ "text-width": u"\uf035",
+ "th": u"\uf00a",
+ "th-large": u"\uf009",
+ "th-list": u"\uf00b",
+ "themeisle": u"\uf2b2",
+ "thumb-tack": u"\uf08d",
+ "thumbs-down": u"\uf165",
+ "thumbs-o-down": u"\uf088",
+ "thumbs-o-up": u"\uf087",
+ "thumbs-up": u"\uf164",
+ "ticket": u"\uf145",
+ "times": u"\uf00d",
+ "times-circle": u"\uf057",
+ "times-circle-o": u"\uf05c",
+ "tint": u"\uf043",
+ "toggle-down (alias)": u"\uf150",
+ "toggle-left (alias)": u"\uf191",
+ "toggle-off": u"\uf204",
+ "toggle-on": u"\uf205",
+ "toggle-right (alias)": u"\uf152",
+ "toggle-up (alias)": u"\uf151",
+ "trademark": u"\uf25c",
+ "train": u"\uf238",
+ "transgender": u"\uf224",
+ "transgender-alt": u"\uf225",
+ "trash": u"\uf1f8",
+ "trash-o": u"\uf014",
+ "tree": u"\uf1bb",
+ "trello": u"\uf181",
+ "tripadvisor": u"\uf262",
+ "trophy": u"\uf091",
+ "truck": u"\uf0d1",
+ "try": u"\uf195",
+ "tty": u"\uf1e4",
+ "tumblr": u"\uf173",
+ "tumblr-square": u"\uf174",
+ "turkish-lira (alias)": u"\uf195",
+ "tv (alias)": u"\uf26c",
+ "twitch": u"\uf1e8",
+ "twitter": u"\uf099",
+ "twitter-square": u"\uf081",
+ "umbrella": u"\uf0e9",
+ "underline": u"\uf0cd",
+ "undo": u"\uf0e2",
+ "universal-access": u"\uf29a",
+ "university": u"\uf19c",
+ "unlink (alias)": u"\uf127",
+ "unlock": u"\uf09c",
+ "unlock-alt": u"\uf13e",
+ "unsorted (alias)": u"\uf0dc",
+ "upload": u"\uf093",
+ "usb": u"\uf287",
+ "usd": u"\uf155",
+ "user": u"\uf007",
+ "user-md": u"\uf0f0",
+ "user-plus": u"\uf234",
+ "user-secret": u"\uf21b",
+ "user-times": u"\uf235",
+ "users": u"\uf0c0",
+ "venus": u"\uf221",
+ "venus-double": u"\uf226",
+ "venus-mars": u"\uf228",
+ "viacoin": u"\uf237",
+ "viadeo": u"\uf2a9",
+ "viadeo-square": u"\uf2aa",
+ "video-camera": u"\uf03d",
+ "vimeo": u"\uf27d",
+ "vimeo-square": u"\uf194",
+ "vine": u"\uf1ca",
+ "vk": u"\uf189",
+ "volume-control-phone": u"\uf2a0",
+ "volume-down": u"\uf027",
+ "volume-off": u"\uf026",
+ "volume-up": u"\uf028",
+ "warning (alias)": u"\uf071",
+ "wechat (alias)": u"\uf1d7",
+ "weibo": u"\uf18a",
+ "weixin": u"\uf1d7",
+ "whatsapp": u"\uf232",
+ "wheelchair": u"\uf193",
+ "wheelchair-alt": u"\uf29b",
+ "wifi": u"\uf1eb",
+ "wikipedia-w": u"\uf266",
+ "windows": u"\uf17a",
+ "won (alias)": u"\uf159",
+ "wordpress": u"\uf19a",
+ "wpbeginner": u"\uf297",
+ "wpforms": u"\uf298",
+ "wrench": u"\uf0ad",
+ "xing": u"\uf168",
+ "xing-square": u"\uf169",
+ "y-combinator": u"\uf23b",
+ "y-combinator-square (alias)": u"\uf1d4",
+ "yahoo": u"\uf19e",
+ "yc (alias)": u"\uf23b",
+ "yc-square (alias)": u"\uf1d4",
+ "yelp": u"\uf1e9",
+ "yen (alias)": u"\uf157",
+ "yoast": u"\uf2b1",
+ "youtube": u"\uf167",
+ "youtube-play": u"\uf16a",
+ "youtube-square": u"\uf166"
+}
diff --git a/pype/tools/pyblish_pype/compat.py b/pype/tools/pyblish_pype/compat.py
new file mode 100644
index 0000000000..bb520d65f5
--- /dev/null
+++ b/pype/tools/pyblish_pype/compat.py
@@ -0,0 +1,14 @@
+import os
+
+
+def __windows_taskbar_compat():
+ """Enable icon and taskbar grouping for Windows 7+"""
+
+ import ctypes
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
+ u"pyblish_pype")
+
+
+def init():
+ if os.name == "nt":
+ __windows_taskbar_compat()
diff --git a/pype/tools/pyblish_pype/constants.py b/pype/tools/pyblish_pype/constants.py
new file mode 100644
index 0000000000..077d93eec0
--- /dev/null
+++ b/pype/tools/pyblish_pype/constants.py
@@ -0,0 +1,95 @@
+from .vendor.Qt import QtCore
+
+
+def flags(*args, **kwargs):
+ type_name = kwargs.pop("type_name", "Flags")
+ with_base = kwargs.pop("with_base", False)
+ enums = {}
+ for idx, attr_name in enumerate(args):
+ if with_base:
+ if idx == 0:
+ enums[attr_name] = 0
+ continue
+ idx -= 1
+ enums[attr_name] = 2**idx
+
+ for attr_name, value in kwargs.items():
+ enums[attr_name] = value
+ return type(type_name, (), enums)
+
+
+def roles(*args, **kwargs):
+ type_name = kwargs.pop("type_name", "Roles")
+ enums = {}
+ for attr_name, value in kwargs.items():
+ enums[attr_name] = value
+
+ offset = 0
+ for idx, attr_name in enumerate(args):
+ _idx = idx + QtCore.Qt.UserRole + offset
+ while _idx in enums.values():
+ offset += 1
+ _idx = idx + offset
+
+ enums[attr_name] = _idx
+
+ return type(type_name, (), enums)
+
+
+Roles = roles(
+ "ObjectIdRole",
+ "ObjectUIdRole",
+ "TypeRole",
+ "PublishFlagsRole",
+ "LogRecordsRole",
+
+ "IsOptionalRole",
+ "IsEnabledRole",
+
+ "FamiliesRole",
+
+ "DocstringRole",
+ "PathModuleRole",
+ "PluginActionsVisibleRole",
+ "PluginValidActionsRole",
+ "PluginActionProgressRole",
+
+ "TerminalItemTypeRole",
+
+ "IntentItemValue",
+
+ type_name="ModelRoles"
+)
+
+InstanceStates = flags(
+ "ContextType",
+ "InProgress",
+ "HasWarning",
+ "HasError",
+ "HasFinished",
+ type_name="InstanceState"
+)
+
+PluginStates = flags(
+ "IsCompatible",
+ "InProgress",
+ "WasProcessed",
+ "WasSkipped",
+ "HasWarning",
+ "HasError",
+ type_name="PluginState"
+)
+
+GroupStates = flags(
+ "HasWarning",
+ "HasError",
+ "HasFinished",
+ type_name="GroupStates"
+)
+
+PluginActionStates = flags(
+ "InProgress",
+ "HasFailed",
+ "HasFinished",
+ type_name="PluginActionStates"
+)
diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py
new file mode 100644
index 0000000000..e64f3d5bfb
--- /dev/null
+++ b/pype/tools/pyblish_pype/control.py
@@ -0,0 +1,414 @@
+"""The Controller in a Model/View/Controller-based application
+The graphical components of Pyblish Lite use this object to perform
+publishing. It communicates via the Qt Signals/Slots mechanism
+and has no direct connection to any graphics. This is important,
+because this is how unittests are able to run without requiring
+an active window manager; such as via Travis-CI.
+"""
+import os
+import sys
+import traceback
+
+from .vendor.Qt import QtCore
+
+import pyblish.api
+import pyblish.util
+import pyblish.logic
+import pyblish.lib
+import pyblish.version
+
+from . import util
+from .constants import InstanceStates
+try:
+ from pypeapp.config import get_presets
+except Exception:
+ get_presets = dict
+
+
+class IterationBreak(Exception):
+ pass
+
+
+class Controller(QtCore.QObject):
+ # Emitted when the GUI is about to start processing;
+ # e.g. resetting, validating or publishing.
+ about_to_process = QtCore.Signal(object, object)
+
+ # ??? Emitted for each process
+ was_processed = QtCore.Signal(dict)
+
+ # Emmited when reset
+ # - all data are reset (plugins, processing, pari yielder, etc.)
+ was_reset = QtCore.Signal()
+
+ # Emmited when previous group changed
+ passed_group = QtCore.Signal(object)
+
+ # Emmited when want to change state of instances
+ switch_toggleability = QtCore.Signal(bool)
+
+ # On action finished
+ was_acted = QtCore.Signal(dict)
+
+ # Emitted when processing has stopped
+ was_stopped = QtCore.Signal()
+
+ # Emitted when processing has finished
+ was_finished = QtCore.Signal()
+
+ # Emitted when plugin was skipped
+ was_skipped = QtCore.Signal(object)
+
+ # store OrderGroups - now it is a singleton
+ order_groups = util.OrderGroups
+
+ def __init__(self, parent=None):
+ super(Controller, self).__init__(parent)
+ self.context = None
+ self.plugins = {}
+ self.optional_default = {}
+
+ def reset_variables(self):
+ # Data internal to the GUI itself
+ self.is_running = False
+ self.stopped = False
+ self.errored = False
+
+ # Active producer of pairs
+ self.pair_generator = None
+ # Active pair
+ self.current_pair = None
+
+ # Orders which changes GUI
+ # - passing collectors order disables plugin/instance toggle
+ self.collectors_order = None
+ self.collect_state = 0
+ self.collected = False
+
+ # - passing validators order disables validate button and gives ability
+ # to know when to stop on validate button press
+ self.validators_order = None
+ self.validated = False
+
+ # Get collectors and validators order
+ self.order_groups.reset()
+ plugin_groups = self.order_groups.groups()
+ plugin_groups_keys = list(plugin_groups.keys())
+ self.collectors_order = plugin_groups_keys[0]
+ self.validators_order = self.order_groups.validation_order()
+ next_group_order = None
+ if len(plugin_groups_keys) > 1:
+ next_group_order = plugin_groups_keys[1]
+
+ # This is used to track whether or not to continue
+ # processing when, for example, validation has failed.
+ self.processing = {
+ "stop_on_validation": False,
+ # Used?
+ "last_plugin_order": None,
+ "current_group_order": self.collectors_order,
+ "next_group_order": next_group_order,
+ "nextOrder": None,
+ "ordersWithError": set()
+ }
+
+ def presets_by_hosts(self):
+ # Get global filters as base
+ presets = get_presets().get("plugins", {})
+ if not presets:
+ return {}
+
+ result = presets.get("global", {}).get("filter", {})
+ hosts = pyblish.api.registered_hosts()
+ for host in hosts:
+ host_presets = presets.get(host, {}).get("filter")
+ if not host_presets:
+ continue
+
+ for key, value in host_presets.items():
+ if value is None:
+ if key in result:
+ result.pop(key)
+ continue
+
+ result[key] = value
+
+ return result
+
+ def reset_context(self):
+ self.context = pyblish.api.Context()
+
+ self.context._publish_states = InstanceStates.ContextType
+ self.context.optional = False
+
+ self.context.data["publish"] = True
+ self.context.data["label"] = "Context"
+ self.context.data["name"] = "context"
+
+ self.context.data["host"] = reversed(pyblish.api.registered_hosts())
+ self.context.data["port"] = int(
+ os.environ.get("PYBLISH_CLIENT_PORT", -1)
+ )
+ self.context.data["connectTime"] = pyblish.lib.time(),
+ self.context.data["pyblishVersion"] = pyblish.version,
+ self.context.data["pythonVersion"] = sys.version
+
+ self.context.data["icon"] = "book"
+
+ self.context.families = ("__context__",)
+
+ def reset(self):
+ """Discover plug-ins and run collection."""
+
+ self.reset_context()
+ self.reset_variables()
+
+ self.possible_presets = self.presets_by_hosts()
+
+ # Load plugins and set pair generator
+ self.load_plugins()
+ self.pair_generator = self._pair_yielder(self.plugins)
+
+ self.was_reset.emit()
+
+ # Process collectors load rest of plugins with collected instances
+ self.collect()
+
+ def load_plugins(self):
+ self.test = pyblish.logic.registered_test()
+ self.optional_default = {}
+
+ plugins = pyblish.api.discover()
+
+ targets = pyblish.logic.registered_targets() or ["default"]
+ self.plugins = pyblish.logic.plugins_by_targets(plugins, targets)
+
+ def on_published(self):
+ if self.is_running:
+ self.is_running = False
+ self.was_finished.emit()
+
+ def stop(self):
+ self.stopped = True
+
+ def act(self, plugin, action):
+ def on_next():
+ result = pyblish.plugin.process(
+ plugin, self.context, None, action.id
+ )
+ self.is_running = False
+ self.was_acted.emit(result)
+
+ self.is_running = True
+ util.defer(100, on_next)
+
+ def emit_(self, signal, kwargs):
+ pyblish.api.emit(signal, **kwargs)
+
+ def _process(self, plugin, instance=None):
+ """Produce `result` from `plugin` and `instance`
+ :func:`process` shares state with :func:`_iterator` such that
+ an instance/plugin pair can be fetched and processed in isolation.
+ Arguments:
+ plugin (pyblish.api.Plugin): Produce result using plug-in
+ instance (optional, pyblish.api.Instance): Process this instance,
+ if no instance is provided, context is processed.
+ """
+
+ self.processing["nextOrder"] = plugin.order
+
+ try:
+ result = pyblish.plugin.process(plugin, self.context, instance)
+ # Make note of the order at which the
+ # potential error error occured.
+ if result["error"] is not None:
+ self.processing["ordersWithError"].add(plugin.order)
+
+ except Exception as exc:
+ raise Exception("Unknown error({}): {}".format(
+ plugin.__name__, str(exc)
+ ))
+
+ return result
+
+ def _pair_yielder(self, plugins):
+ for plugin in plugins:
+ if (
+ self.processing["current_group_order"] is not None
+ and plugin.order > self.processing["current_group_order"]
+ ):
+ new_next_group_order = None
+ new_current_group_order = self.processing["next_group_order"]
+ if new_current_group_order is not None:
+ current_next_order_found = False
+ for order in self.order_groups.groups().keys():
+ if current_next_order_found:
+ new_next_group_order = order
+ break
+
+ if order == new_current_group_order:
+ current_next_order_found = True
+
+ self.processing["next_group_order"] = new_next_group_order
+ self.processing["current_group_order"] = (
+ new_current_group_order
+ )
+
+ if self.collect_state == 0:
+ self.collect_state = 1
+ self.switch_toggleability.emit(True)
+ self.passed_group.emit(new_current_group_order)
+ yield IterationBreak("Collected")
+
+ self.passed_group.emit(new_current_group_order)
+ if self.errored:
+ yield IterationBreak("Last group errored")
+
+ if self.collect_state == 1:
+ self.collect_state = 2
+ self.switch_toggleability.emit(False)
+
+ if not self.validated and plugin.order > self.validators_order:
+ self.validated = True
+ if self.processing["stop_on_validation"]:
+ yield IterationBreak("Validated")
+
+ # Stop if was stopped
+ if self.stopped:
+ self.stopped = False
+ yield IterationBreak("Stopped")
+
+ # check test if will stop
+ self.processing["nextOrder"] = plugin.order
+ message = self.test(**self.processing)
+ if message:
+ yield IterationBreak("Stopped due to \"{}\"".format(message))
+
+ self.processing["last_plugin_order"] = plugin.order
+ if not plugin.active:
+ pyblish.logic.log.debug("%s was inactive, skipping.." % plugin)
+ self.was_skipped.emit(plugin)
+ continue
+
+ if plugin.__instanceEnabled__:
+ instances = pyblish.logic.instances_by_plugin(
+ self.context, plugin
+ )
+ if not instances:
+ self.was_skipped.emit(plugin)
+ continue
+
+ for instance in instances:
+ if instance.data.get("publish") is False:
+ pyblish.logic.log.debug(
+ "%s was inactive, skipping.." % instance
+ )
+ continue
+ yield (plugin, instance)
+ else:
+ families = util.collect_families_from_instances(
+ self.context, only_active=True
+ )
+ plugins = pyblish.logic.plugins_by_families(
+ [plugin], families
+ )
+ if not plugins:
+ self.was_skipped.emit(plugin)
+ continue
+ yield (plugin, None)
+
+ self.passed_group.emit(self.processing["next_group_order"])
+
+ def iterate_and_process(self, on_finished=lambda: None):
+ """ Iterating inserted plugins with current context.
+ Collectors do not contain instances, they are None when collecting!
+ This process don't stop on one
+ """
+ def on_next():
+ try:
+ self.current_pair = next(self.pair_generator)
+ if isinstance(self.current_pair, IterationBreak):
+ raise self.current_pair
+
+ except IterationBreak:
+ self.is_running = False
+ self.was_stopped.emit()
+ return
+
+ except StopIteration:
+ self.is_running = False
+ # All pairs were processed successfully!
+ return util.defer(500, on_finished)
+
+ except Exception:
+ # This is a bug
+ exc_type, exc_msg, exc_tb = sys.exc_info()
+ traceback.print_exception(exc_type, exc_msg, exc_tb)
+ self.is_running = False
+ self.was_stopped.emit()
+ return util.defer(
+ 500, lambda: on_unexpected_error(error=exc_msg)
+ )
+
+ self.about_to_process.emit(*self.current_pair)
+ util.defer(100, on_process)
+
+ def on_process():
+ try:
+ result = self._process(*self.current_pair)
+ if result["error"] is not None:
+ self.errored = True
+
+ self.was_processed.emit(result)
+
+ except Exception:
+ # TODO this should be handled much differently
+ exc_type, exc_msg, exc_tb = sys.exc_info()
+ traceback.print_exception(exc_type, exc_msg, exc_tb)
+ return util.defer(
+ 500, lambda: on_unexpected_error(error=exc_msg)
+ )
+
+ util.defer(10, on_next)
+
+ def on_unexpected_error(error):
+ util.u_print(u"An unexpected error occurred:\n %s" % error)
+ return util.defer(500, on_finished)
+
+ self.is_running = True
+ util.defer(10, on_next)
+
+ def collect(self):
+ """ Iterate and process Collect plugins
+ - load_plugins method is launched again when finished
+ """
+ self.iterate_and_process()
+
+ def validate(self):
+ """ Process plugins to validations_order value."""
+ self.processing["stop_on_validation"] = True
+ self.iterate_and_process()
+
+ def publish(self):
+ """ Iterate and process all remaining plugins."""
+ self.processing["stop_on_validation"] = False
+ self.iterate_and_process(self.on_published)
+
+ def cleanup(self):
+ """Forcefully delete objects from memory
+ In an ideal world, this shouldn't be necessary. Garbage
+ collection guarantees that anything without reference
+ is automatically removed.
+ However, because this application is designed to be run
+ multiple times from the same interpreter process, extra
+ case must be taken to ensure there are no memory leaks.
+ Explicitly deleting objects shines a light on where objects
+ may still be referenced in the form of an error. No errors
+ means this was uneccesary, but that's ok.
+ """
+
+ for instance in self.context:
+ del(instance)
+
+ for plugin in self.plugins:
+ del(plugin)
diff --git a/pype/tools/pyblish_pype/delegate.py b/pype/tools/pyblish_pype/delegate.py
new file mode 100644
index 0000000000..849495cdeb
--- /dev/null
+++ b/pype/tools/pyblish_pype/delegate.py
@@ -0,0 +1,552 @@
+import platform
+
+from .vendor.Qt import QtWidgets, QtGui, QtCore
+
+from . import model
+from .awesome import tags as awesome
+from .constants import (
+ PluginStates, InstanceStates, PluginActionStates, Roles
+)
+
+colors = {
+ "error": QtGui.QColor("#ff4a4a"),
+ "warning": QtGui.QColor("#ff9900"),
+ "ok": QtGui.QColor("#77AE24"),
+ "active": QtGui.QColor("#99CEEE"),
+ "idle": QtCore.Qt.white,
+ "font": QtGui.QColor("#DDD"),
+ "inactive": QtGui.QColor("#888"),
+ "hover": QtGui.QColor(255, 255, 255, 10),
+ "selected": QtGui.QColor(255, 255, 255, 20),
+ "outline": QtGui.QColor("#333"),
+ "group": QtGui.QColor("#333")
+}
+
+scale_factors = {"darwin": 1.5}
+scale_factor = scale_factors.get(platform.system().lower(), 1.0)
+fonts = {
+ "h3": QtGui.QFont("Open Sans", 10 * scale_factor, QtGui.QFont.Normal),
+ "h4": QtGui.QFont("Open Sans", 8 * scale_factor, QtGui.QFont.Normal),
+ "h5": QtGui.QFont("Open Sans", 8 * scale_factor, QtGui.QFont.DemiBold),
+ "awesome6": QtGui.QFont("FontAwesome", 6 * scale_factor),
+ "awesome10": QtGui.QFont("FontAwesome", 10 * scale_factor),
+ "smallAwesome": QtGui.QFont("FontAwesome", 8 * scale_factor),
+ "largeAwesome": QtGui.QFont("FontAwesome", 16 * scale_factor),
+}
+font_metrics = {
+ "awesome6": QtGui.QFontMetrics(fonts["awesome6"]),
+ "h4": QtGui.QFontMetrics(fonts["h4"]),
+ "h5": QtGui.QFontMetrics(fonts["h5"])
+}
+icons = {
+ "action": awesome["adn"],
+ "angle-right": awesome["angle-right"],
+ "angle-left": awesome["angle-left"],
+ "plus-sign": awesome['plus'],
+ "minus-sign": awesome['minus']
+}
+
+
+class PluginItemDelegate(QtWidgets.QStyledItemDelegate):
+ """Generic delegate for model items"""
+
+ def paint(self, painter, option, index):
+ """Paint checkbox and text.
+ _
+ |_| My label >
+ """
+
+ body_rect = QtCore.QRectF(option.rect)
+
+ check_rect = QtCore.QRectF(body_rect)
+ check_rect.setWidth(check_rect.height())
+ check_offset = (check_rect.height() / 4) + 1
+ check_rect.adjust(
+ check_offset, check_offset, -check_offset, -check_offset
+ )
+
+ check_color = colors["idle"]
+
+ perspective_icon = icons["angle-right"]
+ perspective_rect = QtCore.QRectF(body_rect)
+ perspective_rect.setWidth(perspective_rect.height())
+ perspective_rect.adjust(0, 3, 0, 0)
+ perspective_rect.translate(
+ body_rect.width() - (perspective_rect.width() / 2 + 2),
+ 0
+ )
+
+ publish_states = index.data(Roles.PublishFlagsRole)
+ if publish_states & PluginStates.InProgress:
+ check_color = colors["active"]
+
+ elif publish_states & PluginStates.HasError:
+ check_color = colors["error"]
+
+ elif publish_states & PluginStates.HasWarning:
+ check_color = colors["warning"]
+
+ elif publish_states & PluginStates.WasProcessed:
+ check_color = colors["ok"]
+
+ elif not index.data(Roles.IsEnabledRole):
+ check_color = colors["inactive"]
+
+ offset = (body_rect.height() - font_metrics["h4"].height()) / 2
+ label_rect = QtCore.QRectF(body_rect.adjusted(
+ check_rect.width() + 12, offset - 1, 0, 0
+ ))
+
+ assert label_rect.width() > 0
+
+ label = index.data(QtCore.Qt.DisplayRole)
+ label = font_metrics["h4"].elidedText(
+ label,
+ QtCore.Qt.ElideRight,
+ label_rect.width() - 20
+ )
+
+ font_color = colors["idle"]
+ if not index.data(QtCore.Qt.CheckStateRole):
+ font_color = colors["inactive"]
+
+ # Maintain reference to state, so we can restore it once we're done
+ painter.save()
+
+ # Draw perspective icon
+ painter.setFont(fonts["awesome10"])
+ painter.setPen(QtGui.QPen(font_color))
+ painter.drawText(perspective_rect, perspective_icon)
+
+ # Draw label
+ painter.setFont(fonts["h4"])
+ painter.setPen(QtGui.QPen(font_color))
+ painter.drawText(label_rect, label)
+
+ # Draw action icon
+ if index.data(Roles.PluginActionsVisibleRole):
+ painter.save()
+ action_state = index.data(Roles.PluginActionProgressRole)
+ if action_state & PluginActionStates.HasFailed:
+ color = colors["error"]
+ elif action_state & PluginActionStates.HasFinished:
+ color = colors["ok"]
+ elif action_state & PluginActionStates.InProgress:
+ color = colors["active"]
+ else:
+ color = colors["idle"]
+
+ painter.setFont(fonts["smallAwesome"])
+ painter.setPen(QtGui.QPen(color))
+
+ icon_rect = QtCore.QRectF(
+ option.rect.adjusted(
+ label_rect.width() - perspective_rect.width() / 2,
+ label_rect.height() / 3, 0, 0
+ )
+ )
+ painter.drawText(icon_rect, icons["action"])
+
+ painter.restore()
+
+ # Draw checkbox
+ pen = QtGui.QPen(check_color, 1)
+ painter.setPen(pen)
+
+ if index.data(Roles.IsOptionalRole):
+ painter.drawRect(check_rect)
+
+ if index.data(QtCore.Qt.CheckStateRole):
+ optional_check_rect = QtCore.QRectF(check_rect)
+ optional_check_rect.adjust(2, 2, -1, -1)
+ painter.fillRect(optional_check_rect, check_color)
+
+ else:
+ painter.fillRect(check_rect, check_color)
+
+ if option.state & QtWidgets.QStyle.State_MouseOver:
+ painter.fillRect(body_rect, colors["hover"])
+
+ if option.state & QtWidgets.QStyle.State_Selected:
+ painter.fillRect(body_rect, colors["selected"])
+
+ # Ok, we're done, tidy up.
+ painter.restore()
+
+ def sizeHint(self, option, index):
+ return QtCore.QSize(option.rect.width(), 20)
+
+
+class InstanceItemDelegate(QtWidgets.QStyledItemDelegate):
+ """Generic delegate for model items"""
+
+ def paint(self, painter, option, index):
+ """Paint checkbox and text.
+ _
+ |_| My label >
+ """
+
+ body_rect = QtCore.QRectF(option.rect)
+
+ check_rect = QtCore.QRectF(body_rect)
+ check_rect.setWidth(check_rect.height())
+ offset = (check_rect.height() / 4) + 1
+ check_rect.adjust(offset, offset, -(offset), -(offset))
+
+ check_color = colors["idle"]
+
+ perspective_icon = icons["angle-right"]
+ perspective_rect = QtCore.QRectF(body_rect)
+ perspective_rect.setWidth(perspective_rect.height())
+ perspective_rect.adjust(0, 3, 0, 0)
+ perspective_rect.translate(
+ body_rect.width() - (perspective_rect.width() / 2 + 2),
+ 0
+ )
+
+ publish_states = index.data(Roles.PublishFlagsRole)
+ if publish_states & InstanceStates.InProgress:
+ check_color = colors["active"]
+
+ elif publish_states & InstanceStates.HasError:
+ check_color = colors["error"]
+
+ elif publish_states & InstanceStates.HasWarning:
+ check_color = colors["warning"]
+
+ elif publish_states & InstanceStates.HasFinished:
+ check_color = colors["ok"]
+
+ elif not index.data(Roles.IsEnabledRole):
+ check_color = colors["inactive"]
+
+ offset = (body_rect.height() - font_metrics["h4"].height()) / 2
+ label_rect = QtCore.QRectF(body_rect.adjusted(
+ check_rect.width() + 12, offset - 1, 0, 0
+ ))
+
+ assert label_rect.width() > 0
+
+ label = index.data(QtCore.Qt.DisplayRole)
+ label = font_metrics["h4"].elidedText(
+ label,
+ QtCore.Qt.ElideRight,
+ label_rect.width() - 20
+ )
+
+ font_color = colors["idle"]
+ if not index.data(QtCore.Qt.CheckStateRole):
+ font_color = colors["inactive"]
+
+ # Maintain reference to state, so we can restore it once we're done
+ painter.save()
+
+ # Draw perspective icon
+ painter.setFont(fonts["awesome10"])
+ painter.setPen(QtGui.QPen(font_color))
+ painter.drawText(perspective_rect, perspective_icon)
+
+ # Draw label
+ painter.setFont(fonts["h4"])
+ painter.setPen(QtGui.QPen(font_color))
+ painter.drawText(label_rect, label)
+
+ # Draw checkbox
+ pen = QtGui.QPen(check_color, 1)
+ painter.setPen(pen)
+
+ if index.data(Roles.IsOptionalRole):
+ painter.drawRect(check_rect)
+
+ if index.data(QtCore.Qt.CheckStateRole):
+ optional_check_rect = QtCore.QRectF(check_rect)
+ optional_check_rect.adjust(2, 2, -1, -1)
+ painter.fillRect(optional_check_rect, check_color)
+
+ else:
+ painter.fillRect(check_rect, check_color)
+
+ if option.state & QtWidgets.QStyle.State_MouseOver:
+ painter.fillRect(body_rect, colors["hover"])
+
+ if option.state & QtWidgets.QStyle.State_Selected:
+ painter.fillRect(body_rect, colors["selected"])
+
+ # Ok, we're done, tidy up.
+ painter.restore()
+
+ def sizeHint(self, option, index):
+ return QtCore.QSize(option.rect.width(), 20)
+
+
+class OverviewGroupSection(QtWidgets.QStyledItemDelegate):
+ """Generic delegate for section header"""
+
+ item_class = None
+
+ def __init__(self, parent):
+ super(OverviewGroupSection, self).__init__(parent)
+ self.item_delegate = self.item_class(parent)
+
+ def paint(self, painter, option, index):
+ if index.data(Roles.TypeRole) in (
+ model.InstanceType, model.PluginType
+ ):
+ self.item_delegate.paint(painter, option, index)
+ return
+
+ self.group_item_paint(painter, option, index)
+
+ def group_item_paint(self, painter, option, index):
+ """Paint text
+ _
+ My label
+ """
+ body_rect = QtCore.QRectF(option.rect)
+ bg_rect = QtCore.QRectF(
+ body_rect.left(), body_rect.top() + 1,
+ body_rect.width() - 5, body_rect.height() - 2
+ )
+ radius = 8.0
+ bg_path = QtGui.QPainterPath()
+ bg_path.addRoundedRect(bg_rect, radius, radius)
+ painter.fillPath(bg_path, colors["group"])
+
+ expander_rect = QtCore.QRectF(bg_rect)
+ expander_rect.setWidth(expander_rect.height())
+ text_height = font_metrics["awesome6"].height()
+ adjust_value = (expander_rect.height() - text_height) / 2
+ expander_rect.adjust(
+ adjust_value + 1.5, adjust_value - 0.5,
+ -adjust_value + 1.5, -adjust_value - 0.5
+ )
+
+ offset = (bg_rect.height() - font_metrics["h5"].height()) / 2
+ label_rect = QtCore.QRectF(bg_rect.adjusted(
+ expander_rect.width() + 12, offset - 1, 0, 0
+ ))
+
+ assert label_rect.width() > 0
+
+ expander_icon = icons["plus-sign"]
+
+ expanded = self.parent().isExpanded(index)
+ if expanded:
+ expander_icon = icons["minus-sign"]
+ label = index.data(QtCore.Qt.DisplayRole)
+ label = font_metrics["h5"].elidedText(
+ label, QtCore.Qt.ElideRight, label_rect.width()
+ )
+
+ # Maintain reference to state, so we can restore it once we're done
+ painter.save()
+
+ painter.setFont(fonts["awesome6"])
+ painter.setPen(QtGui.QPen(colors["idle"]))
+ painter.drawText(expander_rect, expander_icon)
+
+ # Draw label
+ painter.setFont(fonts["h5"])
+ painter.drawText(label_rect, label)
+
+ if option.state & QtWidgets.QStyle.State_MouseOver:
+ painter.fillPath(bg_path, colors["hover"])
+
+ if option.state & QtWidgets.QStyle.State_Selected:
+ painter.fillPath(bg_path, colors["selected"])
+
+ # Ok, we're done, tidy up.
+ painter.restore()
+
+ def sizeHint(self, option, index):
+ return QtCore.QSize(option.rect.width(), 20)
+
+
+class PluginDelegate(OverviewGroupSection):
+ """Generic delegate for model items in proxy tree view"""
+ item_class = PluginItemDelegate
+
+
+class InstanceDelegate(OverviewGroupSection):
+ """Generic delegate for model items in proxy tree view"""
+ item_class = InstanceItemDelegate
+
+
+class ArtistDelegate(QtWidgets.QStyledItemDelegate):
+ """Delegate used on Artist page"""
+
+ def paint(self, painter, option, index):
+ """Paint checkbox and text
+
+ _______________________________________________
+ | | label | duration |arrow|
+ |toggle |_____________________| | to |
+ | | families | |persp|
+ |_______|_____________________|___________|_____|
+
+ """
+
+ # Layout
+ spacing = 10
+
+ body_rect = QtCore.QRectF(option.rect).adjusted(2, 2, -8, -2)
+ content_rect = body_rect.adjusted(5, 5, -5, -5)
+
+ perspective_rect = QtCore.QRectF(body_rect)
+ perspective_rect.setWidth(35)
+ perspective_rect.setHeight(35)
+ perspective_rect.translate(
+ content_rect.width() - (perspective_rect.width() / 2) + 10,
+ (content_rect.height() / 2) - (perspective_rect.height() / 2)
+ )
+
+ toggle_rect = QtCore.QRectF(body_rect)
+ toggle_rect.setWidth(7)
+ toggle_rect.adjust(1, 1, 0, -1)
+
+ icon_rect = QtCore.QRectF(content_rect)
+ icon_rect.translate(toggle_rect.width() + spacing, 3)
+ icon_rect.setWidth(35)
+ icon_rect.setHeight(35)
+
+ duration_rect = QtCore.QRectF(content_rect)
+ duration_rect.translate(content_rect.width() - 50, 0)
+
+ # Colors
+ check_color = colors["idle"]
+
+ publish_states = index.data(Roles.PublishFlagsRole)
+ if publish_states is None:
+ return
+ if publish_states & InstanceStates.InProgress:
+ check_color = colors["active"]
+
+ elif publish_states & InstanceStates.HasError:
+ check_color = colors["error"]
+
+ elif publish_states & InstanceStates.HasWarning:
+ check_color = colors["warning"]
+
+ elif publish_states & InstanceStates.HasFinished:
+ check_color = colors["ok"]
+
+ elif not index.data(Roles.IsEnabledRole):
+ check_color = colors["inactive"]
+
+ perspective_icon = icons["angle-right"]
+
+ if not index.data(QtCore.Qt.CheckStateRole):
+ font_color = colors["inactive"]
+ else:
+ font_color = colors["idle"]
+
+ if (
+ option.state
+ & (
+ QtWidgets.QStyle.State_MouseOver
+ or QtWidgets.QStyle.State_Selected
+ )
+ ):
+ perspective_color = colors["idle"]
+ else:
+ perspective_color = colors["inactive"]
+ # Maintan reference to state, so we can restore it once we're done
+ painter.save()
+
+ # Draw background
+ painter.fillRect(body_rect, colors["hover"])
+
+ # Draw icon
+ icon = index.data(QtCore.Qt.DecorationRole)
+
+ painter.setFont(fonts["largeAwesome"])
+ painter.setPen(QtGui.QPen(font_color))
+ painter.drawText(icon_rect, icon)
+
+ # Draw label
+ painter.setFont(fonts["h3"])
+ label_rect = QtCore.QRectF(content_rect)
+ label_x_offset = icon_rect.width() + spacing
+ label_rect.translate(
+ label_x_offset,
+ 0
+ )
+ metrics = painter.fontMetrics()
+ label_rect.setHeight(metrics.lineSpacing())
+ label_rect.setWidth(
+ content_rect.width()
+ - label_x_offset
+ - perspective_rect.width()
+ )
+ # Elide label
+ label = index.data(QtCore.Qt.DisplayRole)
+ label = metrics.elidedText(
+ label, QtCore.Qt.ElideRight, label_rect.width()
+ )
+ painter.drawText(label_rect, label)
+
+ # Draw families
+ painter.setFont(fonts["h5"])
+ painter.setPen(QtGui.QPen(colors["inactive"]))
+
+ families = ", ".join(index.data(Roles.FamiliesRole))
+ families = painter.fontMetrics().elidedText(
+ families, QtCore.Qt.ElideRight, label_rect.width()
+ )
+
+ families_rect = QtCore.QRectF(label_rect)
+ families_rect.translate(0, label_rect.height() + spacing)
+
+ painter.drawText(families_rect, families)
+
+ painter.setFont(fonts["largeAwesome"])
+ painter.setPen(QtGui.QPen(perspective_color))
+ painter.drawText(perspective_rect, perspective_icon)
+
+ # Draw checkbox
+ pen = QtGui.QPen(check_color, 1)
+ painter.setPen(pen)
+
+ if index.data(Roles.IsOptionalRole):
+ painter.drawRect(toggle_rect)
+
+ if index.data(QtCore.Qt.CheckStateRole):
+ painter.fillRect(toggle_rect, check_color)
+
+ elif (
+ index.data(QtCore.Qt.CheckStateRole)
+ ):
+ painter.fillRect(toggle_rect, check_color)
+
+ if option.state & QtWidgets.QStyle.State_MouseOver:
+ painter.fillRect(body_rect, colors["hover"])
+
+ if option.state & QtWidgets.QStyle.State_Selected:
+ painter.fillRect(body_rect, colors["selected"])
+
+ painter.setPen(colors["outline"])
+ painter.drawRect(body_rect)
+
+ # Ok, we're done, tidy up.
+ painter.restore()
+
+ def sizeHint(self, option, index):
+ return QtCore.QSize(option.rect.width(), 80)
+
+
+class TerminalItem(QtWidgets.QStyledItemDelegate):
+ """Delegate used exclusively for the Terminal"""
+
+ def paint(self, painter, option, index):
+ super(TerminalItem, self).paint(painter, option, index)
+ item_type = index.data(Roles.TypeRole)
+ if item_type == model.TerminalDetailType:
+ return
+
+ hover = QtGui.QPainterPath()
+ hover.addRect(QtCore.QRectF(option.rect).adjusted(0, 0, -1, -1))
+ if option.state & QtWidgets.QStyle.State_Selected:
+ painter.fillPath(hover, colors["selected"])
+
+ if option.state & QtWidgets.QStyle.State_MouseOver:
+ painter.fillPath(hover, colors["hover"])
diff --git a/pype/tools/pyblish_pype/font/fontawesome/fontawesome-webfont.ttf b/pype/tools/pyblish_pype/font/fontawesome/fontawesome-webfont.ttf
new file mode 100644
index 0000000000..9d02852c14
Binary files /dev/null and b/pype/tools/pyblish_pype/font/fontawesome/fontawesome-webfont.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/LICENSE.txt b/pype/tools/pyblish_pype/font/opensans/LICENSE.txt
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/pype/tools/pyblish_pype/font/opensans/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Bold.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Bold.ttf
new file mode 100644
index 0000000000..fd79d43bea
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Bold.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-BoldItalic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-BoldItalic.ttf
new file mode 100644
index 0000000000..9bc800958a
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-BoldItalic.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBold.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBold.ttf
new file mode 100644
index 0000000000..21f6f84a07
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBold.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBoldItalic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBoldItalic.ttf
new file mode 100644
index 0000000000..31cb688340
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-ExtraBoldItalic.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Italic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Italic.ttf
new file mode 100644
index 0000000000..c90da48ff3
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Italic.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Light.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Light.ttf
new file mode 100644
index 0000000000..0d381897da
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Light.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-LightItalic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-LightItalic.ttf
new file mode 100644
index 0000000000..68299c4bc6
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-LightItalic.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Regular.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Regular.ttf
new file mode 100644
index 0000000000..db433349b7
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Regular.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-Semibold.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-Semibold.ttf
new file mode 100644
index 0000000000..1a7679e394
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-Semibold.ttf differ
diff --git a/pype/tools/pyblish_pype/font/opensans/OpenSans-SemiboldItalic.ttf b/pype/tools/pyblish_pype/font/opensans/OpenSans-SemiboldItalic.ttf
new file mode 100644
index 0000000000..59b6d16b06
Binary files /dev/null and b/pype/tools/pyblish_pype/font/opensans/OpenSans-SemiboldItalic.ttf differ
diff --git a/pype/tools/pyblish_pype/i18n/pyblish_lite.pro b/pype/tools/pyblish_pype/i18n/pyblish_lite.pro
new file mode 100644
index 0000000000..c8e2a5b56f
--- /dev/null
+++ b/pype/tools/pyblish_pype/i18n/pyblish_lite.pro
@@ -0,0 +1,2 @@
+SOURCES = ../window.py
+TRANSLATIONS = zh_CN.ts
\ No newline at end of file
diff --git a/pype/tools/pyblish_pype/i18n/zh_CN.qm b/pype/tools/pyblish_pype/i18n/zh_CN.qm
new file mode 100644
index 0000000000..fed08d8a51
Binary files /dev/null and b/pype/tools/pyblish_pype/i18n/zh_CN.qm differ
diff --git a/pype/tools/pyblish_pype/i18n/zh_CN.ts b/pype/tools/pyblish_pype/i18n/zh_CN.ts
new file mode 100644
index 0000000000..18ba81f69f
--- /dev/null
+++ b/pype/tools/pyblish_pype/i18n/zh_CN.ts
@@ -0,0 +1,96 @@
+
+
+
+ Window
+
+
+ Finishing up reset..
+ 完成重置..
+
+
+
+ Comment..
+ 备注..
+
+
+
+ Processing
+ 处理
+
+
+
+ Stopped due to error(s), see Terminal.
+ 因错误终止, 请查看终端。
+
+
+
+ Finished successfully!
+ 成功完成!
+
+
+
+ About to reset..
+ 即将重置..
+
+
+
+ Preparing validate..
+ 准备校验..
+
+
+
+ Preparing publish..
+ 准备发布..
+
+
+
+ Preparing
+ 准备
+
+
+
+ Action prepared.
+ 动作已就绪。
+
+
+
+ Cleaning up models..
+ 清理数据模型..
+
+
+
+ Cleaning up terminal..
+ 清理终端..
+
+
+
+ Cleaning up controller..
+ 清理控制器..
+
+
+
+ All clean!
+ 清理完成!
+
+
+
+ Good bye
+ 再见
+
+
+
+ ..as soon as processing is finished..
+ ..处理即将完成..
+
+
+
+ Stopping..
+ 正在停止..
+
+
+
+ Closing..
+ 正在关闭..
+
+
+
diff --git a/pype/tools/pyblish_pype/img/down_arrow.png b/pype/tools/pyblish_pype/img/down_arrow.png
new file mode 100644
index 0000000000..e271f7f90b
Binary files /dev/null and b/pype/tools/pyblish_pype/img/down_arrow.png differ
diff --git a/pype/tools/pyblish_pype/img/logo-extrasmall.png b/pype/tools/pyblish_pype/img/logo-extrasmall.png
new file mode 100644
index 0000000000..ebe45c4c6e
Binary files /dev/null and b/pype/tools/pyblish_pype/img/logo-extrasmall.png differ
diff --git a/pype/tools/pyblish_pype/img/tab-home.png b/pype/tools/pyblish_pype/img/tab-home.png
new file mode 100644
index 0000000000..9133d06edc
Binary files /dev/null and b/pype/tools/pyblish_pype/img/tab-home.png differ
diff --git a/pype/tools/pyblish_pype/img/tab-overview.png b/pype/tools/pyblish_pype/img/tab-overview.png
new file mode 100644
index 0000000000..443a750a7c
Binary files /dev/null and b/pype/tools/pyblish_pype/img/tab-overview.png differ
diff --git a/pype/tools/pyblish_pype/img/tab-terminal.png b/pype/tools/pyblish_pype/img/tab-terminal.png
new file mode 100644
index 0000000000..ea1bcff98d
Binary files /dev/null and b/pype/tools/pyblish_pype/img/tab-terminal.png differ
diff --git a/pype/tools/pyblish_pype/mock.py b/pype/tools/pyblish_pype/mock.py
new file mode 100644
index 0000000000..c85ff0f2ba
--- /dev/null
+++ b/pype/tools/pyblish_pype/mock.py
@@ -0,0 +1,732 @@
+import os
+import time
+import subprocess
+
+import pyblish.api
+
+
+class MyAction(pyblish.api.Action):
+ label = "My Action"
+ on = "processed"
+
+ def process(self, context, plugin):
+ self.log.info("Running!")
+
+
+class MyOtherAction(pyblish.api.Action):
+ label = "My Other Action"
+
+ def process(self, context, plugin):
+ self.log.info("Running!")
+
+
+class CollectComment(pyblish.api.ContextPlugin):
+ """This collector has a very long comment.
+
+ The idea is that this comment should either be elided, or word-
+ wrapped in the corresponding view.
+
+ """
+
+ order = pyblish.api.CollectorOrder
+
+ def process(self, context):
+ context.data["comment"] = ""
+
+
+class MyCollector(pyblish.api.ContextPlugin):
+ label = "My Collector"
+ order = pyblish.api.CollectorOrder
+
+ def process(self, context):
+ context.create_instance("MyInstance 1", families=["myFamily"])
+ context.create_instance("MyInstance 2", families=["myFamily 2"])
+ context.create_instance(
+ "MyInstance 3",
+ families=["myFamily 2"],
+ publish=False
+ )
+
+
+class MyValidator(pyblish.api.InstancePlugin):
+ order = pyblish.api.ValidatorOrder
+ active = False
+ label = "My Validator"
+ actions = [MyAction,
+ MyOtherAction]
+
+ def process(self, instance):
+ self.log.info("Validating: %s" % instance)
+
+
+class MyExtractor(pyblish.api.InstancePlugin):
+ order = pyblish.api.ExtractorOrder
+ families = ["myFamily"]
+ label = "My Extractor"
+
+ def process(self, instance):
+ self.log.info("Extracting: %s" % instance)
+
+
+class CollectRenamed(pyblish.api.Collector):
+ def process(self, context):
+ i = context.create_instance("MyInstanceXYZ", family="MyFamily")
+ i.set_data("name", "My instance")
+
+
+class CollectNegatron(pyblish.api.Collector):
+ """Negative collector adds Negatron"""
+
+ order = pyblish.api.Collector.order - 0.49
+
+ def process_context(self, context):
+ self.log.info("Collecting Negatron")
+ context.create_instance("Negatron", family="MyFamily")
+
+
+class CollectPositron(pyblish.api.Collector):
+ """Positive collector adds Positron"""
+
+ order = pyblish.api.Collector.order + 0.49
+
+ def process_context(self, context):
+ self.log.info("Collecting Positron")
+ context.create_instance("Positron", family="MyFamily")
+
+
+class SelectInstances(pyblish.api.Selector):
+ """Select debugging instances
+
+ These instances are part of the evil plan to destroy the world.
+ Be weary, be vigilant, be sexy.
+
+ """
+
+ def process_context(self, context):
+ self.log.info("Selecting instances..")
+
+ for instance in instances[:-1]:
+ name, data = instance["name"], instance["data"]
+ self.log.info("Selecting: %s" % name)
+ instance = context.create_instance(name)
+
+ for key, value in data.items():
+ instance.set_data(key, value)
+
+
+class SelectDiInstances(pyblish.api.Selector):
+ """Select DI instances"""
+
+ name = "Select Dependency Instances"
+
+ def process(self, context):
+ name, data = instances[-1]["name"], instances[-1]["data"]
+ self.log.info("Selecting: %s" % name)
+ instance = context.create_instance(name)
+
+ for key, value in data.items():
+ instance.set_data(key, value)
+
+
+class SelectInstancesFailure(pyblish.api.Selector):
+ """Select some instances, but fail before adding anything to the context.
+
+ That's right. I'm programmed to fail. Try me.
+
+ """
+
+ __fail__ = True
+
+ def process_context(self, context):
+ self.log.warning("I'm about to fail")
+ raise AssertionError("I was programmed to fail")
+
+
+class SelectInstances2(pyblish.api.Selector):
+ def process(self, context):
+ self.log.warning("I'm good")
+
+
+class ValidateNamespace(pyblish.api.Validator):
+ """Namespaces must be orange
+
+ In case a namespace is not orange, report immediately to
+ your officer in charge, ask for a refund, do a backflip.
+
+ This has been an example of:
+
+ - A long doc-string
+ - With a list
+ - And plenty of newlines and tabs.
+
+ """
+
+ families = ["B"]
+
+ def process(self, instance):
+ self.log.info("Validating the namespace of %s" % instance.data("name"))
+ self.log.info("""And here's another message, quite long, in fact it's
+too long to be displayed in a single row of text.
+But that's how we roll down here. It's got \nnew lines\nas well.
+
+- And lists
+- And more lists
+
+ """)
+
+
+class ValidateContext(pyblish.api.Validator):
+ families = ["A", "B"]
+
+ def process_context(self, context):
+ self.log.info("Processing context..")
+
+
+class ValidateContextFailure(pyblish.api.Validator):
+ optional = True
+ families = ["C"]
+ __fail__ = True
+
+ def process_context(self, context):
+ self.log.info("About to fail..")
+ raise AssertionError("""I was programmed to fail
+
+The reason I failed was because the sun was not aligned with the tides,
+and the moon is gray; not yellow. Try again when the moon is yellow.""")
+
+
+class Validator1(pyblish.api.Validator):
+ """Test of the order attribute"""
+ order = pyblish.api.Validator.order + 0.1
+ families = ["A"]
+
+ def process_instance(self, instance):
+ pass
+
+
+class Validator2(pyblish.api.Validator):
+ order = pyblish.api.Validator.order + 0.2
+ families = ["B"]
+
+ def process_instance(self, instance):
+ pass
+
+
+class Validator3(pyblish.api.Validator):
+ order = pyblish.api.Validator.order + 0.3
+ families = ["B"]
+
+ def process_instance(self, instance):
+ pass
+
+
+class ValidateFailureMock(pyblish.api.Validator):
+ """Plug-in that always fails"""
+ optional = True
+ order = pyblish.api.Validator.order + 0.1
+ families = ["C"]
+ __fail__ = True
+
+ def process_instance(self, instance):
+ self.log.debug("e = mc^2")
+ self.log.info("About to fail..")
+ self.log.warning("Failing.. soooon..")
+ self.log.critical("Ok, you're done.")
+ raise AssertionError("""ValidateFailureMock was destined to fail..
+
+Here's some extended information about what went wrong.
+
+It has quite the long string associated with it, including
+a few newlines and a list.
+
+- Item 1
+- Item 2
+
+""")
+
+
+class ValidateIsIncompatible(pyblish.api.Validator):
+ """This plug-in should never appear.."""
+ requires = False # This is invalid
+
+
+class ValidateWithRepair(pyblish.api.Validator):
+ """A validator with repair functionality"""
+ optional = True
+ families = ["C"]
+ __fail__ = True
+
+ def process_instance(self, instance):
+ raise AssertionError(
+ "%s is invalid, try repairing it!" % instance.name
+ )
+
+ def repair_instance(self, instance):
+ self.log.info("Attempting to repair..")
+ self.log.info("Success!")
+
+
+class ValidateWithRepairFailure(pyblish.api.Validator):
+ """A validator with repair functionality that fails"""
+ optional = True
+ families = ["C"]
+ __fail__ = True
+
+ def process_instance(self, instance):
+ raise AssertionError(
+ "%s is invalid, try repairing it!" % instance.name
+ )
+
+ def repair_instance(self, instance):
+ self.log.info("Attempting to repair..")
+ raise AssertionError("Could not repair due to X")
+
+
+class ValidateWithVeryVeryVeryLongLongNaaaaame(pyblish.api.Validator):
+ """A validator with repair functionality that fails"""
+ families = ["A"]
+
+
+class ValidateWithRepairContext(pyblish.api.Validator):
+ """A validator with repair functionality that fails"""
+ optional = True
+ families = ["C"]
+ __fail__ = True
+
+ def process_context(self, context):
+ raise AssertionError("Could not validate context, try repairing it")
+
+ def repair_context(self, context):
+ self.log.info("Attempting to repair..")
+ raise AssertionError("Could not repair")
+
+
+class ExtractAsMa(pyblish.api.Extractor):
+ """Extract contents of each instance into .ma
+
+ Serialise scene using Maya's own facilities and then put
+ it on the hard-disk. Once complete, this plug-in relies
+ on a Conformer to put it in it's final location, as this
+ extractor merely positions it in the users local temp-
+ directory.
+
+ """
+
+ optional = True
+ __expected__ = {
+ "logCount": ">=4"
+ }
+
+ def process_instance(self, instance):
+ self.log.info("About to extract scene to .ma..")
+ self.log.info("Extraction went well, now verifying the data..")
+
+ if instance.name == "Richard05":
+ self.log.warning("You're almost running out of disk space!")
+
+ self.log.info("About to finish up")
+ self.log.info("Finished successfully")
+
+
+class ConformAsset(pyblish.api.Conformer):
+ """Conform the world
+
+ Step 1: Conform all humans and Step 2: Conform all non-humans.
+ Once conforming has completed, rinse and repeat.
+
+ """
+
+ optional = True
+
+ def process_instance(self, instance):
+ self.log.info("About to conform all humans..")
+
+ if instance.name == "Richard05":
+ self.log.warning("Richard05 is a conformist!")
+
+ self.log.info("About to conform all non-humans..")
+ self.log.info("Conformed Successfully")
+
+
+class ValidateInstancesDI(pyblish.api.Validator):
+ """Validate using the DI interface"""
+ families = ["diFamily"]
+
+ def process(self, instance):
+ self.log.info("Validating %s.." % instance.data("name"))
+
+
+class ValidateDIWithRepair(pyblish.api.Validator):
+ families = ["diFamily"]
+ optional = True
+ __fail__ = True
+
+ def process(self, instance):
+ raise AssertionError("I was programmed to fail, for repair")
+
+ def repair(self, instance):
+ self.log.info("Repairing %s" % instance.data("name"))
+
+
+class ExtractInstancesDI(pyblish.api.Extractor):
+ """Extract using the DI interface"""
+ families = ["diFamily"]
+
+ def process(self, instance):
+ self.log.info("Extracting %s.." % instance.data("name"))
+
+
+class ValidateWithLabel(pyblish.api.Validator):
+ """Validate using the DI interface"""
+ label = "Validate with Label"
+
+
+class ValidateWithLongLabel(pyblish.api.Validator):
+ """Validate using the DI interface"""
+ label = "Validate with Loooooooooooooooooooooong Label"
+
+
+class SimplePlugin1(pyblish.api.Plugin):
+ """Validate using the simple-plugin interface"""
+
+ def process(self):
+ self.log.info("I'm a simple plug-in, only processed once")
+
+
+class SimplePlugin2(pyblish.api.Plugin):
+ """Validate using the simple-plugin interface
+
+ It doesn't have an order, and will likely end up *before* all
+ other plug-ins. (due to how sorted([1, 2, 3, None]) works)
+
+ """
+
+ def process(self, context):
+ self.log.info("Processing the context, simply: %s" % context)
+
+
+class SimplePlugin3(pyblish.api.Plugin):
+ """Simply process every instance"""
+
+ def process(self, instance):
+ self.log.info("Processing the instance, simply: %s" % instance)
+
+
+class ContextAction(pyblish.api.Action):
+ label = "Context action"
+
+ def process(self, context):
+ self.log.info("I have access to the context")
+ self.log.info("Context.instances: %s" % str(list(context)))
+
+
+class FailingAction(pyblish.api.Action):
+ label = "Failing action"
+
+ def process(self):
+ self.log.info("About to fail..")
+ raise Exception("I failed")
+
+
+class LongRunningAction(pyblish.api.Action):
+ label = "Long-running action"
+
+ def process(self):
+ self.log.info("Sleeping for 2 seconds..")
+ time.sleep(2)
+ self.log.info("Ah, that's better")
+
+
+class IconAction(pyblish.api.Action):
+ label = "Icon action"
+ icon = "crop"
+
+ def process(self):
+ self.log.info("I have an icon")
+
+
+class PluginAction(pyblish.api.Action):
+ label = "Plugin action"
+
+ def process(self, plugin):
+ self.log.info("I have access to my parent plug-in")
+ self.log.info("Which is %s" % plugin.id)
+
+
+class LaunchExplorerAction(pyblish.api.Action):
+ label = "Open in Explorer"
+ icon = "folder-open"
+
+ def process(self, context):
+ cwd = os.getcwd()
+ self.log.info("Opening %s in Explorer" % cwd)
+ result = subprocess.call("start .", cwd=cwd, shell=True)
+ self.log.debug(result)
+
+
+class ProcessedAction(pyblish.api.Action):
+ label = "Success action"
+ icon = "check"
+ on = "processed"
+
+ def process(self):
+ self.log.info("I am only available on a successful plug-in")
+
+
+class FailedAction(pyblish.api.Action):
+ label = "Failure action"
+ icon = "close"
+ on = "failed"
+
+
+class SucceededAction(pyblish.api.Action):
+ label = "Success action"
+ icon = "check"
+ on = "succeeded"
+
+ def process(self):
+ self.log.info("I am only available on a successful plug-in")
+
+
+class LongLabelAction(pyblish.api.Action):
+ label = "An incredibly, incredicly looooon label. Very long."
+ icon = "close"
+
+
+class BadEventAction(pyblish.api.Action):
+ label = "Bad event action"
+ on = "not exist"
+
+
+class InactiveAction(pyblish.api.Action):
+ active = False
+
+
+class PluginWithActions(pyblish.api.Validator):
+ optional = True
+ actions = [
+ pyblish.api.Category("General"),
+ ContextAction,
+ FailingAction,
+ LongRunningAction,
+ IconAction,
+ PluginAction,
+ pyblish.api.Category("Empty"),
+ pyblish.api.Category("OS"),
+ LaunchExplorerAction,
+ pyblish.api.Separator,
+ FailedAction,
+ SucceededAction,
+ pyblish.api.Category("Debug"),
+ BadEventAction,
+ InactiveAction,
+ LongLabelAction,
+ pyblish.api.Category("Empty"),
+ ]
+
+ def process(self):
+ self.log.info("Ran PluginWithActions")
+
+
+class FailingPluginWithActions(pyblish.api.Validator):
+ optional = True
+ actions = [
+ FailedAction,
+ SucceededAction,
+ ]
+
+ def process(self):
+ raise Exception("I was programmed to fail")
+
+
+class ValidateDefaultOff(pyblish.api.Validator):
+ families = ["A", "B"]
+ active = False
+ optional = True
+
+ def process(self, instance):
+ self.log.info("Processing instance..")
+
+
+class ValidateWithHyperlinks(pyblish.api.Validator):
+ """To learn about Pyblish
+
+ click here (http://pyblish.com)
+
+ """
+
+ families = ["A", "B"]
+
+ def process(self, instance):
+ self.log.info("Processing instance..")
+
+ msg = "To learn about Pyblish, "
+ msg += "click here (http://pyblish.com)"
+
+ self.log.info(msg)
+
+
+class LongRunningCollector(pyblish.api.Collector):
+ """I will take at least 2 seconds..."""
+ def process(self, context):
+ self.log.info("Sleeping for 2 seconds..")
+ time.sleep(2)
+ self.log.info("Good morning")
+
+
+class LongRunningValidator(pyblish.api.Validator):
+ """I will take at least 2 seconds..."""
+ def process(self, context):
+ self.log.info("Sleeping for 2 seconds..")
+ time.sleep(2)
+ self.log.info("Good morning")
+
+
+class RearrangingPlugin(pyblish.api.ContextPlugin):
+ """Sort plug-ins by family, and then reverse it"""
+ order = pyblish.api.CollectorOrder + 0.2
+
+ def process(self, context):
+ self.log.info("Reversing instances in the context..")
+ context[:] = sorted(
+ context,
+ key=lambda i: i.data["family"],
+ reverse=True
+ )
+ self.log.info("Reversed!")
+
+
+class InactiveInstanceCollectorPlugin(pyblish.api.InstancePlugin):
+ """Special case of an InstancePlugin running as a Collector"""
+ order = pyblish.api.CollectorOrder + 0.1
+ active = False
+
+ def process(self, instance):
+ raise TypeError("I shouldn't have run in the first place")
+
+
+class CollectWithIcon(pyblish.api.ContextPlugin):
+ order = pyblish.api.CollectorOrder
+
+ def process(self, context):
+ instance = context.create_instance("With Icon")
+ instance.data["icon"] = "play"
+
+
+instances = [
+ {
+ "name": "Peter01",
+ "data": {
+ "family": "A",
+ "publish": False
+ }
+ },
+ {
+ "name": "Richard05",
+ "data": {
+ "family": "A",
+ }
+ },
+ {
+ "name": "Steven11",
+ "data": {
+ "family": "B",
+ }
+ },
+ {
+ "name": "Piraya12",
+ "data": {
+ "family": "B",
+ }
+ },
+ {
+ "name": "Marcus",
+ "data": {
+ "family": "C",
+ }
+ },
+ {
+ "name": "Extra1",
+ "data": {
+ "family": "C",
+ }
+ },
+ {
+ "name": "DependencyInstance",
+ "data": {
+ "family": "diFamily"
+ }
+ },
+ {
+ "name": "NoFamily",
+ "data": {}
+ },
+ {
+ "name": "Failure 1",
+ "data": {
+ "family": "failure",
+ "fail": False
+ }
+ },
+ {
+ "name": "Failure 2",
+ "data": {
+ "family": "failure",
+ "fail": True
+ }
+ }
+]
+
+plugins = [
+ MyCollector,
+ MyValidator,
+ MyExtractor,
+
+ CollectRenamed,
+ CollectNegatron,
+ CollectPositron,
+ SelectInstances,
+ SelectInstances2,
+ SelectDiInstances,
+ SelectInstancesFailure,
+ ValidateFailureMock,
+ ValidateNamespace,
+ # ValidateIsIncompatible,
+ ValidateWithVeryVeryVeryLongLongNaaaaame,
+ ValidateContext,
+ ValidateContextFailure,
+ Validator1,
+ Validator2,
+ Validator3,
+ ValidateWithRepair,
+ ValidateWithRepairFailure,
+ ValidateWithRepairContext,
+ ValidateWithLabel,
+ ValidateWithLongLabel,
+ ValidateDefaultOff,
+ ValidateWithHyperlinks,
+ ExtractAsMa,
+ ConformAsset,
+
+ SimplePlugin1,
+ SimplePlugin2,
+ SimplePlugin3,
+
+ ValidateInstancesDI,
+ ExtractInstancesDI,
+ ValidateDIWithRepair,
+
+ PluginWithActions,
+ FailingPluginWithActions,
+
+ # LongRunningCollector,
+ # LongRunningValidator,
+
+ RearrangingPlugin,
+ InactiveInstanceCollectorPlugin,
+
+ CollectComment,
+ CollectWithIcon,
+]
+
+pyblish.api.sort_plugins(plugins)
diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py
new file mode 100644
index 0000000000..58ab3ed0b7
--- /dev/null
+++ b/pype/tools/pyblish_pype/model.py
@@ -0,0 +1,1232 @@
+"""Qt models
+
+Description:
+ The model contains the original objects from Pyblish, such as
+ pyblish.api.Instance and pyblish.api.Plugin. The model then
+ provides an interface for reading and writing to those.
+
+GUI data:
+ Aside from original data, such as pyblish.api.Plugin.optional,
+ the GUI also hosts data internal to itself, such as whether or
+ not an item has processed such that it may be colored appropriately
+ in the view. This data is prefixed with two underscores (__).
+
+ E.g.
+
+ _has_processed
+
+ This is so that the the GUI-only data doesn't accidentally overwrite
+ or cause confusion with existing data in plug-ins and instances.
+
+Roles:
+ Data is accessed via standard Qt "roles". You can think of a role
+ as the key of a dictionary, except they can only be integers.
+
+"""
+from __future__ import unicode_literals
+
+import pyblish
+
+from . import settings, util
+from .awesome import tags as awesome
+from .vendor import Qt
+from .vendor.Qt import QtCore, QtGui
+from .vendor.six import text_type
+from .vendor.six.moves import queue
+from .vendor import qtawesome
+from .constants import PluginStates, InstanceStates, GroupStates, Roles
+
+try:
+ from pypeapp import config
+ get_presets = config.get_presets
+except Exception:
+ get_presets = dict
+
+# ItemTypes
+InstanceType = QtGui.QStandardItem.UserType
+PluginType = QtGui.QStandardItem.UserType + 1
+GroupType = QtGui.QStandardItem.UserType + 2
+TerminalLabelType = QtGui.QStandardItem.UserType + 3
+TerminalDetailType = QtGui.QStandardItem.UserType + 4
+
+
+class QAwesomeTextIconFactory:
+ icons = {}
+ @classmethod
+ def icon(cls, icon_name):
+ if icon_name not in cls.icons:
+ cls.icons[icon_name] = awesome.get(icon_name)
+ return cls.icons[icon_name]
+
+
+class QAwesomeIconFactory:
+ icons = {}
+ @classmethod
+ def icon(cls, icon_name, icon_color):
+ if icon_name not in cls.icons:
+ cls.icons[icon_name] = {}
+
+ if icon_color not in cls.icons[icon_name]:
+ cls.icons[icon_name][icon_color] = qtawesome.icon(
+ icon_name,
+ color=icon_color
+ )
+ return cls.icons[icon_name][icon_color]
+
+
+class IntentModel(QtGui.QStandardItemModel):
+ """Model for QComboBox with intents.
+
+ It is expected that one inserted item is dictionary.
+ Key represents #Label and Value represent #Value.
+
+ Example:
+ {
+ "Testing": "test",
+ "Publishing": "publish"
+ }
+
+ First and default value is {"< Not Set >": None}
+ """
+
+ default_item = {"< Not Set >": None}
+
+ def __init__(self, parent=None):
+ super(IntentModel, self).__init__(parent)
+ self._item_count = 0
+ self.default_index = 0
+
+ @property
+ def has_items(self):
+ return self._item_count > 0
+
+ def reset(self):
+ self.clear()
+ self._item_count = 0
+ self.default_index = 0
+
+ intents_preset = (
+ get_presets()
+ .get("tools", {})
+ .get("pyblish", {})
+ .get("ui", {})
+ .get("intents", {})
+ )
+ default = intents_preset.get("default")
+ items = intents_preset.get("items", {})
+ if not items:
+ return
+
+ for idx, item_value in enumerate(items.keys()):
+ if item_value == default:
+ self.default_index = idx
+ break
+
+ self.add_items(items)
+
+ def add_items(self, items):
+ for value, label in items.items():
+ new_item = QtGui.QStandardItem()
+ new_item.setData(label, QtCore.Qt.DisplayRole)
+ new_item.setData(value, Roles.IntentItemValue)
+
+ self.setItem(self._item_count, new_item)
+ self._item_count += 1
+
+
+class PluginItem(QtGui.QStandardItem):
+ """Plugin item implementation."""
+
+ def __init__(self, plugin):
+ super(PluginItem, self).__init__()
+
+ item_text = plugin.__name__
+ if settings.UseLabel:
+ if hasattr(plugin, "label") and plugin.label:
+ item_text = plugin.label
+
+ self.plugin = plugin
+
+ self.setData(item_text, QtCore.Qt.DisplayRole)
+ self.setData(False, Roles.IsEnabledRole)
+ self.setData(0, Roles.PublishFlagsRole)
+ self.setData(0, Roles.PluginActionProgressRole)
+ icon_name = ""
+ if hasattr(plugin, "icon") and plugin.icon:
+ icon_name = plugin.icon
+ icon = QAwesomeTextIconFactory.icon(icon_name)
+ self.setData(icon, QtCore.Qt.DecorationRole)
+
+ actions = []
+ if hasattr(plugin, "actions") and plugin.actions:
+ actions = list(plugin.actions)
+ plugin.actions = actions
+
+ is_checked = True
+ is_optional = getattr(plugin, "optional", False)
+ if is_optional:
+ is_checked = getattr(plugin, "active", True)
+
+ plugin.active = is_checked
+ plugin.optional = is_optional
+
+ self.setData(
+ "{}.{}".format(plugin.__module__, plugin.__name__),
+ Roles.ObjectUIdRole
+ )
+
+ self.setFlags(
+ QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
+ )
+
+ def type(self):
+ return PluginType
+
+ def data(self, role=QtCore.Qt.DisplayRole):
+ if role == Roles.IsOptionalRole:
+ return self.plugin.optional
+
+ if role == Roles.ObjectIdRole:
+ return self.plugin.id
+
+ if role == Roles.TypeRole:
+ return self.type()
+
+ if role == QtCore.Qt.CheckStateRole:
+ return self.plugin.active
+
+ if role == Roles.PathModuleRole:
+ return self.plugin.__module__
+
+ if role == Roles.FamiliesRole:
+ return self.plugin.families
+
+ if role == Roles.DocstringRole:
+ return self.plugin.__doc__
+
+ if role == Roles.PluginActionsVisibleRole:
+ return self._data_actions_visible()
+
+ if role == Roles.PluginValidActionsRole:
+ return self._data_valid_actions()
+
+ return super(PluginItem, self).data(role)
+
+ def _data_actions_visible(self):
+ # Can only run actions on active plug-ins.
+ if not self.plugin.active or not self.plugin.actions:
+ return False
+
+ publish_states = self.data(Roles.PublishFlagsRole)
+ if (
+ not publish_states & PluginStates.IsCompatible
+ or publish_states & PluginStates.WasSkipped
+ ):
+ return False
+
+ # Context specific actions
+ for action in self.plugin.actions:
+ if action.on == "failed":
+ if publish_states & PluginStates.HasError:
+ return True
+
+ elif action.on == "succeeded":
+ if (
+ publish_states & PluginStates.WasProcessed
+ and not publish_states & PluginStates.HasError
+ ):
+ return True
+
+ elif action.on == "processed":
+ if publish_states & PluginStates.WasProcessed:
+ return True
+
+ elif action.on == "notProcessed":
+ if not publish_states & PluginStates.WasProcessed:
+ return True
+ return False
+
+ def _data_valid_actions(self):
+ valid_actions = []
+
+ # Can only run actions on active plug-ins.
+ if not self.plugin.active or not self.plugin.actions:
+ return valid_actions
+
+ if not self.plugin.active or not self.plugin.actions:
+ return False
+
+ publish_states = self.data(Roles.PublishFlagsRole)
+ if (
+ not publish_states & PluginStates.IsCompatible
+ or publish_states & PluginStates.WasSkipped
+ ):
+ return False
+
+ # Context specific actions
+ for action in self.plugin.actions:
+ valid = False
+ if action.on == "failed":
+ if publish_states & PluginStates.HasError:
+ valid = True
+
+ elif action.on == "succeeded":
+ if (
+ publish_states & PluginStates.WasProcessed
+ and not publish_states & PluginStates.HasError
+ ):
+ valid = True
+
+ elif action.on == "processed":
+ if publish_states & PluginStates.WasProcessed:
+ valid = True
+
+ elif action.on == "notProcessed":
+ if not publish_states & PluginStates.WasProcessed:
+ valid = True
+
+ if valid:
+ valid_actions.append(action)
+
+ if not valid_actions:
+ return valid_actions
+
+ actions_len = len(valid_actions)
+ # Discard empty groups
+ indexex_to_remove = []
+ for idx, action in enumerate(valid_actions):
+ if action.__type__ != "category":
+ continue
+
+ next_id = idx + 1
+ if next_id >= actions_len:
+ indexex_to_remove.append(idx)
+ continue
+
+ next = valid_actions[next_id]
+ if next.__type__ != "action":
+ indexex_to_remove.append(idx)
+
+ for idx in reversed(indexex_to_remove):
+ valid_actions.pop(idx)
+
+ return valid_actions
+
+ def setData(self, value, role=None):
+ if role is None:
+ role = QtCore.Qt.UserRole + 1
+
+ if role == QtCore.Qt.CheckStateRole:
+ if not self.data(Roles.IsEnabledRole):
+ return False
+ self.plugin.active = value
+ self.emitDataChanged()
+ return True
+
+ elif role == Roles.PluginActionProgressRole:
+ if isinstance(value, list):
+ _value = self.data(Roles.PluginActionProgressRole)
+ for flag in value:
+ _value |= flag
+ value = _value
+
+ elif isinstance(value, dict):
+ _value = self.data(Roles.PluginActionProgressRole)
+ for flag, _bool in value.items():
+ if _bool is True:
+ _value |= flag
+ elif _value & flag:
+ _value ^= flag
+ value = _value
+
+ elif role == Roles.PublishFlagsRole:
+ if isinstance(value, list):
+ _value = self.data(Roles.PublishFlagsRole)
+ for flag in value:
+ _value |= flag
+ value = _value
+
+ elif isinstance(value, dict):
+ _value = self.data(Roles.PublishFlagsRole)
+ for flag, _bool in value.items():
+ if _bool is True:
+ _value |= flag
+ elif _value & flag:
+ _value ^= flag
+ value = _value
+
+ if value & PluginStates.HasWarning:
+ if self.parent():
+ self.parent().setData(
+ {GroupStates.HasWarning: True},
+ Roles.PublishFlagsRole
+ )
+ if value & PluginStates.HasError:
+ if self.parent():
+ self.parent().setData(
+ {GroupStates.HasError: True},
+ Roles.PublishFlagsRole
+ )
+
+ return super(PluginItem, self).setData(value, role)
+
+
+class GroupItem(QtGui.QStandardItem):
+ def __init__(self, *args, **kwargs):
+ self.order = kwargs.pop("order", None)
+ self.publish_states = 0
+ super(GroupItem, self).__init__(*args, **kwargs)
+
+ def flags(self):
+ return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
+
+ def data(self, role=QtCore.Qt.DisplayRole):
+ if role == Roles.PublishFlagsRole:
+ return self.publish_states
+
+ if role == Roles.TypeRole:
+ return self.type()
+
+ return super(GroupItem, self).data(role)
+
+ def setData(self, value, role=(QtCore.Qt.UserRole + 1)):
+ if role == Roles.PublishFlagsRole:
+ if isinstance(value, list):
+ _value = self.data(Roles.PublishFlagsRole)
+ for flag in value:
+ _value |= flag
+ value = _value
+
+ elif isinstance(value, dict):
+ _value = self.data(Roles.PublishFlagsRole)
+ for flag, _bool in value.items():
+ if _bool is True:
+ _value |= flag
+ elif _value & flag:
+ _value ^= flag
+ value = _value
+ self.publish_states = value
+ self.emitDataChanged()
+ return True
+
+ return super(GroupItem, self).setData(value, role)
+
+ def type(self):
+ return GroupType
+
+
+class PluginModel(QtGui.QStandardItemModel):
+ def __init__(self, controller, *args, **kwargs):
+ super(PluginModel, self).__init__(*args, **kwargs)
+
+ self.controller = controller
+ self.checkstates = {}
+ self.group_items = {}
+ self.plugin_items = {}
+
+ def reset(self):
+ self.group_items = {}
+ self.plugin_items = {}
+ self.clear()
+
+ def append(self, plugin):
+ plugin_groups = self.controller.order_groups.groups()
+ label = None
+ order = None
+ for _order, _label in reversed(plugin_groups.items()):
+ if _order is None or plugin.order < _order:
+ label = _label
+ order = _order
+ else:
+ break
+
+ if label is None:
+ label = "Other"
+
+ if order is None:
+ order = 99999999999999
+
+ group_item = self.group_items.get(label)
+ if not group_item:
+ group_item = GroupItem(label, order=order)
+ self.appendRow(group_item)
+ self.group_items[label] = group_item
+
+ new_item = PluginItem(plugin)
+ group_item.appendRow(new_item)
+
+ self.plugin_items[plugin._id] = new_item
+
+ def store_checkstates(self):
+ self.checkstates.clear()
+
+ for plugin_item in self.plugin_items.values():
+ if not plugin_item.plugin.optional:
+ continue
+
+ uid = plugin_item.data(Roles.ObjectUIdRole)
+ self.checkstates[uid] = plugin_item.data(QtCore.Qt.CheckStateRole)
+
+ def restore_checkstates(self):
+ for plugin_item in self.plugin_items.values():
+ if not plugin_item.plugin.optional:
+ continue
+
+ uid = plugin_item.data(Roles.ObjectUIdRole)
+ state = self.checkstates.get(uid)
+ if state is not None:
+ plugin_item.setData(state, QtCore.Qt.CheckStateRole)
+
+ def update_with_result(self, result):
+ plugin = result["plugin"]
+ item = self.plugin_items[plugin.id]
+
+ new_flag_states = {
+ PluginStates.InProgress: False,
+ PluginStates.WasProcessed: True
+ }
+
+ publish_states = item.data(Roles.PublishFlagsRole)
+
+ has_warning = publish_states & PluginStates.HasWarning
+ new_records = result.get("records") or []
+ if not has_warning:
+ for record in new_records:
+ if not hasattr(record, "levelname"):
+ continue
+
+ if str(record.levelname).lower() in [
+ "warning", "critical", "error"
+ ]:
+ new_flag_states[PluginStates.HasWarning] = True
+ break
+
+ if (
+ not publish_states & PluginStates.HasError
+ and not result["success"]
+ ):
+ new_flag_states[PluginStates.HasError] = True
+
+ item.setData(new_flag_states, Roles.PublishFlagsRole)
+
+ records = item.data(Roles.LogRecordsRole) or []
+ records.extend(new_records)
+
+ item.setData(records, Roles.LogRecordsRole)
+
+ return item
+
+ def update_compatibility(self):
+ context = self.controller.context
+
+ families = util.collect_families_from_instances(context, True)
+ for plugin_item in self.plugin_items.values():
+ publish_states = plugin_item.data(Roles.PublishFlagsRole)
+ if (
+ publish_states & PluginStates.WasProcessed
+ or publish_states & PluginStates.WasSkipped
+ ):
+ continue
+
+ is_compatible = False
+ # A plugin should always show if it has processed.
+ if plugin_item.plugin.__instanceEnabled__:
+ compatible_instances = pyblish.logic.instances_by_plugin(
+ context, plugin_item.plugin
+ )
+ for instance in context:
+ if not instance.data.get("publish"):
+ continue
+
+ if instance in compatible_instances:
+ is_compatible = True
+ break
+ else:
+ plugins = pyblish.logic.plugins_by_families(
+ [plugin_item.plugin], families
+ )
+ if plugins:
+ is_compatible = True
+
+ current_is_compatible = publish_states & PluginStates.IsCompatible
+ if (
+ (is_compatible and not current_is_compatible)
+ or (not is_compatible and current_is_compatible)
+ ):
+ new_flag = {
+ PluginStates.IsCompatible: is_compatible
+ }
+ plugin_item.setData(new_flag, Roles.PublishFlagsRole)
+
+
+class PluginFilterProxy(QtCore.QSortFilterProxyModel):
+ def filterAcceptsRow(self, source_row, source_parent):
+ index = self.sourceModel().index(source_row, 0, source_parent)
+ item_type = index.data(Roles.TypeRole)
+ if item_type != PluginType:
+ return True
+
+ publish_states = index.data(Roles.PublishFlagsRole)
+ if (
+ publish_states & PluginStates.WasSkipped
+ or not publish_states & PluginStates.IsCompatible
+ ):
+ return False
+ return True
+
+
+class InstanceItem(QtGui.QStandardItem):
+ """Instance item implementation."""
+
+ def __init__(self, instance):
+ super(InstanceItem, self).__init__()
+
+ self.instance = instance
+ self.is_context = False
+ publish_states = getattr(instance, "_publish_states", 0)
+ if publish_states & InstanceStates.ContextType:
+ self.is_context = True
+
+ instance._publish_states = publish_states
+ instance._logs = []
+ instance.optional = getattr(instance, "optional", True)
+ instance.data["publish"] = instance.data.get("publish", True)
+ instance.data["label"] = (
+ instance.data.get("label")
+ or getattr(instance, "label", None)
+ or instance.data["name"]
+ )
+
+ family = self.data(Roles.FamiliesRole)[0]
+ self.setData(
+ "{}.{}".format(family, self.instance.data["name"]),
+ Roles.ObjectUIdRole
+ )
+
+ def flags(self):
+ return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
+
+ def type(self):
+ return InstanceType
+
+ def data(self, role=QtCore.Qt.DisplayRole):
+ if role == QtCore.Qt.DisplayRole:
+ if settings.UseLabel:
+ return self.instance.data["label"]
+ return self.instance.data["name"]
+
+ if role == QtCore.Qt.DecorationRole:
+ icon_name = self.instance.data.get("icon") or "file"
+ return QAwesomeTextIconFactory.icon(icon_name)
+
+ if role == Roles.TypeRole:
+ return self.type()
+
+ if role == Roles.ObjectIdRole:
+ return self.instance.id
+
+ if role == Roles.FamiliesRole:
+ if self.is_context:
+ return ["Context"]
+
+ families = []
+ family = self.instance.data.get("family")
+ if family:
+ families.append(family)
+
+ _families = self.instance.data.get("families") or []
+ for _family in _families:
+ if _family not in families:
+ families.append(_family)
+
+ return families
+
+ if role == Roles.IsOptionalRole:
+ return self.instance.optional
+
+ if role == QtCore.Qt.CheckStateRole:
+ return self.instance.data["publish"]
+
+ if role == Roles.PublishFlagsRole:
+ return self.instance._publish_states
+
+ if role == Roles.LogRecordsRole:
+ return self.instance._logs
+
+ return super(InstanceItem, self).data(role)
+
+ def setData(self, value, role=(QtCore.Qt.UserRole + 1)):
+ if role == QtCore.Qt.CheckStateRole:
+ if not self.data(Roles.IsEnabledRole):
+ return False
+ self.instance.data["publish"] = value
+ self.emitDataChanged()
+ return True
+
+ if role == Roles.IsEnabledRole:
+ if not self.instance.optional:
+ return False
+
+ if role == Roles.PublishFlagsRole:
+ if isinstance(value, list):
+ _value = self.instance._publish_states
+ for flag in value:
+ _value |= flag
+ value = _value
+
+ elif isinstance(value, dict):
+ _value = self.instance._publish_states
+ for flag, _bool in value.items():
+ if _bool is True:
+ _value |= flag
+ elif _value & flag:
+ _value ^= flag
+ value = _value
+
+ if value & InstanceStates.HasWarning:
+ if self.parent():
+ self.parent().setData(
+ {GroupStates.HasWarning: True},
+ Roles.PublishFlagsRole
+ )
+ if value & InstanceStates.HasError:
+ if self.parent():
+ self.parent().setData(
+ {GroupStates.HasError: True},
+ Roles.PublishFlagsRole
+ )
+
+ self.instance._publish_states = value
+ self.emitDataChanged()
+ return True
+
+ if role == Roles.LogRecordsRole:
+ self.instance._logs = value
+ self.emitDataChanged()
+ return True
+
+ return super(InstanceItem, self).setData(value, role)
+
+
+class InstanceModel(QtGui.QStandardItemModel):
+
+ group_created = QtCore.Signal(QtCore.QModelIndex)
+
+ def __init__(self, controller, *args, **kwargs):
+ super(InstanceModel, self).__init__(*args, **kwargs)
+
+ self.controller = controller
+ self.checkstates = {}
+ self.group_items = {}
+ self.instance_items = {}
+
+ def reset(self):
+ self.group_items = {}
+ self.instance_items = {}
+ self.clear()
+
+ def append(self, instance):
+ new_item = InstanceItem(instance)
+ families = new_item.data(Roles.FamiliesRole)
+ group_item = self.group_items.get(families[0])
+ if not group_item:
+ group_item = GroupItem(families[0])
+ self.appendRow(group_item)
+ self.group_items[families[0]] = group_item
+ self.group_created.emit(group_item.index())
+
+ group_item.appendRow(new_item)
+ instance_id = instance.id
+ self.instance_items[instance_id] = new_item
+
+ def remove(self, instance_id):
+ instance_item = self.instance_items.pop(instance_id)
+ parent_item = instance_item.parent()
+ parent_item.removeRow(instance_item.row())
+ if parent_item.rowCount():
+ return
+
+ self.group_items.pop(parent_item.data(QtCore.Qt.DisplayRole))
+ self.removeRow(parent_item.row())
+
+ def store_checkstates(self):
+ self.checkstates.clear()
+
+ for instance_item in self.instance_items.values():
+ if not instance_item.instance.optional:
+ continue
+
+ uid = instance_item.data(Roles.ObjectUIdRole)
+ self.checkstates[uid] = instance_item.data(
+ QtCore.Qt.CheckStateRole
+ )
+
+ def restore_checkstates(self):
+ for instance_item in self.instance_items.values():
+ if not instance_item.instance.optional:
+ continue
+
+ uid = instance_item.data(Roles.ObjectUIdRole)
+ state = self.checkstates.get(uid)
+ if state is not None:
+ instance_item.setData(state, QtCore.Qt.CheckStateRole)
+
+ def update_with_result(self, result):
+ instance = result["instance"]
+ if instance is None:
+ instance_id = self.controller.context.id
+ else:
+ instance_id = instance.id
+
+ item = self.instance_items.get(instance_id)
+ if not item:
+ return
+
+ new_flag_states = {
+ InstanceStates.InProgress: False
+ }
+
+ publish_states = item.data(Roles.PublishFlagsRole)
+ has_warning = publish_states & InstanceStates.HasWarning
+ new_records = result.get("records") or []
+ if not has_warning:
+ for record in new_records:
+ if not hasattr(record, "levelname"):
+ continue
+
+ if str(record.levelname).lower() in [
+ "warning", "critical", "error"
+ ]:
+ new_flag_states[InstanceStates.HasWarning] = True
+ break
+
+ if (
+ not publish_states & InstanceStates.HasError
+ and not result["success"]
+ ):
+ new_flag_states[InstanceStates.HasError] = True
+
+ item.setData(new_flag_states, Roles.PublishFlagsRole)
+
+ records = item.data(Roles.LogRecordsRole) or []
+ records.extend(new_records)
+
+ item.setData(records, Roles.LogRecordsRole)
+
+ return item
+
+ def update_compatibility(self, context, instances):
+ families = util.collect_families_from_instances(context, True)
+ for plugin_item in self.plugin_items.values():
+ publish_states = plugin_item.data(Roles.PublishFlagsRole)
+ if (
+ publish_states & PluginStates.WasProcessed
+ or publish_states & PluginStates.WasSkipped
+ ):
+ continue
+
+ is_compatible = False
+ # A plugin should always show if it has processed.
+ if plugin_item.plugin.__instanceEnabled__:
+ compatibleInstances = pyblish.logic.instances_by_plugin(
+ context, plugin_item.plugin
+ )
+ for instance in instances:
+ if not instance.data.get("publish"):
+ continue
+
+ if instance in compatibleInstances:
+ is_compatible = True
+ break
+ else:
+ plugins = pyblish.logic.plugins_by_families(
+ [plugin_item.plugin], families
+ )
+ if plugins:
+ is_compatible = True
+
+ current_is_compatible = publish_states & PluginStates.IsCompatible
+ if (
+ (is_compatible and not current_is_compatible)
+ or (not is_compatible and current_is_compatible)
+ ):
+ plugin_item.setData(
+ {PluginStates.IsCompatible: is_compatible},
+ Roles.PublishFlagsRole
+ )
+
+
+class ArtistProxy(QtCore.QAbstractProxyModel):
+ def __init__(self, *args, **kwargs):
+ self.mapping_from = []
+ self.mapping_to = []
+ super(ArtistProxy, self).__init__(*args, **kwargs)
+
+ def on_rows_inserted(self, parent_index, from_row, to_row):
+ if not parent_index.isValid():
+ return
+
+ parent_row = parent_index.row()
+ if parent_row >= len(self.mapping_from):
+ self.mapping_from.append(list())
+
+ new_from = None
+ new_to = None
+ for row_num in range(from_row, to_row + 1):
+ new_row = len(self.mapping_to)
+ new_to = new_row
+ if new_from is None:
+ new_from = new_row
+
+ self.mapping_from[parent_row].insert(row_num, new_row)
+ self.mapping_to.insert(new_row, [parent_row, row_num])
+
+ self.rowsInserted.emit(self.parent(), new_from, new_to + 1)
+
+ def _remove_rows(self, parent_row, from_row, to_row):
+ removed_rows = []
+ increment_num = self.mapping_from[parent_row][from_row]
+ _emit_last = None
+ for row_num in reversed(range(from_row, to_row + 1)):
+ row = self.mapping_from[parent_row].pop(row_num)
+ _emit_last = row
+ removed_rows.append(row)
+
+ _emit_first = int(increment_num)
+ mapping_from_len = len(self.mapping_from)
+ mapping_from_parent_len = len(self.mapping_from[parent_row])
+ if parent_row < mapping_from_len:
+ for idx in range(from_row, mapping_from_parent_len):
+ self.mapping_from[parent_row][idx] = increment_num
+ increment_num += 1
+
+ if parent_row < mapping_from_len - 1:
+ for idx_i in range(parent_row + 1, mapping_from_len):
+ sub_values = self.mapping_from[idx_i]
+ if not sub_values:
+ continue
+
+ for idx_j in range(0, len(sub_values)):
+ self.mapping_from[idx_i][idx_j] = increment_num
+ increment_num += 1
+
+ first_to_row = None
+ for row in removed_rows:
+ if first_to_row is None:
+ first_to_row = row
+ self.mapping_to.pop(row)
+
+ return (_emit_first, _emit_last)
+
+ def on_rows_removed(self, parent_index, from_row, to_row):
+ if parent_index.isValid():
+ parent_row = parent_index.row()
+ _emit_first, _emit_last = self._remove_rows(
+ parent_row, from_row, to_row
+ )
+ self.rowsRemoved.emit(self.parent(), _emit_first, _emit_last)
+
+ else:
+ removed_rows = False
+ emit_first = None
+ emit_last = None
+ for row_num in reversed(range(from_row, to_row + 1)):
+ remaining_rows = self.mapping_from[row_num]
+ if remaining_rows:
+ removed_rows = True
+ _emit_first, _emit_last = self._remove_rows(
+ row_num, 0, len(remaining_rows) - 1
+ )
+ if emit_first is None:
+ emit_first = _emit_first
+ emit_last = _emit_last
+
+ self.mapping_from.pop(row_num)
+
+ diff = to_row - from_row + 1
+ mapping_to_len = len(self.mapping_to)
+ if from_row < mapping_to_len:
+ for idx in range(from_row, mapping_to_len):
+ self.mapping_to[idx][0] -= diff
+
+ if removed_rows:
+ self.rowsRemoved.emit(self.parent(), emit_first, emit_last)
+
+ def on_reset(self):
+ self.modelReset.emit()
+ self.mapping_from = []
+ self.mapping_to = []
+
+ def setSourceModel(self, source_model):
+ super(ArtistProxy, self).setSourceModel(source_model)
+ source_model.rowsInserted.connect(self.on_rows_inserted)
+ source_model.rowsRemoved.connect(self.on_rows_removed)
+ source_model.modelReset.connect(self.on_reset)
+ source_model.dataChanged.connect(self.on_data_changed)
+
+ def on_data_changed(self, from_index, to_index, roles=None):
+ proxy_from_index = self.mapFromSource(from_index)
+ if from_index == to_index:
+ proxy_to_index = proxy_from_index
+ else:
+ proxy_to_index = self.mapFromSource(to_index)
+
+ args = [proxy_from_index, proxy_to_index]
+ if Qt.__binding__ not in ("PyQt4", "PySide"):
+ args.append(roles or [])
+ self.dataChanged.emit(*args)
+
+ def columnCount(self, parent=QtCore.QModelIndex()):
+ # This is not right for global proxy, but in this case it is enough
+ return self.sourceModel().columnCount()
+
+ def rowCount(self, parent=QtCore.QModelIndex()):
+ if parent.isValid():
+ return 0
+ return len(self.mapping_to)
+
+ def mapFromSource(self, index):
+ if not index.isValid():
+ return QtCore.QModelIndex()
+
+ parent_index = index.parent()
+ if not parent_index.isValid():
+ return QtCore.QModelIndex()
+
+ parent_idx = self.mapping_from[parent_index.row()]
+ my_row = parent_idx[index.row()]
+ return self.index(my_row, index.column())
+
+ def mapToSource(self, index):
+ if not index.isValid() or index.row() > len(self.mapping_to):
+ return self.sourceModel().index(index.row(), index.column())
+
+ parent_row, item_row = self.mapping_to[index.row()]
+ parent_index = self.sourceModel().index(parent_row, 0)
+ return self.sourceModel().index(item_row, 0, parent_index)
+
+ def index(self, row, column, parent=QtCore.QModelIndex()):
+ return self.createIndex(row, column, QtCore.QModelIndex())
+
+ def parent(self, index=None):
+ return QtCore.QModelIndex()
+
+
+class TerminalModel(QtGui.QStandardItemModel):
+ key_label_record_map = (
+ ("instance", "Instance"),
+ ("msg", "Message"),
+ ("name", "Plugin"),
+ ("pathname", "Path"),
+ ("lineno", "Line"),
+ ("traceback", "Traceback"),
+ ("levelname", "Level"),
+ ("threadName", "Thread"),
+ ("msecs", "Millis")
+ )
+
+ item_icon_name = {
+ "info": "fa.info",
+ "record": "fa.circle",
+ "error": "fa.exclamation-triangle",
+ }
+
+ item_icon_colors = {
+ "info": "#ffffff",
+ "error": "#ff4a4a",
+ "log_debug": "#ff66e8",
+ "log_info": "#66abff",
+ "log_warning": "#ffba66",
+ "log_error": "#ff4d58",
+ "log_critical": "#ff4f75",
+ None: "#333333"
+ }
+
+ level_to_record = (
+ (10, "log_debug"),
+ (20, "log_info"),
+ (30, "log_warning"),
+ (40, "log_error"),
+ (50, "log_critical")
+
+ )
+
+ def __init__(self, *args, **kwargs):
+ super(TerminalModel, self).__init__(*args, **kwargs)
+ self.reset()
+
+ def reset(self):
+ self.items_to_set_widget = queue.Queue()
+ self.clear()
+
+ def prepare_records(self, result):
+ prepared_records = []
+ instance_name = None
+ instance = result["instance"]
+ if instance is not None:
+ instance_name = instance.data["name"]
+
+ for record in result.get("records") or []:
+ if isinstance(record, dict):
+ record_item = record
+ else:
+ record_item = {
+ "label": text_type(record.msg),
+ "type": "record",
+ "levelno": record.levelno,
+ "threadName": record.threadName,
+ "name": record.name,
+ "filename": record.filename,
+ "pathname": record.pathname,
+ "lineno": record.lineno,
+ "msg": text_type(record.msg),
+ "msecs": record.msecs,
+ "levelname": record.levelname
+ }
+
+ if instance_name is not None:
+ record_item["instance"] = instance_name
+
+ prepared_records.append(record_item)
+
+ error = result.get("error")
+ if error:
+ fname, line_no, func, exc = error.traceback
+ error_item = {
+ "label": str(error),
+ "type": "error",
+ "filename": str(fname),
+ "lineno": str(line_no),
+ "func": str(func),
+ "traceback": error.formatted_traceback,
+ }
+
+ if instance_name is not None:
+ error_item["instance"] = instance_name
+
+ prepared_records.append(error_item)
+
+ return prepared_records
+
+ def append(self, record_item):
+ record_type = record_item["type"]
+
+ terminal_item_type = None
+ if record_type == "record":
+ for level, _type in self.level_to_record:
+ if level > record_item["levelno"]:
+ break
+ terminal_item_type = _type
+
+ else:
+ terminal_item_type = record_type
+
+ icon_color = self.item_icon_colors.get(terminal_item_type)
+ icon_name = self.item_icon_name.get(record_type)
+
+ top_item_icon = None
+ if icon_color and icon_name:
+ top_item_icon = QAwesomeIconFactory.icon(icon_name, icon_color)
+
+ label = record_item["label"].split("\n")[0]
+
+ top_item = QtGui.QStandardItem()
+ top_item.setData(TerminalLabelType, Roles.TypeRole)
+ top_item.setData(terminal_item_type, Roles.TerminalItemTypeRole)
+ top_item.setData(label, QtCore.Qt.DisplayRole)
+ top_item.setFlags(
+ QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
+ )
+
+ if top_item_icon:
+ top_item.setData(top_item_icon, QtCore.Qt.DecorationRole)
+
+ self.appendRow(top_item)
+
+ detail_text = self.prepare_detail_text(record_item)
+ detail_item = QtGui.QStandardItem(detail_text)
+ detail_item.setData(TerminalDetailType, Roles.TypeRole)
+ top_item.appendRow(detail_item)
+ self.items_to_set_widget.put(detail_item)
+
+ def update_with_result(self, result):
+ for record in result["records"]:
+ self.append(record)
+
+ def prepare_detail_text(self, item_data):
+ if item_data["type"] == "info":
+ return item_data["label"]
+
+ html_text = ""
+ for key, title in self.key_label_record_map:
+ if key not in item_data:
+ continue
+ value = item_data[key]
+ text = (
+ str(value)
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace('\n', '
')
+ .replace(' ', ' ')
+ )
+
+ title_tag = (
+ '{}: '
+ ' color:#fff;\" >{}: '
+ ).format(title)
+
+ html_text += (
+ '
| {} |
'
+ '| {} |
'
+ ).format(title_tag, text)
+
+ html_text = ''.format(
+ html_text
+ )
+ return html_text
+
+
+class TerminalProxy(QtCore.QSortFilterProxyModel):
+ filter_buttons_checks = {
+ "info": settings.TerminalFilters.get("info", True),
+ "log_debug": settings.TerminalFilters.get("log_debug", True),
+ "log_info": settings.TerminalFilters.get("log_info", True),
+ "log_warning": settings.TerminalFilters.get("log_warning", True),
+ "log_error": settings.TerminalFilters.get("log_error", True),
+ "log_critical": settings.TerminalFilters.get("log_critical", True),
+ "error": settings.TerminalFilters.get("error", True)
+ }
+
+ instances = []
+
+ def __init__(self, view, *args, **kwargs):
+ super(self.__class__, self).__init__(*args, **kwargs)
+ self.__class__.instances.append(self)
+ # Store parent because by own `QSortFilterProxyModel` has `parent`
+ # method not returning parent QObject in PySide and PyQt4
+ self.view = view
+
+ @classmethod
+ def change_filter(cls, name, value):
+ cls.filter_buttons_checks[name] = value
+
+ for instance in cls.instances:
+ try:
+ instance.invalidate()
+ if instance.view:
+ instance.view.updateGeometry()
+
+ except RuntimeError:
+ # C++ Object was deleted
+ cls.instances.remove(instance)
+
+ def filterAcceptsRow(self, source_row, source_parent):
+ index = self.sourceModel().index(source_row, 0, source_parent)
+ item_type = index.data(Roles.TypeRole)
+ if not item_type == TerminalLabelType:
+ return True
+ terminal_item_type = index.data(Roles.TerminalItemTypeRole)
+ return self.__class__.filter_buttons_checks.get(
+ terminal_item_type, True
+ )
diff --git a/pype/tools/pyblish_pype/settings.py b/pype/tools/pyblish_pype/settings.py
new file mode 100644
index 0000000000..a3ae83ff0a
--- /dev/null
+++ b/pype/tools/pyblish_pype/settings.py
@@ -0,0 +1,19 @@
+WindowTitle = "Pyblish" # Customize the window of the pyblish-lite window.
+UseLabel = True # Customize whether to show label names for plugins.
+
+# Customize which tab to start on. Possible choices are: "artist", "overview"
+# and "terminal".
+InitialTab = "artist"
+
+# Customize the window size.
+WindowSize = (430, 600)
+
+TerminalFilters = {
+ "info": True,
+ "log_debug": True,
+ "log_info": True,
+ "log_warning": True,
+ "log_error": True,
+ "log_critical": True,
+ "traceback": True,
+}
diff --git a/pype/tools/pyblish_pype/util.py b/pype/tools/pyblish_pype/util.py
new file mode 100644
index 0000000000..82bf4eb51d
--- /dev/null
+++ b/pype/tools/pyblish_pype/util.py
@@ -0,0 +1,311 @@
+from __future__ import (absolute_import, division, print_function,
+ unicode_literals)
+
+import os
+import sys
+import numbers
+import copy
+import collections
+
+from .vendor.Qt import QtCore
+from .vendor.six import text_type
+import pyblish.api
+
+root = os.path.dirname(__file__)
+
+
+def get_asset(*path):
+ """Return path to asset, relative the install directory
+
+ Usage:
+ >>> path = get_asset("dir", "to", "asset.png")
+ >>> path == os.path.join(root, "dir", "to", "asset.png")
+ True
+
+ Arguments:
+ path (str): One or more paths, to be concatenated
+
+ """
+
+ return os.path.join(root, *path)
+
+
+def defer(delay, func):
+ """Append artificial delay to `func`
+
+ This aids in keeping the GUI responsive, but complicates logic
+ when producing tests. To combat this, the environment variable ensures
+ that every operation is synchonous.
+
+ Arguments:
+ delay (float): Delay multiplier; default 1, 0 means no delay
+ func (callable): Any callable
+
+ """
+
+ delay *= float(os.getenv("PYBLISH_DELAY", 1))
+ if delay > 0:
+ return QtCore.QTimer.singleShot(delay, func)
+ else:
+ return func()
+
+
+def u_print(msg, **kwargs):
+ """`print` with encoded unicode.
+
+ `print` unicode may cause UnicodeEncodeError
+ or non-readable result when `PYTHONIOENCODING` is not set.
+ this will fix it.
+
+ Arguments:
+ msg (unicode): Message to print.
+ **kwargs: Keyword argument for `print` function.
+ """
+
+ if isinstance(msg, text_type):
+ encoding = None
+ try:
+ encoding = os.getenv('PYTHONIOENCODING', sys.stdout.encoding)
+ except AttributeError:
+ # `sys.stdout.encoding` may not exists.
+ pass
+ msg = msg.encode(encoding or 'utf-8', 'replace')
+ print(msg, **kwargs)
+
+
+def collect_families_from_instances(instances, only_active=False):
+ all_families = set()
+ for instance in instances:
+ if only_active:
+ if instance.data.get("publish") is False:
+ continue
+ family = instance.data.get("family")
+ if family:
+ all_families.add(family)
+
+ families = instance.data.get("families") or tuple()
+ for family in families:
+ all_families.add(family)
+
+ return list(all_families)
+
+
+class OrderGroups:
+ # Validator order can be set with environment "PYBLISH_VALIDATION_ORDER"
+ # - this variable sets when validation button will hide and proecssing
+ # of validation will end with ability to continue in process
+ default_validation_order = pyblish.api.ValidatorOrder + 0.5
+
+ # Group range can be set with environment "PYBLISH_GROUP_RANGE"
+ default_group_range = 1
+
+ # Group string can be set with environment "PYBLISH_GROUP_SETTING"
+ default_groups = {
+ pyblish.api.CollectorOrder + 0.5: "Collect",
+ pyblish.api.ValidatorOrder + 0.5: "Validate",
+ pyblish.api.ExtractorOrder + 0.5: "Extract",
+ pyblish.api.IntegratorOrder + 0.5: "Integrate",
+ None: "Other"
+ }
+
+ # *** This example should have same result as is `default_groups` if
+ # `group_range` is set to "1"
+ __groups_str_example__ = (
+ # half of `group_range` is added to 0 because number means it is Order
+ "0=Collect"
+ # if `<` is before than it means group range is not used
+ # but is expected that number is already max
+ ",<1.5=Validate"
+ # "Extractor" will be used in range `<1.5; 2.5)`
+ ",<2.5=Extract"
+ ",<3.5=Integrate"
+ # "Other" if number is not set than all remaining plugins are in
+ # - in this case Other's range is <3.5; infinity)
+ ",Other"
+ )
+
+ _groups = None
+ _validation_order = None
+ _group_range = None
+
+ def __init__(
+ self, group_str=None, group_range=None, validation_order=None
+ ):
+ super(OrderGroups, self).__init__()
+ # Override class methods with object methods
+ self.groups = self._object_groups
+ self.validation_order = self._object_validation_order
+ self.group_range = self._object_group_range
+ self.reset = self._object_reset
+
+ # set
+ if group_range is not None:
+ self._group_range = self.parse_group_range(
+ group_range
+ )
+
+ if group_str is not None:
+ self._groups = self.parse_group_str(
+ group_str
+ )
+
+ if validation_order is not None:
+ self._validation_order = self.parse_validation_order(
+ validation_order
+ )
+
+ @staticmethod
+ def _groups_method(obj):
+ if obj._groups is None:
+ obj._groups = obj.parse_group_str(
+ group_range=obj.group_range()
+ )
+ return obj._groups
+
+ @staticmethod
+ def _reset_method(obj):
+ obj._groups = None
+ obj._validation_order = None
+ obj._group_range = None
+
+ @classmethod
+ def reset(cls):
+ return cls._reset_method(cls)
+
+ def _object_reset(self):
+ return self._reset_method(self)
+
+ @classmethod
+ def groups(cls):
+ return cls._groups_method(cls)
+
+ def _object_groups(self):
+ return self._groups_method(self)
+
+ @staticmethod
+ def _validation_order_method(obj):
+ if obj._validation_order is None:
+ obj._validation_order = obj.parse_validation_order(
+ group_range=obj.group_range()
+ )
+ return obj._validation_order
+
+ @classmethod
+ def validation_order(cls):
+ return cls._validation_order_method(cls)
+
+ def _object_validation_order(self):
+ return self._validation_order_method(self)
+
+ @staticmethod
+ def _group_range_method(obj):
+ if obj._group_range is None:
+ obj._group_range = obj.parse_group_range()
+ return obj._group_range
+
+ @classmethod
+ def group_range(cls):
+ return cls._group_range_method(cls)
+
+ def _object_group_range(self):
+ return self._group_range_method(self)
+
+ @staticmethod
+ def sort_groups(_groups_dict):
+ sorted_dict = collections.OrderedDict()
+
+ # make sure wont affect any dictionary as pointer
+ groups_dict = copy.deepcopy(_groups_dict)
+ last_order = None
+ if None in groups_dict:
+ last_order = groups_dict.pop(None)
+
+ for key in sorted(groups_dict):
+ sorted_dict[key] = groups_dict[key]
+
+ if last_order is not None:
+ sorted_dict[None] = last_order
+
+ return sorted_dict
+
+ @staticmethod
+ def parse_group_str(groups_str=None, group_range=None):
+ if groups_str is None:
+ groups_str = os.environ.get("PYBLISH_GROUP_SETTING")
+
+ if groups_str is None:
+ return OrderGroups.sort_groups(OrderGroups.default_groups)
+
+ items = groups_str.split(",")
+ groups = {}
+ for item in items:
+ if "=" not in item:
+ order = None
+ label = item
+ else:
+ order, label = item.split("=")
+ order = order.strip()
+ if not order:
+ order = None
+ elif order.startswith("<"):
+ order = float(order.replace("<", ""))
+ else:
+ if group_range is None:
+ group_range = OrderGroups.default_group_range
+ print(
+ "Using default Plugin group range \"{}\".".format(
+ OrderGroups.default_group_range
+ )
+ )
+ order = float(order) + float(group_range) / 2
+
+ if order in groups:
+ print((
+ "Order \"{}\" is registered more than once."
+ " Using first found."
+ ).format(str(order)))
+ continue
+
+ groups[order] = label
+
+ return OrderGroups.sort_groups(groups)
+
+ @staticmethod
+ def parse_validation_order(validation_order_value=None, group_range=None):
+ if validation_order_value is None:
+ validation_order_value = os.environ.get("PYBLISH_VALIDATION_ORDER")
+
+ if validation_order_value is None:
+ return OrderGroups.default_validation_order
+
+ if group_range is None:
+ group_range = OrderGroups.default_group_range
+
+ group_range_half = float(group_range) / 2
+
+ if isinstance(validation_order_value, numbers.Integral):
+ return validation_order_value + group_range_half
+
+ if validation_order_value.startswith("<"):
+ validation_order_value = float(
+ validation_order_value.replace("<", "")
+ )
+ else:
+ validation_order_value = (
+ float(validation_order_value)
+ + group_range_half
+ )
+ return validation_order_value
+
+ @staticmethod
+ def parse_group_range(group_range=None):
+ if group_range is None:
+ group_range = os.environ.get("PYBLISH_GROUP_RANGE")
+
+ if group_range is None:
+ return OrderGroups.default_group_range
+
+ if isinstance(group_range, numbers.Integral):
+ return group_range
+
+ return float(group_range)
diff --git a/pype/tools/pyblish_pype/vendor/Qt.py b/pype/tools/pyblish_pype/vendor/Qt.py
new file mode 100644
index 0000000000..841c823c5c
--- /dev/null
+++ b/pype/tools/pyblish_pype/vendor/Qt.py
@@ -0,0 +1,1827 @@
+"""Minimal Python 2 & 3 shim around all Qt bindings
+
+DOCUMENTATION
+ Qt.py was born in the film and visual effects industry to address
+ the growing need for the development of software capable of running
+ with more than one flavour of the Qt bindings for Python - PySide,
+ PySide2, PyQt4 and PyQt5.
+
+ 1. Build for one, run with all
+ 2. Explicit is better than implicit
+ 3. Support co-existence
+
+ Default resolution order:
+ - PySide2
+ - PyQt5
+ - PySide
+ - PyQt4
+
+ Usage:
+ >> import sys
+ >> from Qt import QtWidgets
+ >> app = QtWidgets.QApplication(sys.argv)
+ >> button = QtWidgets.QPushButton("Hello World")
+ >> button.show()
+ >> app.exec_()
+
+ All members of PySide2 are mapped from other bindings, should they exist.
+ If no equivalent member exist, it is excluded from Qt.py and inaccessible.
+ The idea is to highlight members that exist across all supported binding,
+ and guarantee that code that runs on one binding runs on all others.
+
+ For more details, visit https://github.com/mottosso/Qt.py
+
+LICENSE
+
+ See end of file for license (MIT, BSD) information.
+
+"""
+
+import os
+import sys
+import types
+import shutil
+
+
+__version__ = "1.2.0.b2"
+
+# Enable support for `from Qt import *`
+__all__ = []
+
+# Flags from environment variables
+QT_VERBOSE = bool(os.getenv("QT_VERBOSE"))
+QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "")
+QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT")
+
+# Reference to Qt.py
+Qt = sys.modules[__name__]
+Qt.QtCompat = types.ModuleType("QtCompat")
+
+try:
+ long
+except NameError:
+ # Python 3 compatibility
+ long = int
+
+
+"""Common members of all bindings
+
+This is where each member of Qt.py is explicitly defined.
+It is based on a "lowest common denominator" of all bindings;
+including members found in each of the 4 bindings.
+
+The "_common_members" dictionary is generated using the
+build_membership.sh script.
+
+"""
+
+_common_members = {
+ "QtCore": [
+ "QAbstractAnimation",
+ "QAbstractEventDispatcher",
+ "QAbstractItemModel",
+ "QAbstractListModel",
+ "QAbstractState",
+ "QAbstractTableModel",
+ "QAbstractTransition",
+ "QAnimationGroup",
+ "QBasicTimer",
+ "QBitArray",
+ "QBuffer",
+ "QByteArray",
+ "QByteArrayMatcher",
+ "QChildEvent",
+ "QCoreApplication",
+ "QCryptographicHash",
+ "QDataStream",
+ "QDate",
+ "QDateTime",
+ "QDir",
+ "QDirIterator",
+ "QDynamicPropertyChangeEvent",
+ "QEasingCurve",
+ "QElapsedTimer",
+ "QEvent",
+ "QEventLoop",
+ "QEventTransition",
+ "QFile",
+ "QFileInfo",
+ "QFileSystemWatcher",
+ "QFinalState",
+ "QGenericArgument",
+ "QGenericReturnArgument",
+ "QHistoryState",
+ "QItemSelectionRange",
+ "QIODevice",
+ "QLibraryInfo",
+ "QLine",
+ "QLineF",
+ "QLocale",
+ "QMargins",
+ "QMetaClassInfo",
+ "QMetaEnum",
+ "QMetaMethod",
+ "QMetaObject",
+ "QMetaProperty",
+ "QMimeData",
+ "QModelIndex",
+ "QMutex",
+ "QMutexLocker",
+ "QObject",
+ "QParallelAnimationGroup",
+ "QPauseAnimation",
+ "QPersistentModelIndex",
+ "QPluginLoader",
+ "QPoint",
+ "QPointF",
+ "QProcess",
+ "QProcessEnvironment",
+ "QPropertyAnimation",
+ "QReadLocker",
+ "QReadWriteLock",
+ "QRect",
+ "QRectF",
+ "QRegExp",
+ "QResource",
+ "QRunnable",
+ "QSemaphore",
+ "QSequentialAnimationGroup",
+ "QSettings",
+ "QSignalMapper",
+ "QSignalTransition",
+ "QSize",
+ "QSizeF",
+ "QSocketNotifier",
+ "QState",
+ "QStateMachine",
+ "QSysInfo",
+ "QSystemSemaphore",
+ "QT_TRANSLATE_NOOP",
+ "QT_TR_NOOP",
+ "QT_TR_NOOP_UTF8",
+ "QTemporaryFile",
+ "QTextBoundaryFinder",
+ "QTextCodec",
+ "QTextDecoder",
+ "QTextEncoder",
+ "QTextStream",
+ "QTextStreamManipulator",
+ "QThread",
+ "QThreadPool",
+ "QTime",
+ "QTimeLine",
+ "QTimer",
+ "QTimerEvent",
+ "QTranslator",
+ "QUrl",
+ "QVariantAnimation",
+ "QWaitCondition",
+ "QWriteLocker",
+ "QXmlStreamAttribute",
+ "QXmlStreamAttributes",
+ "QXmlStreamEntityDeclaration",
+ "QXmlStreamEntityResolver",
+ "QXmlStreamNamespaceDeclaration",
+ "QXmlStreamNotationDeclaration",
+ "QXmlStreamReader",
+ "QXmlStreamWriter",
+ "Qt",
+ "QtCriticalMsg",
+ "QtDebugMsg",
+ "QtFatalMsg",
+ "QtMsgType",
+ "QtSystemMsg",
+ "QtWarningMsg",
+ "qAbs",
+ "qAddPostRoutine",
+ "qChecksum",
+ "qCritical",
+ "qDebug",
+ "qFatal",
+ "qFuzzyCompare",
+ "qIsFinite",
+ "qIsInf",
+ "qIsNaN",
+ "qIsNull",
+ "qRegisterResourceData",
+ "qUnregisterResourceData",
+ "qVersion",
+ "qWarning",
+ "qrand",
+ "qsrand"
+ ],
+ "QtGui": [
+ "QAbstractTextDocumentLayout",
+ "QActionEvent",
+ "QBitmap",
+ "QBrush",
+ "QClipboard",
+ "QCloseEvent",
+ "QColor",
+ "QConicalGradient",
+ "QContextMenuEvent",
+ "QCursor",
+ "QDesktopServices",
+ "QDoubleValidator",
+ "QDrag",
+ "QDragEnterEvent",
+ "QDragLeaveEvent",
+ "QDragMoveEvent",
+ "QDropEvent",
+ "QFileOpenEvent",
+ "QFocusEvent",
+ "QFont",
+ "QFontDatabase",
+ "QFontInfo",
+ "QFontMetrics",
+ "QFontMetricsF",
+ "QGradient",
+ "QHelpEvent",
+ "QHideEvent",
+ "QHoverEvent",
+ "QIcon",
+ "QIconDragEvent",
+ "QIconEngine",
+ "QImage",
+ "QImageIOHandler",
+ "QImageReader",
+ "QImageWriter",
+ "QInputEvent",
+ "QInputMethodEvent",
+ "QIntValidator",
+ "QKeyEvent",
+ "QKeySequence",
+ "QLinearGradient",
+ "QMatrix2x2",
+ "QMatrix2x3",
+ "QMatrix2x4",
+ "QMatrix3x2",
+ "QMatrix3x3",
+ "QMatrix3x4",
+ "QMatrix4x2",
+ "QMatrix4x3",
+ "QMatrix4x4",
+ "QMouseEvent",
+ "QMoveEvent",
+ "QMovie",
+ "QPaintDevice",
+ "QPaintEngine",
+ "QPaintEngineState",
+ "QPaintEvent",
+ "QPainter",
+ "QPainterPath",
+ "QPainterPathStroker",
+ "QPalette",
+ "QPen",
+ "QPicture",
+ "QPictureIO",
+ "QPixmap",
+ "QPixmapCache",
+ "QPolygon",
+ "QPolygonF",
+ "QQuaternion",
+ "QRadialGradient",
+ "QRegExpValidator",
+ "QRegion",
+ "QResizeEvent",
+ "QSessionManager",
+ "QShortcutEvent",
+ "QShowEvent",
+ "QStandardItem",
+ "QStandardItemModel",
+ "QStatusTipEvent",
+ "QSyntaxHighlighter",
+ "QTabletEvent",
+ "QTextBlock",
+ "QTextBlockFormat",
+ "QTextBlockGroup",
+ "QTextBlockUserData",
+ "QTextCharFormat",
+ "QTextCursor",
+ "QTextDocument",
+ "QTextDocumentFragment",
+ "QTextFormat",
+ "QTextFragment",
+ "QTextFrame",
+ "QTextFrameFormat",
+ "QTextImageFormat",
+ "QTextInlineObject",
+ "QTextItem",
+ "QTextLayout",
+ "QTextLength",
+ "QTextLine",
+ "QTextList",
+ "QTextListFormat",
+ "QTextObject",
+ "QTextObjectInterface",
+ "QTextOption",
+ "QTextTable",
+ "QTextTableCell",
+ "QTextTableCellFormat",
+ "QTextTableFormat",
+ "QTouchEvent",
+ "QTransform",
+ "QValidator",
+ "QVector2D",
+ "QVector3D",
+ "QVector4D",
+ "QWhatsThisClickedEvent",
+ "QWheelEvent",
+ "QWindowStateChangeEvent",
+ "qAlpha",
+ "qBlue",
+ "qGray",
+ "qGreen",
+ "qIsGray",
+ "qRed",
+ "qRgb",
+ "qRgba"
+ ],
+ "QtHelp": [
+ "QHelpContentItem",
+ "QHelpContentModel",
+ "QHelpContentWidget",
+ "QHelpEngine",
+ "QHelpEngineCore",
+ "QHelpIndexModel",
+ "QHelpIndexWidget",
+ "QHelpSearchEngine",
+ "QHelpSearchQuery",
+ "QHelpSearchQueryWidget",
+ "QHelpSearchResultWidget"
+ ],
+ "QtMultimedia": [
+ "QAbstractVideoBuffer",
+ "QAbstractVideoSurface",
+ "QAudio",
+ "QAudioDeviceInfo",
+ "QAudioFormat",
+ "QAudioInput",
+ "QAudioOutput",
+ "QVideoFrame",
+ "QVideoSurfaceFormat"
+ ],
+ "QtNetwork": [
+ "QAbstractNetworkCache",
+ "QAbstractSocket",
+ "QAuthenticator",
+ "QHostAddress",
+ "QHostInfo",
+ "QLocalServer",
+ "QLocalSocket",
+ "QNetworkAccessManager",
+ "QNetworkAddressEntry",
+ "QNetworkCacheMetaData",
+ "QNetworkConfiguration",
+ "QNetworkConfigurationManager",
+ "QNetworkCookie",
+ "QNetworkCookieJar",
+ "QNetworkDiskCache",
+ "QNetworkInterface",
+ "QNetworkProxy",
+ "QNetworkProxyFactory",
+ "QNetworkProxyQuery",
+ "QNetworkReply",
+ "QNetworkRequest",
+ "QNetworkSession",
+ "QSsl",
+ "QTcpServer",
+ "QTcpSocket",
+ "QUdpSocket"
+ ],
+ "QtOpenGL": [
+ "QGL",
+ "QGLContext",
+ "QGLFormat",
+ "QGLWidget"
+ ],
+ "QtPrintSupport": [
+ "QAbstractPrintDialog",
+ "QPageSetupDialog",
+ "QPrintDialog",
+ "QPrintEngine",
+ "QPrintPreviewDialog",
+ "QPrintPreviewWidget",
+ "QPrinter",
+ "QPrinterInfo"
+ ],
+ "QtSql": [
+ "QSql",
+ "QSqlDatabase",
+ "QSqlDriver",
+ "QSqlDriverCreatorBase",
+ "QSqlError",
+ "QSqlField",
+ "QSqlIndex",
+ "QSqlQuery",
+ "QSqlQueryModel",
+ "QSqlRecord",
+ "QSqlRelation",
+ "QSqlRelationalDelegate",
+ "QSqlRelationalTableModel",
+ "QSqlResult",
+ "QSqlTableModel"
+ ],
+ "QtSvg": [
+ "QGraphicsSvgItem",
+ "QSvgGenerator",
+ "QSvgRenderer",
+ "QSvgWidget"
+ ],
+ "QtTest": [
+ "QTest"
+ ],
+ "QtWidgets": [
+ "QAbstractButton",
+ "QAbstractGraphicsShapeItem",
+ "QAbstractItemDelegate",
+ "QAbstractItemView",
+ "QAbstractScrollArea",
+ "QAbstractSlider",
+ "QAbstractSpinBox",
+ "QAction",
+ "QActionGroup",
+ "QApplication",
+ "QBoxLayout",
+ "QButtonGroup",
+ "QCalendarWidget",
+ "QCheckBox",
+ "QColorDialog",
+ "QColumnView",
+ "QComboBox",
+ "QCommandLinkButton",
+ "QCommonStyle",
+ "QCompleter",
+ "QDataWidgetMapper",
+ "QDateEdit",
+ "QDateTimeEdit",
+ "QDesktopWidget",
+ "QDial",
+ "QDialog",
+ "QDialogButtonBox",
+ "QDirModel",
+ "QDockWidget",
+ "QDoubleSpinBox",
+ "QErrorMessage",
+ "QFileDialog",
+ "QFileIconProvider",
+ "QFileSystemModel",
+ "QFocusFrame",
+ "QFontComboBox",
+ "QFontDialog",
+ "QFormLayout",
+ "QFrame",
+ "QGesture",
+ "QGestureEvent",
+ "QGestureRecognizer",
+ "QGraphicsAnchor",
+ "QGraphicsAnchorLayout",
+ "QGraphicsBlurEffect",
+ "QGraphicsColorizeEffect",
+ "QGraphicsDropShadowEffect",
+ "QGraphicsEffect",
+ "QGraphicsEllipseItem",
+ "QGraphicsGridLayout",
+ "QGraphicsItem",
+ "QGraphicsItemGroup",
+ "QGraphicsLayout",
+ "QGraphicsLayoutItem",
+ "QGraphicsLineItem",
+ "QGraphicsLinearLayout",
+ "QGraphicsObject",
+ "QGraphicsOpacityEffect",
+ "QGraphicsPathItem",
+ "QGraphicsPixmapItem",
+ "QGraphicsPolygonItem",
+ "QGraphicsProxyWidget",
+ "QGraphicsRectItem",
+ "QGraphicsRotation",
+ "QGraphicsScale",
+ "QGraphicsScene",
+ "QGraphicsSceneContextMenuEvent",
+ "QGraphicsSceneDragDropEvent",
+ "QGraphicsSceneEvent",
+ "QGraphicsSceneHelpEvent",
+ "QGraphicsSceneHoverEvent",
+ "QGraphicsSceneMouseEvent",
+ "QGraphicsSceneMoveEvent",
+ "QGraphicsSceneResizeEvent",
+ "QGraphicsSceneWheelEvent",
+ "QGraphicsSimpleTextItem",
+ "QGraphicsTextItem",
+ "QGraphicsTransform",
+ "QGraphicsView",
+ "QGraphicsWidget",
+ "QGridLayout",
+ "QGroupBox",
+ "QHBoxLayout",
+ "QHeaderView",
+ "QInputDialog",
+ "QItemDelegate",
+ "QItemEditorCreatorBase",
+ "QItemEditorFactory",
+ "QKeyEventTransition",
+ "QLCDNumber",
+ "QLabel",
+ "QLayout",
+ "QLayoutItem",
+ "QLineEdit",
+ "QListView",
+ "QListWidget",
+ "QListWidgetItem",
+ "QMainWindow",
+ "QMdiArea",
+ "QMdiSubWindow",
+ "QMenu",
+ "QMenuBar",
+ "QMessageBox",
+ "QMouseEventTransition",
+ "QPanGesture",
+ "QPinchGesture",
+ "QPlainTextDocumentLayout",
+ "QPlainTextEdit",
+ "QProgressBar",
+ "QProgressDialog",
+ "QPushButton",
+ "QRadioButton",
+ "QRubberBand",
+ "QScrollArea",
+ "QScrollBar",
+ "QShortcut",
+ "QSizeGrip",
+ "QSizePolicy",
+ "QSlider",
+ "QSpacerItem",
+ "QSpinBox",
+ "QSplashScreen",
+ "QSplitter",
+ "QSplitterHandle",
+ "QStackedLayout",
+ "QStackedWidget",
+ "QStatusBar",
+ "QStyle",
+ "QStyleFactory",
+ "QStyleHintReturn",
+ "QStyleHintReturnMask",
+ "QStyleHintReturnVariant",
+ "QStyleOption",
+ "QStyleOptionButton",
+ "QStyleOptionComboBox",
+ "QStyleOptionComplex",
+ "QStyleOptionDockWidget",
+ "QStyleOptionFocusRect",
+ "QStyleOptionFrame",
+ "QStyleOptionGraphicsItem",
+ "QStyleOptionGroupBox",
+ "QStyleOptionHeader",
+ "QStyleOptionMenuItem",
+ "QStyleOptionProgressBar",
+ "QStyleOptionRubberBand",
+ "QStyleOptionSizeGrip",
+ "QStyleOptionSlider",
+ "QStyleOptionSpinBox",
+ "QStyleOptionTab",
+ "QStyleOptionTabBarBase",
+ "QStyleOptionTabWidgetFrame",
+ "QStyleOptionTitleBar",
+ "QStyleOptionToolBar",
+ "QStyleOptionToolBox",
+ "QStyleOptionToolButton",
+ "QStyleOptionViewItem",
+ "QStylePainter",
+ "QStyledItemDelegate",
+ "QSwipeGesture",
+ "QSystemTrayIcon",
+ "QTabBar",
+ "QTabWidget",
+ "QTableView",
+ "QTableWidget",
+ "QTableWidgetItem",
+ "QTableWidgetSelectionRange",
+ "QTapAndHoldGesture",
+ "QTapGesture",
+ "QTextBrowser",
+ "QTextEdit",
+ "QTimeEdit",
+ "QToolBar",
+ "QToolBox",
+ "QToolButton",
+ "QToolTip",
+ "QTreeView",
+ "QTreeWidget",
+ "QTreeWidgetItem",
+ "QTreeWidgetItemIterator",
+ "QUndoCommand",
+ "QUndoGroup",
+ "QUndoStack",
+ "QUndoView",
+ "QVBoxLayout",
+ "QWhatsThis",
+ "QWidget",
+ "QWidgetAction",
+ "QWidgetItem",
+ "QWizard",
+ "QWizardPage"
+ ],
+ "QtX11Extras": [
+ "QX11Info"
+ ],
+ "QtXml": [
+ "QDomAttr",
+ "QDomCDATASection",
+ "QDomCharacterData",
+ "QDomComment",
+ "QDomDocument",
+ "QDomDocumentFragment",
+ "QDomDocumentType",
+ "QDomElement",
+ "QDomEntity",
+ "QDomEntityReference",
+ "QDomImplementation",
+ "QDomNamedNodeMap",
+ "QDomNode",
+ "QDomNodeList",
+ "QDomNotation",
+ "QDomProcessingInstruction",
+ "QDomText",
+ "QXmlAttributes",
+ "QXmlContentHandler",
+ "QXmlDTDHandler",
+ "QXmlDeclHandler",
+ "QXmlDefaultHandler",
+ "QXmlEntityResolver",
+ "QXmlErrorHandler",
+ "QXmlInputSource",
+ "QXmlLexicalHandler",
+ "QXmlLocator",
+ "QXmlNamespaceSupport",
+ "QXmlParseException",
+ "QXmlReader",
+ "QXmlSimpleReader"
+ ],
+ "QtXmlPatterns": [
+ "QAbstractMessageHandler",
+ "QAbstractUriResolver",
+ "QAbstractXmlNodeModel",
+ "QAbstractXmlReceiver",
+ "QSourceLocation",
+ "QXmlFormatter",
+ "QXmlItem",
+ "QXmlName",
+ "QXmlNamePool",
+ "QXmlNodeModelIndex",
+ "QXmlQuery",
+ "QXmlResultItems",
+ "QXmlSchema",
+ "QXmlSchemaValidator",
+ "QXmlSerializer"
+ ]
+}
+
+
+def _qInstallMessageHandler(handler):
+ """Install a message handler that works in all bindings
+
+ Args:
+ handler: A function that takes 3 arguments, or None
+ """
+ def messageOutputHandler(*args):
+ # In Qt4 bindings, message handlers are passed 2 arguments
+ # In Qt5 bindings, message handlers are passed 3 arguments
+ # The first argument is a QtMsgType
+ # The last argument is the message to be printed
+ # The Middle argument (if passed) is a QMessageLogContext
+ if len(args) == 3:
+ msgType, logContext, msg = args
+ elif len(args) == 2:
+ msgType, msg = args
+ logContext = None
+ else:
+ raise TypeError(
+ "handler expected 2 or 3 arguments, got {0}".format(len(args)))
+
+ if isinstance(msg, bytes):
+ # In python 3, some bindings pass a bytestring, which cannot be
+ # used elsewhere. Decoding a python 2 or 3 bytestring object will
+ # consistently return a unicode object.
+ msg = msg.decode()
+
+ handler(msgType, logContext, msg)
+
+ passObject = messageOutputHandler if handler else handler
+ if Qt.IsPySide or Qt.IsPyQt4:
+ return Qt._QtCore.qInstallMsgHandler(passObject)
+ elif Qt.IsPySide2 or Qt.IsPyQt5:
+ return Qt._QtCore.qInstallMessageHandler(passObject)
+
+
+def _getcpppointer(object):
+ if hasattr(Qt, "_shiboken2"):
+ return getattr(Qt, "_shiboken2").getCppPointer(object)[0]
+ elif hasattr(Qt, "_shiboken"):
+ return getattr(Qt, "_shiboken").getCppPointer(object)[0]
+ elif hasattr(Qt, "_sip"):
+ return getattr(Qt, "_sip").unwrapinstance(object)
+ raise AttributeError("'module' has no attribute 'getCppPointer'")
+
+
+def _wrapinstance(ptr, base=None):
+ """Enable implicit cast of pointer to most suitable class
+
+ This behaviour is available in sip per default.
+
+ Based on http://nathanhorne.com/pyqtpyside-wrap-instance
+
+ Usage:
+ This mechanism kicks in under these circumstances.
+ 1. Qt.py is using PySide 1 or 2.
+ 2. A `base` argument is not provided.
+
+ See :func:`QtCompat.wrapInstance()`
+
+ Arguments:
+ ptr (long): Pointer to QObject in memory
+ base (QObject, optional): Base class to wrap with. Defaults to QObject,
+ which should handle anything.
+
+ """
+
+ assert isinstance(ptr, long), "Argument 'ptr' must be of type "
+ assert (base is None) or issubclass(base, Qt.QtCore.QObject), (
+ "Argument 'base' must be of type ")
+
+ if Qt.IsPyQt4 or Qt.IsPyQt5:
+ func = getattr(Qt, "_sip").wrapinstance
+ elif Qt.IsPySide2:
+ func = getattr(Qt, "_shiboken2").wrapInstance
+ elif Qt.IsPySide:
+ func = getattr(Qt, "_shiboken").wrapInstance
+ else:
+ raise AttributeError("'module' has no attribute 'wrapInstance'")
+
+ if base is None:
+ q_object = func(long(ptr), Qt.QtCore.QObject)
+ meta_object = q_object.metaObject()
+ class_name = meta_object.className()
+ super_class_name = meta_object.superClass().className()
+
+ if hasattr(Qt.QtWidgets, class_name):
+ base = getattr(Qt.QtWidgets, class_name)
+
+ elif hasattr(Qt.QtWidgets, super_class_name):
+ base = getattr(Qt.QtWidgets, super_class_name)
+
+ else:
+ base = Qt.QtCore.QObject
+
+ return func(long(ptr), base)
+
+
+def _translate(context, sourceText, *args):
+ # In Qt4 bindings, translate can be passed 2 or 3 arguments
+ # In Qt5 bindings, translate can be passed 2 arguments
+ # The first argument is disambiguation[str]
+ # The last argument is n[int]
+ # The middle argument can be encoding[QtCore.QCoreApplication.Encoding]
+ if len(args) == 3:
+ disambiguation, encoding, n = args
+ elif len(args) == 2:
+ disambiguation, n = args
+ encoding = None
+ else:
+ raise TypeError(
+ "Expected 4 or 5 arguments, got {0}.".format(len(args)+2))
+
+ if hasattr(Qt.QtCore, "QCoreApplication"):
+ app = getattr(Qt.QtCore, "QCoreApplication")
+ else:
+ raise NotImplementedError(
+ "Missing QCoreApplication implementation for {binding}".format(
+ binding=Qt.__binding__,
+ )
+ )
+ if Qt.__binding__ in ("PySide2", "PyQt5"):
+ sanitized_args = [context, sourceText, disambiguation, n]
+ else:
+ sanitized_args = [
+ context,
+ sourceText,
+ disambiguation,
+ encoding or app.CodecForTr,
+ n
+ ]
+ return app.translate(*sanitized_args)
+
+
+def _loadUi(uifile, baseinstance=None):
+ """Dynamically load a user interface from the given `uifile`
+
+ This function calls `uic.loadUi` if using PyQt bindings,
+ else it implements a comparable binding for PySide.
+
+ Documentation:
+ http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi
+
+ Arguments:
+ uifile (str): Absolute path to Qt Designer file.
+ baseinstance (QWidget): Instantiated QWidget or subclass thereof
+
+ Return:
+ baseinstance if `baseinstance` is not `None`. Otherwise
+ return the newly created instance of the user interface.
+
+ """
+ if hasattr(Qt, "_uic"):
+ return Qt._uic.loadUi(uifile, baseinstance)
+
+ elif hasattr(Qt, "_QtUiTools"):
+ # Implement `PyQt5.uic.loadUi` for PySide(2)
+
+ class _UiLoader(Qt._QtUiTools.QUiLoader):
+ """Create the user interface in a base instance.
+
+ Unlike `Qt._QtUiTools.QUiLoader` itself this class does not
+ create a new instance of the top-level widget, but creates the user
+ interface in an existing instance of the top-level class if needed.
+
+ This mimics the behaviour of `PyQt5.uic.loadUi`.
+
+ """
+
+ def __init__(self, baseinstance):
+ super(_UiLoader, self).__init__(baseinstance)
+ self.baseinstance = baseinstance
+
+ def load(self, uifile, *args, **kwargs):
+ from xml.etree.ElementTree import ElementTree
+
+ # For whatever reason, if this doesn't happen then
+ # reading an invalid or non-existing .ui file throws
+ # a RuntimeError.
+ etree = ElementTree()
+ etree.parse(uifile)
+
+ widget = Qt._QtUiTools.QUiLoader.load(
+ self, uifile, *args, **kwargs)
+
+ # Workaround for PySide 1.0.9, see issue #208
+ widget.parentWidget()
+
+ return widget
+
+ def createWidget(self, class_name, parent=None, name=""):
+ """Called for each widget defined in ui file
+
+ Overridden here to populate `baseinstance` instead.
+
+ """
+
+ if parent is None and self.baseinstance:
+ # Supposed to create the top-level widget,
+ # return the base instance instead
+ return self.baseinstance
+
+ # For some reason, Line is not in the list of available
+ # widgets, but works fine, so we have to special case it here.
+ if class_name in self.availableWidgets() + ["Line"]:
+ # Create a new widget for child widgets
+ widget = Qt._QtUiTools.QUiLoader.createWidget(self,
+ class_name,
+ parent,
+ name)
+
+ else:
+ raise Exception("Custom widget '%s' not supported"
+ % class_name)
+
+ if self.baseinstance:
+ # Set an attribute for the new child widget on the base
+ # instance, just like PyQt5.uic.loadUi does.
+ setattr(self.baseinstance, name, widget)
+
+ return widget
+
+ widget = _UiLoader(baseinstance).load(uifile)
+ Qt.QtCore.QMetaObject.connectSlotsByName(widget)
+
+ return widget
+
+ else:
+ raise NotImplementedError("No implementation available for loadUi")
+
+
+"""Misplaced members
+
+These members from the original submodule are misplaced relative PySide2
+
+"""
+_misplaced_members = {
+ "PySide2": {
+ "QtGui.QStringListModel": "QtCore.QStringListModel",
+ "QtCore.Property": "QtCore.Property",
+ "QtCore.Signal": "QtCore.Signal",
+ "QtCore.Slot": "QtCore.Slot",
+ "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
+ "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
+ "QtCore.QItemSelection": "QtCore.QItemSelection",
+ "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
+ "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange",
+ "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi],
+ "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance],
+ "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer],
+ "QtWidgets.qApp": "QtWidgets.QApplication.instance()",
+ "QtCore.QCoreApplication.translate": [
+ "QtCompat.translate", _translate
+ ],
+ "QtWidgets.QApplication.translate": [
+ "QtCompat.translate", _translate
+ ],
+ "QtCore.qInstallMessageHandler": [
+ "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
+ ],
+ },
+ "PyQt5": {
+ "QtCore.pyqtProperty": "QtCore.Property",
+ "QtCore.pyqtSignal": "QtCore.Signal",
+ "QtCore.pyqtSlot": "QtCore.Slot",
+ "QtCore.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
+ "QtCore.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
+ "QtCore.QStringListModel": "QtCore.QStringListModel",
+ "QtCore.QItemSelection": "QtCore.QItemSelection",
+ "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
+ "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange",
+ "uic.loadUi": ["QtCompat.loadUi", _loadUi],
+ "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance],
+ "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer],
+ "QtWidgets.qApp": "QtWidgets.QApplication.instance()",
+ "QtCore.QCoreApplication.translate": [
+ "QtCompat.translate", _translate
+ ],
+ "QtWidgets.QApplication.translate": [
+ "QtCompat.translate", _translate
+ ],
+ "QtCore.qInstallMessageHandler": [
+ "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
+ ],
+ },
+ "PySide": {
+ "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
+ "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
+ "QtGui.QStringListModel": "QtCore.QStringListModel",
+ "QtGui.QItemSelection": "QtCore.QItemSelection",
+ "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel",
+ "QtCore.Property": "QtCore.Property",
+ "QtCore.Signal": "QtCore.Signal",
+ "QtCore.Slot": "QtCore.Slot",
+ "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange",
+ "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog",
+ "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog",
+ "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog",
+ "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine",
+ "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog",
+ "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget",
+ "QtGui.QPrinter": "QtPrintSupport.QPrinter",
+ "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo",
+ "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi],
+ "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance],
+ "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer],
+ "QtGui.qApp": "QtWidgets.QApplication.instance()",
+ "QtCore.QCoreApplication.translate": [
+ "QtCompat.translate", _translate
+ ],
+ "QtGui.QApplication.translate": [
+ "QtCompat.translate", _translate
+ ],
+ "QtCore.qInstallMsgHandler": [
+ "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
+ ],
+ },
+ "PyQt4": {
+ "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
+ "QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
+ "QtGui.QItemSelection": "QtCore.QItemSelection",
+ "QtGui.QStringListModel": "QtCore.QStringListModel",
+ "QtGui.QItemSelectionModel": "QtCore.QItemSelectionModel",
+ "QtCore.pyqtProperty": "QtCore.Property",
+ "QtCore.pyqtSignal": "QtCore.Signal",
+ "QtCore.pyqtSlot": "QtCore.Slot",
+ "QtGui.QItemSelectionRange": "QtCore.QItemSelectionRange",
+ "QtGui.QAbstractPrintDialog": "QtPrintSupport.QAbstractPrintDialog",
+ "QtGui.QPageSetupDialog": "QtPrintSupport.QPageSetupDialog",
+ "QtGui.QPrintDialog": "QtPrintSupport.QPrintDialog",
+ "QtGui.QPrintEngine": "QtPrintSupport.QPrintEngine",
+ "QtGui.QPrintPreviewDialog": "QtPrintSupport.QPrintPreviewDialog",
+ "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget",
+ "QtGui.QPrinter": "QtPrintSupport.QPrinter",
+ "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo",
+ # "QtCore.pyqtSignature": "QtCore.Slot",
+ "uic.loadUi": ["QtCompat.loadUi", _loadUi],
+ "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance],
+ "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer],
+ "QtCore.QString": "str",
+ "QtGui.qApp": "QtWidgets.QApplication.instance()",
+ "QtCore.QCoreApplication.translate": [
+ "QtCompat.translate", _translate
+ ],
+ "QtGui.QApplication.translate": [
+ "QtCompat.translate", _translate
+ ],
+ "QtCore.qInstallMsgHandler": [
+ "QtCompat.qInstallMessageHandler", _qInstallMessageHandler
+ ],
+ }
+}
+
+""" Compatibility Members
+
+This dictionary is used to build Qt.QtCompat objects that provide a consistent
+interface for obsolete members, and differences in binding return values.
+
+{
+ "binding": {
+ "classname": {
+ "targetname": "binding_namespace",
+ }
+ }
+}
+"""
+_compatibility_members = {
+ "PySide2": {
+ "QWidget": {
+ "grab": "QtWidgets.QWidget.grab",
+ },
+ "QHeaderView": {
+ "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
+ "setSectionsClickable":
+ "QtWidgets.QHeaderView.setSectionsClickable",
+ "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
+ "setSectionResizeMode":
+ "QtWidgets.QHeaderView.setSectionResizeMode",
+ "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
+ "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
+ },
+ "QFileDialog": {
+ "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
+ "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
+ "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
+ },
+ },
+ "PyQt5": {
+ "QWidget": {
+ "grab": "QtWidgets.QWidget.grab",
+ },
+ "QHeaderView": {
+ "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
+ "setSectionsClickable":
+ "QtWidgets.QHeaderView.setSectionsClickable",
+ "sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
+ "setSectionResizeMode":
+ "QtWidgets.QHeaderView.setSectionResizeMode",
+ "sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
+ "setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
+ },
+ "QFileDialog": {
+ "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
+ "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
+ "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
+ },
+ },
+ "PySide": {
+ "QWidget": {
+ "grab": "QtWidgets.QPixmap.grabWidget",
+ },
+ "QHeaderView": {
+ "sectionsClickable": "QtWidgets.QHeaderView.isClickable",
+ "setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
+ "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
+ "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
+ "sectionsMovable": "QtWidgets.QHeaderView.isMovable",
+ "setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
+ },
+ "QFileDialog": {
+ "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
+ "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
+ "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
+ },
+ },
+ "PyQt4": {
+ "QWidget": {
+ "grab": "QtWidgets.QPixmap.grabWidget",
+ },
+ "QHeaderView": {
+ "sectionsClickable": "QtWidgets.QHeaderView.isClickable",
+ "setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
+ "sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
+ "setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
+ "sectionsMovable": "QtWidgets.QHeaderView.isMovable",
+ "setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
+ },
+ "QFileDialog": {
+ "getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
+ "getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
+ "getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
+ },
+ },
+}
+
+
+def _apply_site_config():
+ try:
+ import QtSiteConfig
+ except ImportError:
+ # If no QtSiteConfig module found, no modifications
+ # to _common_members are needed.
+ pass
+ else:
+ # Provide the ability to modify the dicts used to build Qt.py
+ if hasattr(QtSiteConfig, 'update_members'):
+ QtSiteConfig.update_members(_common_members)
+
+ if hasattr(QtSiteConfig, 'update_misplaced_members'):
+ QtSiteConfig.update_misplaced_members(members=_misplaced_members)
+
+ if hasattr(QtSiteConfig, 'update_compatibility_members'):
+ QtSiteConfig.update_compatibility_members(
+ members=_compatibility_members)
+
+
+def _new_module(name):
+ return types.ModuleType(__name__ + "." + name)
+
+
+def _import_sub_module(module, name):
+ """import_sub_module will mimic the function of importlib.import_module"""
+ module = __import__(module.__name__ + "." + name)
+ for level in name.split("."):
+ module = getattr(module, level)
+ return module
+
+
+def _setup(module, extras):
+ """Install common submodules"""
+
+ Qt.__binding__ = module.__name__
+
+ for name in list(_common_members) + extras:
+ try:
+ submodule = _import_sub_module(
+ module, name)
+ except ImportError:
+ try:
+ # For extra modules like sip and shiboken that may not be
+ # children of the binding.
+ submodule = __import__(name)
+ except ImportError:
+ continue
+
+ setattr(Qt, "_" + name, submodule)
+
+ if name not in extras:
+ # Store reference to original binding,
+ # but don't store speciality modules
+ # such as uic or QtUiTools
+ setattr(Qt, name, _new_module(name))
+
+
+def _reassign_misplaced_members(binding):
+ """Apply misplaced members from `binding` to Qt.py
+
+ Arguments:
+ binding (dict): Misplaced members
+
+ """
+
+ for src, dst in _misplaced_members[binding].items():
+ dst_value = None
+
+ src_parts = src.split(".")
+ src_module = src_parts[0]
+ src_member = None
+ if len(src_parts) > 1:
+ src_member = src_parts[1:]
+
+ if isinstance(dst, (list, tuple)):
+ dst, dst_value = dst
+
+ dst_parts = dst.split(".")
+ dst_module = dst_parts[0]
+ dst_member = None
+ if len(dst_parts) > 1:
+ dst_member = dst_parts[1]
+
+ # Get the member we want to store in the namesapce.
+ if not dst_value:
+ try:
+ _part = getattr(Qt, "_" + src_module)
+ while src_member:
+ member = src_member.pop(0)
+ _part = getattr(_part, member)
+ dst_value = _part
+ except AttributeError:
+ # If the member we want to store in the namespace does not
+ # exist, there is no need to continue. This can happen if a
+ # request was made to rename a member that didn't exist, for
+ # example if QtWidgets isn't available on the target platform.
+ _log("Misplaced member has no source: {}".format(src))
+ continue
+
+ try:
+ src_object = getattr(Qt, dst_module)
+ except AttributeError:
+ if dst_module not in _common_members:
+ # Only create the Qt parent module if its listed in
+ # _common_members. Without this check, if you remove QtCore
+ # from _common_members, the default _misplaced_members will add
+ # Qt.QtCore so it can add Signal, Slot, etc.
+ msg = 'Not creating missing member module "{m}" for "{c}"'
+ _log(msg.format(m=dst_module, c=dst_member))
+ continue
+ # If the dst is valid but the Qt parent module does not exist
+ # then go ahead and create a new module to contain the member.
+ setattr(Qt, dst_module, _new_module(dst_module))
+ src_object = getattr(Qt, dst_module)
+ # Enable direct import of the new module
+ sys.modules[__name__ + "." + dst_module] = src_object
+
+ if not dst_value:
+ dst_value = getattr(Qt, "_" + src_module)
+ if src_member:
+ dst_value = getattr(dst_value, src_member)
+
+ setattr(
+ src_object,
+ dst_member or dst_module,
+ dst_value
+ )
+
+
+def _build_compatibility_members(binding, decorators=None):
+ """Apply `binding` to QtCompat
+
+ Arguments:
+ binding (str): Top level binding in _compatibility_members.
+ decorators (dict, optional): Provides the ability to decorate the
+ original Qt methods when needed by a binding. This can be used
+ to change the returned value to a standard value. The key should
+ be the classname, the value is a dict where the keys are the
+ target method names, and the values are the decorator functions.
+
+ """
+
+ decorators = decorators or dict()
+
+ # Allow optional site-level customization of the compatibility members.
+ # This method does not need to be implemented in QtSiteConfig.
+ try:
+ import QtSiteConfig
+ except ImportError:
+ pass
+ else:
+ if hasattr(QtSiteConfig, 'update_compatibility_decorators'):
+ QtSiteConfig.update_compatibility_decorators(binding, decorators)
+
+ _QtCompat = type("QtCompat", (object,), {})
+
+ for classname, bindings in _compatibility_members[binding].items():
+ attrs = {}
+ for target, binding in bindings.items():
+ namespaces = binding.split('.')
+ try:
+ src_object = getattr(Qt, "_" + namespaces[0])
+ except AttributeError as e:
+ _log("QtCompat: AttributeError: %s" % e)
+ # Skip reassignment of non-existing members.
+ # This can happen if a request was made to
+ # rename a member that didn't exist, for example
+ # if QtWidgets isn't available on the target platform.
+ continue
+
+ # Walk down any remaining namespace getting the object assuming
+ # that if the first namespace exists the rest will exist.
+ for namespace in namespaces[1:]:
+ src_object = getattr(src_object, namespace)
+
+ # decorate the Qt method if a decorator was provided.
+ if target in decorators.get(classname, []):
+ # staticmethod must be called on the decorated method to
+ # prevent a TypeError being raised when the decorated method
+ # is called.
+ src_object = staticmethod(
+ decorators[classname][target](src_object))
+
+ attrs[target] = src_object
+
+ # Create the QtCompat class and install it into the namespace
+ compat_class = type(classname, (_QtCompat,), attrs)
+ setattr(Qt.QtCompat, classname, compat_class)
+
+
+def _pyside2():
+ """Initialise PySide2
+
+ These functions serve to test the existence of a binding
+ along with set it up in such a way that it aligns with
+ the final step; adding members from the original binding
+ to Qt.py
+
+ """
+
+ import PySide2 as module
+ extras = ["QtUiTools"]
+ try:
+ try:
+ # Before merge of PySide and shiboken
+ import shiboken2
+ except ImportError:
+ # After merge of PySide and shiboken, May 2017
+ from PySide2 import shiboken2
+ extras.append("shiboken2")
+ except ImportError:
+ pass
+
+ _setup(module, extras)
+ Qt.__binding_version__ = module.__version__
+
+ if hasattr(Qt, "_shiboken2"):
+ Qt.QtCompat.wrapInstance = _wrapinstance
+ Qt.QtCompat.getCppPointer = _getcpppointer
+
+ if hasattr(Qt, "_QtUiTools"):
+ Qt.QtCompat.loadUi = _loadUi
+
+ if hasattr(Qt, "_QtCore"):
+ Qt.__qt_version__ = Qt._QtCore.qVersion()
+
+ if hasattr(Qt, "_QtWidgets"):
+ Qt.QtCompat.setSectionResizeMode = \
+ Qt._QtWidgets.QHeaderView.setSectionResizeMode
+
+ _reassign_misplaced_members("PySide2")
+ _build_compatibility_members("PySide2")
+
+
+def _pyside():
+ """Initialise PySide"""
+
+ import PySide as module
+ extras = ["QtUiTools"]
+ try:
+ try:
+ # Before merge of PySide and shiboken
+ import shiboken
+ except ImportError:
+ # After merge of PySide and shiboken, May 2017
+ from PySide import shiboken
+ extras.append("shiboken")
+ except ImportError:
+ pass
+
+ _setup(module, extras)
+ Qt.__binding_version__ = module.__version__
+
+ if hasattr(Qt, "_shiboken"):
+ Qt.QtCompat.wrapInstance = _wrapinstance
+ Qt.QtCompat.getCppPointer = _getcpppointer
+
+ if hasattr(Qt, "_QtUiTools"):
+ Qt.QtCompat.loadUi = _loadUi
+
+ if hasattr(Qt, "_QtGui"):
+ setattr(Qt, "QtWidgets", _new_module("QtWidgets"))
+ setattr(Qt, "_QtWidgets", Qt._QtGui)
+ if hasattr(Qt._QtGui, "QX11Info"):
+ setattr(Qt, "QtX11Extras", _new_module("QtX11Extras"))
+ Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info
+
+ Qt.QtCompat.setSectionResizeMode = Qt._QtGui.QHeaderView.setResizeMode
+
+ if hasattr(Qt, "_QtCore"):
+ Qt.__qt_version__ = Qt._QtCore.qVersion()
+
+ _reassign_misplaced_members("PySide")
+ _build_compatibility_members("PySide")
+
+
+def _pyqt5():
+ """Initialise PyQt5"""
+
+ import PyQt5 as module
+ extras = ["uic"]
+ try:
+ import sip
+ extras.append(sip.__name__)
+ except ImportError:
+ sip = None
+
+ _setup(module, extras)
+ if hasattr(Qt, "_sip"):
+ Qt.QtCompat.wrapInstance = _wrapinstance
+ Qt.QtCompat.getCppPointer = _getcpppointer
+
+ if hasattr(Qt, "_uic"):
+ Qt.QtCompat.loadUi = _loadUi
+
+ if hasattr(Qt, "_QtCore"):
+ Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR
+ Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR
+
+ if hasattr(Qt, "_QtWidgets"):
+ Qt.QtCompat.setSectionResizeMode = \
+ Qt._QtWidgets.QHeaderView.setSectionResizeMode
+
+ _reassign_misplaced_members("PyQt5")
+ _build_compatibility_members('PyQt5')
+
+
+def _pyqt4():
+ """Initialise PyQt4"""
+
+ import sip
+
+ # Validation of envivornment variable. Prevents an error if
+ # the variable is invalid since it's just a hint.
+ try:
+ hint = int(QT_SIP_API_HINT)
+ except TypeError:
+ hint = None # Variable was None, i.e. not set.
+ except ValueError:
+ raise ImportError("QT_SIP_API_HINT=%s must be a 1 or 2")
+
+ for api in ("QString",
+ "QVariant",
+ "QDate",
+ "QDateTime",
+ "QTextStream",
+ "QTime",
+ "QUrl"):
+ try:
+ sip.setapi(api, hint or 2)
+ except AttributeError:
+ raise ImportError("PyQt4 < 4.6 isn't supported by Qt.py")
+ except ValueError:
+ actual = sip.getapi(api)
+ if not hint:
+ raise ImportError("API version already set to %d" % actual)
+ else:
+ # Having provided a hint indicates a soft constraint, one
+ # that doesn't throw an exception.
+ sys.stderr.write(
+ "Warning: API '%s' has already been set to %d.\n"
+ % (api, actual)
+ )
+
+ import PyQt4 as module
+ extras = ["uic"]
+ try:
+ import sip
+ extras.append(sip.__name__)
+ except ImportError:
+ sip = None
+
+ _setup(module, extras)
+ if hasattr(Qt, "_sip"):
+ Qt.QtCompat.wrapInstance = _wrapinstance
+ Qt.QtCompat.getCppPointer = _getcpppointer
+
+ if hasattr(Qt, "_uic"):
+ Qt.QtCompat.loadUi = _loadUi
+
+ if hasattr(Qt, "_QtGui"):
+ setattr(Qt, "QtWidgets", _new_module("QtWidgets"))
+ setattr(Qt, "_QtWidgets", Qt._QtGui)
+ if hasattr(Qt._QtGui, "QX11Info"):
+ setattr(Qt, "QtX11Extras", _new_module("QtX11Extras"))
+ Qt.QtX11Extras.QX11Info = Qt._QtGui.QX11Info
+
+ Qt.QtCompat.setSectionResizeMode = \
+ Qt._QtGui.QHeaderView.setResizeMode
+
+ if hasattr(Qt, "_QtCore"):
+ Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR
+ Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR
+
+ _reassign_misplaced_members("PyQt4")
+
+ # QFileDialog QtCompat decorator
+ def _standardizeQFileDialog(some_function):
+ """Decorator that makes PyQt4 return conform to other bindings"""
+ def wrapper(*args, **kwargs):
+ ret = (some_function(*args, **kwargs))
+
+ # PyQt4 only returns the selected filename, force it to a
+ # standard return of the selected filename, and a empty string
+ # for the selected filter
+ return ret, ''
+
+ wrapper.__doc__ = some_function.__doc__
+ wrapper.__name__ = some_function.__name__
+
+ return wrapper
+
+ decorators = {
+ "QFileDialog": {
+ "getOpenFileName": _standardizeQFileDialog,
+ "getOpenFileNames": _standardizeQFileDialog,
+ "getSaveFileName": _standardizeQFileDialog,
+ }
+ }
+ _build_compatibility_members('PyQt4', decorators)
+
+
+def _none():
+ """Internal option (used in installer)"""
+
+ Mock = type("Mock", (), {"__getattr__": lambda Qt, attr: None})
+
+ Qt.__binding__ = "None"
+ Qt.__qt_version__ = "0.0.0"
+ Qt.__binding_version__ = "0.0.0"
+ Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None
+ Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None
+
+ for submodule in _common_members.keys():
+ setattr(Qt, submodule, Mock())
+ setattr(Qt, "_" + submodule, Mock())
+
+
+def _log(text):
+ if QT_VERBOSE:
+ sys.stdout.write(text + "\n")
+
+
+def _convert(lines):
+ """Convert compiled .ui file from PySide2 to Qt.py
+
+ Arguments:
+ lines (list): Each line of of .ui file
+
+ Usage:
+ >> with open("myui.py") as f:
+ .. lines = _convert(f.readlines())
+
+ """
+
+ def parse(line):
+ line = line.replace("from PySide2 import", "from Qt import QtCompat,")
+ line = line.replace("QtWidgets.QApplication.translate",
+ "QtCompat.translate")
+ if "QtCore.SIGNAL" in line:
+ raise NotImplementedError("QtCore.SIGNAL is missing from PyQt5 "
+ "and so Qt.py does not support it: you "
+ "should avoid defining signals inside "
+ "your ui files.")
+ return line
+
+ parsed = list()
+ for line in lines:
+ line = parse(line)
+ parsed.append(line)
+
+ return parsed
+
+
+def _cli(args):
+ """Qt.py command-line interface"""
+ import argparse
+
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--convert",
+ help="Path to compiled Python module, e.g. my_ui.py")
+ parser.add_argument("--compile",
+ help="Accept raw .ui file and compile with native "
+ "PySide2 compiler.")
+ parser.add_argument("--stdout",
+ help="Write to stdout instead of file",
+ action="store_true")
+ parser.add_argument("--stdin",
+ help="Read from stdin instead of file",
+ action="store_true")
+
+ args = parser.parse_args(args)
+
+ if args.stdout:
+ raise NotImplementedError("--stdout")
+
+ if args.stdin:
+ raise NotImplementedError("--stdin")
+
+ if args.compile:
+ raise NotImplementedError("--compile")
+
+ if args.convert:
+ sys.stdout.write("#\n"
+ "# WARNING: --convert is an ALPHA feature.\n#\n"
+ "# See https://github.com/mottosso/Qt.py/pull/132\n"
+ "# for details.\n"
+ "#\n")
+
+ #
+ # ------> Read
+ #
+ with open(args.convert) as f:
+ lines = _convert(f.readlines())
+
+ backup = "%s_backup%s" % os.path.splitext(args.convert)
+ sys.stdout.write("Creating \"%s\"..\n" % backup)
+ shutil.copy(args.convert, backup)
+
+ #
+ # <------ Write
+ #
+ with open(args.convert, "w") as f:
+ f.write("".join(lines))
+
+ sys.stdout.write("Successfully converted \"%s\"\n" % args.convert)
+
+
+def _install():
+ # Default order (customise order and content via QT_PREFERRED_BINDING)
+ default_order = ("PySide2", "PyQt5", "PySide", "PyQt4")
+ preferred_order = list(
+ b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b
+ )
+
+ order = preferred_order or default_order
+
+ available = {
+ "PySide2": _pyside2,
+ "PyQt5": _pyqt5,
+ "PySide": _pyside,
+ "PyQt4": _pyqt4,
+ "None": _none
+ }
+
+ _log("Order: '%s'" % "', '".join(order))
+
+ # Allow site-level customization of the available modules.
+ _apply_site_config()
+
+ found_binding = False
+ for name in order:
+ _log("Trying %s" % name)
+
+ try:
+ available[name]()
+ found_binding = True
+ break
+
+ except ImportError as e:
+ _log("ImportError: %s" % e)
+
+ except KeyError:
+ _log("ImportError: Preferred binding '%s' not found." % name)
+
+ if not found_binding:
+ # If not binding were found, throw this error
+ raise ImportError("No Qt binding were found.")
+
+ # Install individual members
+ for name, members in _common_members.items():
+ try:
+ their_submodule = getattr(Qt, "_%s" % name)
+ except AttributeError:
+ continue
+
+ our_submodule = getattr(Qt, name)
+
+ # Enable import *
+ __all__.append(name)
+
+ # Enable direct import of submodule,
+ # e.g. import Qt.QtCore
+ sys.modules[__name__ + "." + name] = our_submodule
+
+ for member in members:
+ # Accept that a submodule may miss certain members.
+ try:
+ their_member = getattr(their_submodule, member)
+ except AttributeError:
+ _log("'%s.%s' was missing." % (name, member))
+ continue
+
+ setattr(our_submodule, member, their_member)
+
+ # Enable direct import of QtCompat
+ sys.modules['Qt.QtCompat'] = Qt.QtCompat
+
+ # Backwards compatibility
+ if hasattr(Qt.QtCompat, 'loadUi'):
+ Qt.QtCompat.load_ui = Qt.QtCompat.loadUi
+
+
+_install()
+
+# Setup Binding Enum states
+Qt.IsPySide2 = Qt.__binding__ == 'PySide2'
+Qt.IsPyQt5 = Qt.__binding__ == 'PyQt5'
+Qt.IsPySide = Qt.__binding__ == 'PySide'
+Qt.IsPyQt4 = Qt.__binding__ == 'PyQt4'
+
+"""Augment QtCompat
+
+QtCompat contains wrappers and added functionality
+to the original bindings, such as the CLI interface
+and otherwise incompatible members between bindings,
+such as `QHeaderView.setSectionResizeMode`.
+
+"""
+
+Qt.QtCompat._cli = _cli
+Qt.QtCompat._convert = _convert
+
+# Enable command-line interface
+if __name__ == "__main__":
+ _cli(sys.argv[1:])
+
+
+# The MIT License (MIT)
+#
+# Copyright (c) 2016-2017 Marcus Ottosson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+# In PySide(2), loadUi does not exist, so we implement it
+#
+# `_UiLoader` is adapted from the qtpy project, which was further influenced
+# by qt-helpers which was released under a 3-clause BSD license which in turn
+# is based on a solution at:
+#
+# - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
+#
+# The License for this code is as follows:
+#
+# qt-helpers - a common front-end to various Qt modules
+#
+# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of the Glue project nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# Which itself was based on the solution at
+#
+# https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
+#
+# which was released under the MIT license:
+#
+# Copyright (c) 2011 Sebastian Wiesner
+# Modifications by Charl Botha
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files
+# (the "Software"),to deal in the Software without restriction,
+# including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/pype/tools/pyblish_pype/vendor/__init__.py b/pype/tools/pyblish_pype/vendor/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/__init__.py b/pype/tools/pyblish_pype/vendor/qtawesome/__init__.py
new file mode 100644
index 0000000000..4a0001ebb7
--- /dev/null
+++ b/pype/tools/pyblish_pype/vendor/qtawesome/__init__.py
@@ -0,0 +1,39 @@
+"""
+qtawesome - use font-awesome in PyQt / PySide applications
+
+This is a port to Python of the C++ QtAwesome library by Rick Blommers
+"""
+from .iconic_font import IconicFont, set_global_defaults
+from .animation import Pulse, Spin
+from ._version import version_info, __version__
+
+_resource = {'iconic': None, }
+
+
+def _instance():
+ if _resource['iconic'] is None:
+ _resource['iconic'] = IconicFont(('fa', 'fontawesome-webfont.ttf', 'fontawesome-webfont-charmap.json'),
+ ('ei', 'elusiveicons-webfont.ttf', 'elusiveicons-webfont-charmap.json'))
+ return _resource['iconic']
+
+
+def icon(*args, **kwargs):
+ return _instance().icon(*args, **kwargs)
+
+
+def load_font(*args, **kwargs):
+ return _instance().load_font(*args, **kwargs)
+
+
+def charmap(prefixed_name):
+ prefix, name = prefixed_name.split('.')
+ return _instance().charmap[prefix][name]
+
+
+def font(*args, **kwargs):
+ return _instance().font(*args, **kwargs)
+
+
+def set_defaults(**kwargs):
+ return set_global_defaults(**kwargs)
+
diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/_version.py b/pype/tools/pyblish_pype/vendor/qtawesome/_version.py
new file mode 100644
index 0000000000..7af886d1a0
--- /dev/null
+++ b/pype/tools/pyblish_pype/vendor/qtawesome/_version.py
@@ -0,0 +1,2 @@
+version_info = (0, 3, 0, 'dev')
+__version__ = '.'.join(map(str, version_info))
diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/animation.py b/pype/tools/pyblish_pype/vendor/qtawesome/animation.py
new file mode 100644
index 0000000000..a9638d74b0
--- /dev/null
+++ b/pype/tools/pyblish_pype/vendor/qtawesome/animation.py
@@ -0,0 +1,41 @@
+from ..Qt import QtCore
+
+
+class Spin:
+
+ def __init__(self, parent_widget, interval=10, step=1):
+ self.parent_widget = parent_widget
+ self.interval, self.step = interval, step
+ self.info = {}
+
+ def _update(self, parent_widget):
+ if self.parent_widget in self.info:
+ timer, angle, step = self.info[self.parent_widget]
+
+ if angle >= 360:
+ angle = 0
+
+ angle += step
+ self.info[parent_widget] = timer, angle, step
+ parent_widget.update()
+
+ def setup(self, icon_painter, painter, rect):
+
+ if self.parent_widget not in self.info:
+ timer = QtCore.QTimer()
+ timer.timeout.connect(lambda: self._update(self.parent_widget))
+ self.info[self.parent_widget] = [timer, 0, self.step]
+ timer.start(self.interval)
+ else:
+ timer, angle, self.step = self.info[self.parent_widget]
+ x_center = rect.width() * 0.5
+ y_center = rect.height() * 0.5
+ painter.translate(x_center, y_center)
+ painter.rotate(angle)
+ painter.translate(-x_center, -y_center)
+
+
+class Pulse(Spin):
+
+ def __init__(self, parent_widget):
+ Spin.__init__(self, parent_widget, interval=300, step=45)
diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont-charmap.json b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont-charmap.json
new file mode 100644
index 0000000000..099bcb818c
--- /dev/null
+++ b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont-charmap.json
@@ -0,0 +1,306 @@
+{
+ "address-book": "0xf102",
+ "address-book-alt": "0xf101",
+ "adjust": "0xf104",
+ "adjust-alt": "0xf103",
+ "adult": "0xf105",
+ "align-center": "0xf106",
+ "align-justify": "0xf107",
+ "align-left": "0xf108",
+ "align-right": "0xf109",
+ "arrow-down": "0xf10a",
+ "arrow-left": "0xf10b",
+ "arrow-right": "0xf10c",
+ "arrow-up": "0xf10d",
+ "asl": "0xf10e",
+ "asterisk": "0xf10f",
+ "backward": "0xf110",
+ "ban-circle": "0xf111",
+ "barcode": "0xf112",
+ "behance": "0xf113",
+ "bell": "0xf114",
+ "blind": "0xf115",
+ "blogger": "0xf116",
+ "bold": "0xf117",
+ "book": "0xf118",
+ "bookmark": "0xf11a",
+ "bookmark-empty": "0xf119",
+ "braille": "0xf11b",
+ "briefcase": "0xf11c",
+ "broom": "0xf11d",
+ "brush": "0xf11e",
+ "bulb": "0xf11f",
+ "bullhorn": "0xf120",
+ "calendar": "0xf122",
+ "calendar-sign": "0xf121",
+ "camera": "0xf123",
+ "car": "0xf124",
+ "caret-down": "0xf125",
+ "caret-left": "0xf126",
+ "caret-right": "0xf127",
+ "caret-up": "0xf128",
+ "cc": "0xf129",
+ "certificate": "0xf12a",
+ "check": "0xf12c",
+ "check-empty": "0xf12b",
+ "chevron-down": "0xf12d",
+ "chevron-left": "0xf12e",
+ "chevron-right": "0xf12f",
+ "chevron-up": "0xf130",
+ "child": "0xf131",
+ "circle-arrow-down": "0xf132",
+ "circle-arrow-left": "0xf133",
+ "circle-arrow-right": "0xf134",
+ "circle-arrow-up": "0xf135",
+ "cloud": "0xf137",
+ "cloud-alt": "0xf136",
+ "cog": "0xf139",
+ "cog-alt": "0xf138",
+ "cogs": "0xf13a",
+ "comment": "0xf13c",
+ "comment-alt": "0xf13b",
+ "compass": "0xf13e",
+ "compass-alt": "0xf13d",
+ "credit-card": "0xf13f",
+ "css": "0xf140",
+ "dashboard": "0xf141",
+ "delicious": "0xf142",
+ "deviantart": "0xf143",
+ "digg": "0xf144",
+ "download": "0xf146",
+ "download-alt": "0xf145",
+ "dribbble": "0xf147",
+ "edit": "0xf148",
+ "eject": "0xf149",
+ "envelope": "0xf14b",
+ "envelope-alt": "0xf14a",
+ "error": "0xf14d",
+ "error-alt": "0xf14c",
+ "eur": "0xf14e",
+ "exclamation-sign": "0xf14f",
+ "eye-close": "0xf150",
+ "eye-open": "0xf151",
+ "facebook": "0xf152",
+ "facetime-video": "0xf153",
+ "fast-backward": "0xf154",
+ "fast-forward": "0xf155",
+ "female": "0xf156",
+ "file": "0xf15c",
+ "file-alt": "0xf157",
+ "file-edit": "0xf159",
+ "file-edit-alt": "0xf158",
+ "file-new": "0xf15b",
+ "file-new-alt": "0xf15a",
+ "film": "0xf15d",
+ "filter": "0xf15e",
+ "fire": "0xf15f",
+ "flag": "0xf161",
+ "flag-alt": "0xf160",
+ "flickr": "0xf162",
+ "folder": "0xf166",
+ "folder-close": "0xf163",
+ "folder-open": "0xf164",
+ "folder-sign": "0xf165",
+ "font": "0xf167",
+ "fontsize": "0xf168",
+ "fork": "0xf169",
+ "forward": "0xf16b",
+ "forward-alt": "0xf16a",
+ "foursquare": "0xf16c",
+ "friendfeed": "0xf16e",
+ "friendfeed-rect": "0xf16d",
+ "fullscreen": "0xf16f",
+ "gbp": "0xf170",
+ "gift": "0xf171",
+ "github": "0xf173",
+ "github-text": "0xf172",
+ "glass": "0xf174",
+ "glasses": "0xf175",
+ "globe": "0xf177",
+ "globe-alt": "0xf176",
+ "googleplus": "0xf178",
+ "graph": "0xf17a",
+ "graph-alt": "0xf179",
+ "group": "0xf17c",
+ "group-alt": "0xf17b",
+ "guidedog": "0xf17d",
+ "hand-down": "0xf17e",
+ "hand-left": "0xf17f",
+ "hand-right": "0xf180",
+ "hand-up": "0xf181",
+ "hdd": "0xf182",
+ "headphones": "0xf183",
+ "hearing-impaired": "0xf184",
+ "heart": "0xf187",
+ "heart-alt": "0xf185",
+ "heart-empty": "0xf186",
+ "home": "0xf189",
+ "home-alt": "0xf188",
+ "hourglass": "0xf18a",
+ "idea": "0xf18c",
+ "idea-alt": "0xf18b",
+ "inbox": "0xf18f",
+ "inbox-alt": "0xf18d",
+ "inbox-box": "0xf18e",
+ "indent-left": "0xf190",
+ "indent-right": "0xf191",
+ "info-circle": "0xf192",
+ "instagram": "0xf193",
+ "iphone-home": "0xf194",
+ "italic": "0xf195",
+ "key": "0xf196",
+ "laptop": "0xf198",
+ "laptop-alt": "0xf197",
+ "lastfm": "0xf199",
+ "leaf": "0xf19a",
+ "lines": "0xf19b",
+ "link": "0xf19c",
+ "linkedin": "0xf19d",
+ "list": "0xf19f",
+ "list-alt": "0xf19e",
+ "livejournal": "0xf1a0",
+ "lock": "0xf1a2",
+ "lock-alt": "0xf1a1",
+ "magic": "0xf1a3",
+ "magnet": "0xf1a4",
+ "male": "0xf1a5",
+ "map-marker": "0xf1a7",
+ "map-marker-alt": "0xf1a6",
+ "mic": "0xf1a9",
+ "mic-alt": "0xf1a8",
+ "minus": "0xf1ab",
+ "minus-sign": "0xf1aa",
+ "move": "0xf1ac",
+ "music": "0xf1ad",
+ "myspace": "0xf1ae",
+ "network": "0xf1af",
+ "off": "0xf1b0",
+ "ok": "0xf1b3",
+ "ok-circle": "0xf1b1",
+ "ok-sign": "0xf1b2",
+ "opensource": "0xf1b4",
+ "paper-clip": "0xf1b6",
+ "paper-clip-alt": "0xf1b5",
+ "path": "0xf1b7",
+ "pause": "0xf1b9",
+ "pause-alt": "0xf1b8",
+ "pencil": "0xf1bb",
+ "pencil-alt": "0xf1ba",
+ "person": "0xf1bc",
+ "phone": "0xf1be",
+ "phone-alt": "0xf1bd",
+ "photo": "0xf1c0",
+ "photo-alt": "0xf1bf",
+ "picasa": "0xf1c1",
+ "picture": "0xf1c2",
+ "pinterest": "0xf1c3",
+ "plane": "0xf1c4",
+ "play": "0xf1c7",
+ "play-alt": "0xf1c5",
+ "play-circle": "0xf1c6",
+ "plurk": "0xf1c9",
+ "plurk-alt": "0xf1c8",
+ "plus": "0xf1cb",
+ "plus-sign": "0xf1ca",
+ "podcast": "0xf1cc",
+ "print": "0xf1cd",
+ "puzzle": "0xf1ce",
+ "qrcode": "0xf1cf",
+ "question": "0xf1d1",
+ "question-sign": "0xf1d0",
+ "quote-alt": "0xf1d2",
+ "quote-right": "0xf1d4",
+ "quote-right-alt": "0xf1d3",
+ "quotes": "0xf1d5",
+ "random": "0xf1d6",
+ "record": "0xf1d7",
+ "reddit": "0xf1d8",
+ "redux": "0xf1d9",
+ "refresh": "0xf1da",
+ "remove": "0xf1dd",
+ "remove-circle": "0xf1db",
+ "remove-sign": "0xf1dc",
+ "repeat": "0xf1df",
+ "repeat-alt": "0xf1de",
+ "resize-full": "0xf1e0",
+ "resize-horizontal": "0xf1e1",
+ "resize-small": "0xf1e2",
+ "resize-vertical": "0xf1e3",
+ "return-key": "0xf1e4",
+ "retweet": "0xf1e5",
+ "reverse-alt": "0xf1e6",
+ "road": "0xf1e7",
+ "rss": "0xf1e8",
+ "scissors": "0xf1e9",
+ "screen": "0xf1eb",
+ "screen-alt": "0xf1ea",
+ "screenshot": "0xf1ec",
+ "search": "0xf1ee",
+ "search-alt": "0xf1ed",
+ "share": "0xf1f0",
+ "share-alt": "0xf1ef",
+ "shopping-cart": "0xf1f2",
+ "shopping-cart-sign": "0xf1f1",
+ "signal": "0xf1f3",
+ "skype": "0xf1f4",
+ "slideshare": "0xf1f5",
+ "smiley": "0xf1f7",
+ "smiley-alt": "0xf1f6",
+ "soundcloud": "0xf1f8",
+ "speaker": "0xf1f9",
+ "spotify": "0xf1fa",
+ "stackoverflow": "0xf1fb",
+ "star": "0xf1fe",
+ "star-alt": "0xf1fc",
+ "star-empty": "0xf1fd",
+ "step-backward": "0xf1ff",
+ "step-forward": "0xf200",
+ "stop": "0xf202",
+ "stop-alt": "0xf201",
+ "stumbleupon": "0xf203",
+ "tag": "0xf204",
+ "tags": "0xf205",
+ "tasks": "0xf206",
+ "text-height": "0xf207",
+ "text-width": "0xf208",
+ "th": "0xf20b",
+ "th-large": "0xf209",
+ "th-list": "0xf20a",
+ "thumbs-down": "0xf20c",
+ "thumbs-up": "0xf20d",
+ "time": "0xf20f",
+ "time-alt": "0xf20e",
+ "tint": "0xf210",
+ "torso": "0xf211",
+ "trash": "0xf213",
+ "trash-alt": "0xf212",
+ "tumblr": "0xf214",
+ "twitter": "0xf215",
+ "universal-access": "0xf216",
+ "unlock": "0xf218",
+ "unlock-alt": "0xf217",
+ "upload": "0xf219",
+ "usd": "0xf21a",
+ "user": "0xf21b",
+ "viadeo": "0xf21c",
+ "video": "0xf21f",
+ "video-alt": "0xf21d",
+ "video-chat": "0xf21e",
+ "view-mode": "0xf220",
+ "vimeo": "0xf221",
+ "vkontakte": "0xf222",
+ "volume-down": "0xf223",
+ "volume-off": "0xf224",
+ "volume-up": "0xf225",
+ "w3c": "0xf226",
+ "warning-sign": "0xf227",
+ "website": "0xf229",
+ "website-alt": "0xf228",
+ "wheelchair": "0xf22a",
+ "wordpress": "0xf22b",
+ "wrench": "0xf22d",
+ "wrench-alt": "0xf22c",
+ "youtube": "0xf22e",
+ "zoom-in": "0xf22f",
+ "zoom-out": "0xf230"
+}
diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont.ttf b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont.ttf
new file mode 100644
index 0000000000..b6fe85d4b2
Binary files /dev/null and b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/elusiveicons-webfont.ttf differ
diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont-charmap.json b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont-charmap.json
new file mode 100644
index 0000000000..0e97d031e6
--- /dev/null
+++ b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont-charmap.json
@@ -0,0 +1,696 @@
+{
+ "500px": "f26e",
+ "adjust": "f042",
+ "adn": "f170",
+ "align-center": "f037",
+ "align-justify": "f039",
+ "align-left": "f036",
+ "align-right": "f038",
+ "amazon": "f270",
+ "ambulance": "f0f9",
+ "anchor": "f13d",
+ "android": "f17b",
+ "angellist": "f209",
+ "angle-double-down": "f103",
+ "angle-double-left": "f100",
+ "angle-double-right": "f101",
+ "angle-double-up": "f102",
+ "angle-down": "f107",
+ "angle-left": "f104",
+ "angle-right": "f105",
+ "angle-up": "f106",
+ "apple": "f179",
+ "archive": "f187",
+ "area-chart": "f1fe",
+ "arrow-circle-down": "f0ab",
+ "arrow-circle-left": "f0a8",
+ "arrow-circle-o-down": "f01a",
+ "arrow-circle-o-left": "f190",
+ "arrow-circle-o-right": "f18e",
+ "arrow-circle-o-up": "f01b",
+ "arrow-circle-right": "f0a9",
+ "arrow-circle-up": "f0aa",
+ "arrow-down": "f063",
+ "arrow-left": "f060",
+ "arrow-right": "f061",
+ "arrow-up": "f062",
+ "arrows": "f047",
+ "arrows-alt": "f0b2",
+ "arrows-h": "f07e",
+ "arrows-v": "f07d",
+ "asterisk": "f069",
+ "at": "f1fa",
+ "automobile": "f1b9",
+ "backward": "f04a",
+ "balance-scale": "f24e",
+ "ban": "f05e",
+ "bank": "f19c",
+ "bar-chart": "f080",
+ "bar-chart-o": "f080",
+ "barcode": "f02a",
+ "bars": "f0c9",
+ "battery-0": "f244",
+ "battery-1": "f243",
+ "battery-2": "f242",
+ "battery-3": "f241",
+ "battery-4": "f240",
+ "battery-empty": "f244",
+ "battery-full": "f240",
+ "battery-half": "f242",
+ "battery-quarter": "f243",
+ "battery-three-quarters": "f241",
+ "bed": "f236",
+ "beer": "f0fc",
+ "behance": "f1b4",
+ "behance-square": "f1b5",
+ "bell": "f0f3",
+ "bell-o": "f0a2",
+ "bell-slash": "f1f6",
+ "bell-slash-o": "f1f7",
+ "bicycle": "f206",
+ "binoculars": "f1e5",
+ "birthday-cake": "f1fd",
+ "bitbucket": "f171",
+ "bitbucket-square": "f172",
+ "bitcoin": "f15a",
+ "black-tie": "f27e",
+ "bluetooth": "f293",
+ "bluetooth-b": "f294",
+ "bold": "f032",
+ "bolt": "f0e7",
+ "bomb": "f1e2",
+ "book": "f02d",
+ "bookmark": "f02e",
+ "bookmark-o": "f097",
+ "briefcase": "f0b1",
+ "btc": "f15a",
+ "bug": "f188",
+ "building": "f1ad",
+ "building-o": "f0f7",
+ "bullhorn": "f0a1",
+ "bullseye": "f140",
+ "bus": "f207",
+ "buysellads": "f20d",
+ "cab": "f1ba",
+ "calculator": "f1ec",
+ "calendar": "f073",
+ "calendar-check-o": "f274",
+ "calendar-minus-o": "f272",
+ "calendar-o": "f133",
+ "calendar-plus-o": "f271",
+ "calendar-times-o": "f273",
+ "camera": "f030",
+ "camera-retro": "f083",
+ "car": "f1b9",
+ "caret-down": "f0d7",
+ "caret-left": "f0d9",
+ "caret-right": "f0da",
+ "caret-square-o-down": "f150",
+ "caret-square-o-left": "f191",
+ "caret-square-o-right": "f152",
+ "caret-square-o-up": "f151",
+ "caret-up": "f0d8",
+ "cart-arrow-down": "f218",
+ "cart-plus": "f217",
+ "cc": "f20a",
+ "cc-amex": "f1f3",
+ "cc-diners-club": "f24c",
+ "cc-discover": "f1f2",
+ "cc-jcb": "f24b",
+ "cc-mastercard": "f1f1",
+ "cc-paypal": "f1f4",
+ "cc-stripe": "f1f5",
+ "cc-visa": "f1f0",
+ "certificate": "f0a3",
+ "chain": "f0c1",
+ "chain-broken": "f127",
+ "check": "f00c",
+ "check-circle": "f058",
+ "check-circle-o": "f05d",
+ "check-square": "f14a",
+ "check-square-o": "f046",
+ "chevron-circle-down": "f13a",
+ "chevron-circle-left": "f137",
+ "chevron-circle-right": "f138",
+ "chevron-circle-up": "f139",
+ "chevron-down": "f078",
+ "chevron-left": "f053",
+ "chevron-right": "f054",
+ "chevron-up": "f077",
+ "child": "f1ae",
+ "chrome": "f268",
+ "circle": "f111",
+ "circle-o": "f10c",
+ "circle-o-notch": "f1ce",
+ "circle-thin": "f1db",
+ "clipboard": "f0ea",
+ "clock-o": "f017",
+ "clone": "f24d",
+ "close": "f00d",
+ "cloud": "f0c2",
+ "cloud-download": "f0ed",
+ "cloud-upload": "f0ee",
+ "cny": "f157",
+ "code": "f121",
+ "code-fork": "f126",
+ "codepen": "f1cb",
+ "codiepie": "f284",
+ "coffee": "f0f4",
+ "cog": "f013",
+ "cogs": "f085",
+ "columns": "f0db",
+ "comment": "f075",
+ "comment-o": "f0e5",
+ "commenting": "f27a",
+ "commenting-o": "f27b",
+ "comments": "f086",
+ "comments-o": "f0e6",
+ "compass": "f14e",
+ "compress": "f066",
+ "connectdevelop": "f20e",
+ "contao": "f26d",
+ "copy": "f0c5",
+ "copyright": "f1f9",
+ "creative-commons": "f25e",
+ "credit-card": "f09d",
+ "credit-card-alt": "f283",
+ "crop": "f125",
+ "crosshairs": "f05b",
+ "css3": "f13c",
+ "cube": "f1b2",
+ "cubes": "f1b3",
+ "cut": "f0c4",
+ "cutlery": "f0f5",
+ "dashboard": "f0e4",
+ "dashcube": "f210",
+ "database": "f1c0",
+ "dedent": "f03b",
+ "delicious": "f1a5",
+ "desktop": "f108",
+ "deviantart": "f1bd",
+ "diamond": "f219",
+ "digg": "f1a6",
+ "dollar": "f155",
+ "dot-circle-o": "f192",
+ "download": "f019",
+ "dribbble": "f17d",
+ "dropbox": "f16b",
+ "drupal": "f1a9",
+ "edge": "f282",
+ "edit": "f044",
+ "eject": "f052",
+ "ellipsis-h": "f141",
+ "ellipsis-v": "f142",
+ "empire": "f1d1",
+ "envelope": "f0e0",
+ "envelope-o": "f003",
+ "envelope-square": "f199",
+ "eraser": "f12d",
+ "eur": "f153",
+ "euro": "f153",
+ "exchange": "f0ec",
+ "exclamation": "f12a",
+ "exclamation-circle": "f06a",
+ "exclamation-triangle": "f071",
+ "expand": "f065",
+ "expeditedssl": "f23e",
+ "external-link": "f08e",
+ "external-link-square": "f14c",
+ "eye": "f06e",
+ "eye-slash": "f070",
+ "eyedropper": "f1fb",
+ "facebook": "f09a",
+ "facebook-f": "f09a",
+ "facebook-official": "f230",
+ "facebook-square": "f082",
+ "fast-backward": "f049",
+ "fast-forward": "f050",
+ "fax": "f1ac",
+ "feed": "f09e",
+ "female": "f182",
+ "fighter-jet": "f0fb",
+ "file": "f15b",
+ "file-archive-o": "f1c6",
+ "file-audio-o": "f1c7",
+ "file-code-o": "f1c9",
+ "file-excel-o": "f1c3",
+ "file-image-o": "f1c5",
+ "file-movie-o": "f1c8",
+ "file-o": "f016",
+ "file-pdf-o": "f1c1",
+ "file-photo-o": "f1c5",
+ "file-picture-o": "f1c5",
+ "file-powerpoint-o": "f1c4",
+ "file-sound-o": "f1c7",
+ "file-text": "f15c",
+ "file-text-o": "f0f6",
+ "file-video-o": "f1c8",
+ "file-word-o": "f1c2",
+ "file-zip-o": "f1c6",
+ "files-o": "f0c5",
+ "film": "f008",
+ "filter": "f0b0",
+ "fire": "f06d",
+ "fire-extinguisher": "f134",
+ "firefox": "f269",
+ "flag": "f024",
+ "flag-checkered": "f11e",
+ "flag-o": "f11d",
+ "flash": "f0e7",
+ "flask": "f0c3",
+ "flickr": "f16e",
+ "floppy-o": "f0c7",
+ "folder": "f07b",
+ "folder-o": "f114",
+ "folder-open": "f07c",
+ "folder-open-o": "f115",
+ "font": "f031",
+ "fonticons": "f280",
+ "fort-awesome": "f286",
+ "forumbee": "f211",
+ "forward": "f04e",
+ "foursquare": "f180",
+ "frown-o": "f119",
+ "futbol-o": "f1e3",
+ "gamepad": "f11b",
+ "gavel": "f0e3",
+ "gbp": "f154",
+ "ge": "f1d1",
+ "gear": "f013",
+ "gears": "f085",
+ "genderless": "f22d",
+ "get-pocket": "f265",
+ "gg": "f260",
+ "gg-circle": "f261",
+ "gift": "f06b",
+ "git": "f1d3",
+ "git-square": "f1d2",
+ "github": "f09b",
+ "github-alt": "f113",
+ "github-square": "f092",
+ "gittip": "f184",
+ "glass": "f000",
+ "globe": "f0ac",
+ "google": "f1a0",
+ "google-plus": "f0d5",
+ "google-plus-square": "f0d4",
+ "google-wallet": "f1ee",
+ "graduation-cap": "f19d",
+ "gratipay": "f184",
+ "group": "f0c0",
+ "h-square": "f0fd",
+ "hacker-news": "f1d4",
+ "hand-grab-o": "f255",
+ "hand-lizard-o": "f258",
+ "hand-o-down": "f0a7",
+ "hand-o-left": "f0a5",
+ "hand-o-right": "f0a4",
+ "hand-o-up": "f0a6",
+ "hand-paper-o": "f256",
+ "hand-peace-o": "f25b",
+ "hand-pointer-o": "f25a",
+ "hand-rock-o": "f255",
+ "hand-scissors-o": "f257",
+ "hand-spock-o": "f259",
+ "hand-stop-o": "f256",
+ "hashtag": "f292",
+ "hdd-o": "f0a0",
+ "header": "f1dc",
+ "headphones": "f025",
+ "heart": "f004",
+ "heart-o": "f08a",
+ "heartbeat": "f21e",
+ "history": "f1da",
+ "home": "f015",
+ "hospital-o": "f0f8",
+ "hotel": "f236",
+ "hourglass": "f254",
+ "hourglass-1": "f251",
+ "hourglass-2": "f252",
+ "hourglass-3": "f253",
+ "hourglass-end": "f253",
+ "hourglass-half": "f252",
+ "hourglass-o": "f250",
+ "hourglass-start": "f251",
+ "houzz": "f27c",
+ "html5": "f13b",
+ "i-cursor": "f246",
+ "ils": "f20b",
+ "image": "f03e",
+ "inbox": "f01c",
+ "indent": "f03c",
+ "industry": "f275",
+ "info": "f129",
+ "info-circle": "f05a",
+ "inr": "f156",
+ "instagram": "f16d",
+ "institution": "f19c",
+ "internet-explorer": "f26b",
+ "intersex": "f224",
+ "ioxhost": "f208",
+ "italic": "f033",
+ "joomla": "f1aa",
+ "jpy": "f157",
+ "jsfiddle": "f1cc",
+ "key": "f084",
+ "keyboard-o": "f11c",
+ "krw": "f159",
+ "language": "f1ab",
+ "laptop": "f109",
+ "lastfm": "f202",
+ "lastfm-square": "f203",
+ "leaf": "f06c",
+ "leanpub": "f212",
+ "legal": "f0e3",
+ "lemon-o": "f094",
+ "level-down": "f149",
+ "level-up": "f148",
+ "life-bouy": "f1cd",
+ "life-buoy": "f1cd",
+ "life-ring": "f1cd",
+ "life-saver": "f1cd",
+ "lightbulb-o": "f0eb",
+ "line-chart": "f201",
+ "link": "f0c1",
+ "linkedin": "f0e1",
+ "linkedin-square": "f08c",
+ "linux": "f17c",
+ "list": "f03a",
+ "list-alt": "f022",
+ "list-ol": "f0cb",
+ "list-ul": "f0ca",
+ "location-arrow": "f124",
+ "lock": "f023",
+ "long-arrow-down": "f175",
+ "long-arrow-left": "f177",
+ "long-arrow-right": "f178",
+ "long-arrow-up": "f176",
+ "magic": "f0d0",
+ "magnet": "f076",
+ "mail-forward": "f064",
+ "mail-reply": "f112",
+ "mail-reply-all": "f122",
+ "male": "f183",
+ "map": "f279",
+ "map-marker": "f041",
+ "map-o": "f278",
+ "map-pin": "f276",
+ "map-signs": "f277",
+ "mars": "f222",
+ "mars-double": "f227",
+ "mars-stroke": "f229",
+ "mars-stroke-h": "f22b",
+ "mars-stroke-v": "f22a",
+ "maxcdn": "f136",
+ "meanpath": "f20c",
+ "medium": "f23a",
+ "medkit": "f0fa",
+ "meh-o": "f11a",
+ "mercury": "f223",
+ "microphone": "f130",
+ "microphone-slash": "f131",
+ "minus": "f068",
+ "minus-circle": "f056",
+ "minus-square": "f146",
+ "minus-square-o": "f147",
+ "mixcloud": "f289",
+ "mobile": "f10b",
+ "mobile-phone": "f10b",
+ "modx": "f285",
+ "money": "f0d6",
+ "moon-o": "f186",
+ "mortar-board": "f19d",
+ "motorcycle": "f21c",
+ "mouse-pointer": "f245",
+ "music": "f001",
+ "navicon": "f0c9",
+ "neuter": "f22c",
+ "newspaper-o": "f1ea",
+ "object-group": "f247",
+ "object-ungroup": "f248",
+ "odnoklassniki": "f263",
+ "odnoklassniki-square": "f264",
+ "opencart": "f23d",
+ "openid": "f19b",
+ "opera": "f26a",
+ "optin-monster": "f23c",
+ "outdent": "f03b",
+ "pagelines": "f18c",
+ "paint-brush": "f1fc",
+ "paper-plane": "f1d8",
+ "paper-plane-o": "f1d9",
+ "paperclip": "f0c6",
+ "paragraph": "f1dd",
+ "paste": "f0ea",
+ "pause": "f04c",
+ "pause-circle": "f28b",
+ "pause-circle-o": "f28c",
+ "paw": "f1b0",
+ "paypal": "f1ed",
+ "pencil": "f040",
+ "pencil-square": "f14b",
+ "pencil-square-o": "f044",
+ "percent": "f295",
+ "phone": "f095",
+ "phone-square": "f098",
+ "photo": "f03e",
+ "picture-o": "f03e",
+ "pie-chart": "f200",
+ "pied-piper": "f1a7",
+ "pied-piper-alt": "f1a8",
+ "pinterest": "f0d2",
+ "pinterest-p": "f231",
+ "pinterest-square": "f0d3",
+ "plane": "f072",
+ "play": "f04b",
+ "play-circle": "f144",
+ "play-circle-o": "f01d",
+ "plug": "f1e6",
+ "plus": "f067",
+ "plus-circle": "f055",
+ "plus-square": "f0fe",
+ "plus-square-o": "f196",
+ "power-off": "f011",
+ "print": "f02f",
+ "product-hunt": "f288",
+ "puzzle-piece": "f12e",
+ "qq": "f1d6",
+ "qrcode": "f029",
+ "question": "f128",
+ "question-circle": "f059",
+ "quote-left": "f10d",
+ "quote-right": "f10e",
+ "ra": "f1d0",
+ "random": "f074",
+ "rebel": "f1d0",
+ "recycle": "f1b8",
+ "reddit": "f1a1",
+ "reddit-alien": "f281",
+ "reddit-square": "f1a2",
+ "refresh": "f021",
+ "registered": "f25d",
+ "remove": "f00d",
+ "renren": "f18b",
+ "reorder": "f0c9",
+ "repeat": "f01e",
+ "reply": "f112",
+ "reply-all": "f122",
+ "retweet": "f079",
+ "rmb": "f157",
+ "road": "f018",
+ "rocket": "f135",
+ "rotate-left": "f0e2",
+ "rotate-right": "f01e",
+ "rouble": "f158",
+ "rss": "f09e",
+ "rss-square": "f143",
+ "rub": "f158",
+ "ruble": "f158",
+ "rupee": "f156",
+ "safari": "f267",
+ "save": "f0c7",
+ "scissors": "f0c4",
+ "scribd": "f28a",
+ "search": "f002",
+ "search-minus": "f010",
+ "search-plus": "f00e",
+ "sellsy": "f213",
+ "send": "f1d8",
+ "send-o": "f1d9",
+ "server": "f233",
+ "share": "f064",
+ "share-alt": "f1e0",
+ "share-alt-square": "f1e1",
+ "share-square": "f14d",
+ "share-square-o": "f045",
+ "shekel": "f20b",
+ "sheqel": "f20b",
+ "shield": "f132",
+ "ship": "f21a",
+ "shirtsinbulk": "f214",
+ "shopping-bag": "f290",
+ "shopping-basket": "f291",
+ "shopping-cart": "f07a",
+ "sign-in": "f090",
+ "sign-out": "f08b",
+ "signal": "f012",
+ "simplybuilt": "f215",
+ "sitemap": "f0e8",
+ "skyatlas": "f216",
+ "skype": "f17e",
+ "slack": "f198",
+ "sliders": "f1de",
+ "slideshare": "f1e7",
+ "smile-o": "f118",
+ "soccer-ball-o": "f1e3",
+ "sort": "f0dc",
+ "sort-alpha-asc": "f15d",
+ "sort-alpha-desc": "f15e",
+ "sort-amount-asc": "f160",
+ "sort-amount-desc": "f161",
+ "sort-asc": "f0de",
+ "sort-desc": "f0dd",
+ "sort-down": "f0dd",
+ "sort-numeric-asc": "f162",
+ "sort-numeric-desc": "f163",
+ "sort-up": "f0de",
+ "soundcloud": "f1be",
+ "space-shuttle": "f197",
+ "spinner": "f110",
+ "spoon": "f1b1",
+ "spotify": "f1bc",
+ "square": "f0c8",
+ "square-o": "f096",
+ "stack-exchange": "f18d",
+ "stack-overflow": "f16c",
+ "star": "f005",
+ "star-half": "f089",
+ "star-half-empty": "f123",
+ "star-half-full": "f123",
+ "star-half-o": "f123",
+ "star-o": "f006",
+ "steam": "f1b6",
+ "steam-square": "f1b7",
+ "step-backward": "f048",
+ "step-forward": "f051",
+ "stethoscope": "f0f1",
+ "sticky-note": "f249",
+ "sticky-note-o": "f24a",
+ "stop": "f04d",
+ "stop-circle": "f28d",
+ "stop-circle-o": "f28e",
+ "street-view": "f21d",
+ "strikethrough": "f0cc",
+ "stumbleupon": "f1a4",
+ "stumbleupon-circle": "f1a3",
+ "subscript": "f12c",
+ "subway": "f239",
+ "suitcase": "f0f2",
+ "sun-o": "f185",
+ "superscript": "f12b",
+ "support": "f1cd",
+ "table": "f0ce",
+ "tablet": "f10a",
+ "tachometer": "f0e4",
+ "tag": "f02b",
+ "tags": "f02c",
+ "tasks": "f0ae",
+ "taxi": "f1ba",
+ "television": "f26c",
+ "tencent-weibo": "f1d5",
+ "terminal": "f120",
+ "text-height": "f034",
+ "text-width": "f035",
+ "th": "f00a",
+ "th-large": "f009",
+ "th-list": "f00b",
+ "thumb-tack": "f08d",
+ "thumbs-down": "f165",
+ "thumbs-o-down": "f088",
+ "thumbs-o-up": "f087",
+ "thumbs-up": "f164",
+ "ticket": "f145",
+ "times": "f00d",
+ "times-circle": "f057",
+ "times-circle-o": "f05c",
+ "tint": "f043",
+ "toggle-down": "f150",
+ "toggle-left": "f191",
+ "toggle-off": "f204",
+ "toggle-on": "f205",
+ "toggle-right": "f152",
+ "toggle-up": "f151",
+ "trademark": "f25c",
+ "train": "f238",
+ "transgender": "f224",
+ "transgender-alt": "f225",
+ "trash": "f1f8",
+ "trash-o": "f014",
+ "tree": "f1bb",
+ "trello": "f181",
+ "tripadvisor": "f262",
+ "trophy": "f091",
+ "truck": "f0d1",
+ "try": "f195",
+ "tty": "f1e4",
+ "tumblr": "f173",
+ "tumblr-square": "f174",
+ "turkish-lira": "f195",
+ "tv": "f26c",
+ "twitch": "f1e8",
+ "twitter": "f099",
+ "twitter-square": "f081",
+ "umbrella": "f0e9",
+ "underline": "f0cd",
+ "undo": "f0e2",
+ "university": "f19c",
+ "unlink": "f127",
+ "unlock": "f09c",
+ "unlock-alt": "f13e",
+ "unsorted": "f0dc",
+ "upload": "f093",
+ "usb": "f287",
+ "usd": "f155",
+ "user": "f007",
+ "user-md": "f0f0",
+ "user-plus": "f234",
+ "user-secret": "f21b",
+ "user-times": "f235",
+ "users": "f0c0",
+ "venus": "f221",
+ "venus-double": "f226",
+ "venus-mars": "f228",
+ "viacoin": "f237",
+ "video-camera": "f03d",
+ "vimeo": "f27d",
+ "vimeo-square": "f194",
+ "vine": "f1ca",
+ "vk": "f189",
+ "volume-down": "f027",
+ "volume-off": "f026",
+ "volume-up": "f028",
+ "warning": "f071",
+ "wechat": "f1d7",
+ "weibo": "f18a",
+ "weixin": "f1d7",
+ "whatsapp": "f232",
+ "wheelchair": "f193",
+ "wifi": "f1eb",
+ "wikipedia-w": "f266",
+ "windows": "f17a",
+ "won": "f159",
+ "wordpress": "f19a",
+ "wrench": "f0ad",
+ "xing": "f168",
+ "xing-square": "f169",
+ "y-combinator": "f23b",
+ "y-combinator-square": "f1d4",
+ "yahoo": "f19e",
+ "yc": "f23b",
+ "yc-square": "f1d4",
+ "yelp": "f1e9",
+ "yen": "f157",
+ "youtube": "f167",
+ "youtube-play": "f16a",
+ "youtube-square": "f166"
+}
\ No newline at end of file
diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont.ttf b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont.ttf
new file mode 100644
index 0000000000..26dea7951a
Binary files /dev/null and b/pype/tools/pyblish_pype/vendor/qtawesome/fonts/fontawesome-webfont.ttf differ
diff --git a/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py b/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py
new file mode 100644
index 0000000000..70f5ec2dec
--- /dev/null
+++ b/pype/tools/pyblish_pype/vendor/qtawesome/iconic_font.py
@@ -0,0 +1,287 @@
+"""Classes handling iconic fonts"""
+
+from __future__ import print_function
+
+import json
+import os
+
+from .. import six
+from ..Qt import QtCore, QtGui
+
+
+_default_options = {
+ 'color': QtGui.QColor(50, 50, 50),
+ 'color_disabled': QtGui.QColor(150, 150, 150),
+ 'opacity': 1.0,
+ 'scale_factor': 1.0,
+}
+
+
+def set_global_defaults(**kwargs):
+ """Set global defaults for all icons"""
+ valid_options = ['active', 'animation', 'color', 'color_active',
+ 'color_disabled', 'color_selected', 'disabled', 'offset',
+ 'scale_factor', 'selected']
+ for kw in kwargs:
+ if kw in valid_options:
+ _default_options[kw] = kwargs[kw]
+ else:
+ error = "Invalid option '{0}'".format(kw)
+ raise KeyError(error)
+
+
+class CharIconPainter:
+
+ """Char icon painter"""
+
+ def paint(self, iconic, painter, rect, mode, state, options):
+ """Main paint method"""
+ for opt in options:
+ self._paint_icon(iconic, painter, rect, mode, state, opt)
+
+ def _paint_icon(self, iconic, painter, rect, mode, state, options):
+ """Paint a single icon"""
+ painter.save()
+ color, char = options['color'], options['char']
+
+ if mode == QtGui.QIcon.Disabled:
+ color = options.get('color_disabled', color)
+ char = options.get('disabled', char)
+ elif mode == QtGui.QIcon.Active:
+ color = options.get('color_active', color)
+ char = options.get('active', char)
+ elif mode == QtGui.QIcon.Selected:
+ color = options.get('color_selected', color)
+ char = options.get('selected', char)
+
+ painter.setPen(QtGui.QColor(color))
+ # A 16 pixel-high icon yields a font size of 14, which is pixel perfect
+ # for font-awesome. 16 * 0.875 = 14
+ # The reason for not using full-sized glyphs is the negative bearing of
+ # fonts.
+ draw_size = 0.875 * round(rect.height() * options['scale_factor'])
+ prefix = options['prefix']
+
+ # Animation setup hook
+ animation = options.get('animation')
+ if animation is not None:
+ animation.setup(self, painter, rect)
+
+ painter.setFont(iconic.font(prefix, draw_size))
+ if 'offset' in options:
+ rect = QtCore.QRect(rect)
+ rect.translate(options['offset'][0] * rect.width(),
+ options['offset'][1] * rect.height())
+
+ painter.setOpacity(options.get('opacity', 1.0))
+
+ painter.drawText(rect,
+ QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter,
+ char)
+ painter.restore()
+
+
+class CharIconEngine(QtGui.QIconEngine):
+
+ """Specialization of QtGui.QIconEngine used to draw font-based icons"""
+
+ def __init__(self, iconic, painter, options):
+ super(CharIconEngine, self).__init__()
+ self.iconic = iconic
+ self.painter = painter
+ self.options = options
+
+ def paint(self, painter, rect, mode, state):
+ self.painter.paint(
+ self.iconic, painter, rect, mode, state, self.options)
+
+ def pixmap(self, size, mode, state):
+ pm = QtGui.QPixmap(size)
+ pm.fill(QtCore.Qt.transparent)
+ self.paint(QtGui.QPainter(pm),
+ QtCore.QRect(QtCore.QPoint(0, 0), size),
+ mode,
+ state)
+ return pm
+
+
+class IconicFont(QtCore.QObject):
+
+ """Main class for managing iconic fonts"""
+
+ def __init__(self, *args):
+ """Constructor
+
+ :param *args: tuples
+ Each positional argument is a tuple of 3 or 4 values
+ - The prefix string to be used when accessing a given font set
+ - The ttf font filename
+ - The json charmap filename
+ - Optionally, the directory containing these files. When not
+ provided, the files will be looked up in ./fonts/
+ """
+ super(IconicFont, self).__init__()
+ self.painter = CharIconPainter()
+ self.painters = {}
+ self.fontname = {}
+ self.charmap = {}
+ for fargs in args:
+ self.load_font(*fargs)
+
+ def load_font(self,
+ prefix,
+ ttf_filename,
+ charmap_filename,
+ directory=None):
+ """Loads a font file and the associated charmap
+
+ If `directory` is None, the files will be looked up in ./fonts/
+
+ Arguments
+ ---------
+ prefix: str
+ prefix string to be used when accessing a given font set
+ ttf_filename: str
+ ttf font filename
+ charmap_filename: str
+ charmap filename
+ directory: str or None, optional
+ directory for font and charmap files
+ """
+
+ def hook(obj):
+ result = {}
+ for key in obj:
+ result[key] = six.unichr(int(obj[key], 16))
+ return result
+
+ if directory is None:
+ directory = os.path.join(
+ os.path.dirname(os.path.realpath(__file__)), 'fonts')
+
+ with open(os.path.join(directory, charmap_filename), 'r') as codes:
+ self.charmap[prefix] = json.load(codes, object_hook=hook)
+
+ id_ = QtGui.QFontDatabase.addApplicationFont(
+ os.path.join(directory, ttf_filename))
+
+ loadedFontFamilies = QtGui.QFontDatabase.applicationFontFamilies(id_)
+
+ if(loadedFontFamilies):
+ self.fontname[prefix] = loadedFontFamilies[0]
+ else:
+ print('Font is empty')
+
+ def icon(self, *names, **kwargs):
+ """Returns a QtGui.QIcon object corresponding to the provided icon name
+ (including prefix)
+
+ Arguments
+ ---------
+ names: list of str
+ icon name, of the form PREFIX.NAME
+
+ options: dict
+ options to be passed to the icon painter
+ """
+ options_list = kwargs.pop('options', [{}] * len(names))
+ general_options = kwargs
+
+ if len(options_list) != len(names):
+ error = '"options" must be a list of size {0}'.format(len(names))
+ raise Exception(error)
+
+ parsed_options = []
+ for i in range(len(options_list)):
+ specific_options = options_list[i]
+ parsed_options.append(self._parse_options(specific_options,
+ general_options,
+ names[i]))
+
+ # Process high level API
+ api_options = parsed_options
+
+ return self._icon_by_painter(self.painter, api_options)
+
+ def _parse_options(self, specific_options, general_options, name):
+ """ """
+ options = dict(_default_options, **general_options)
+ options.update(specific_options)
+
+ # Handle icons for states
+ icon_kw = ['disabled', 'active', 'selected', 'char']
+ names = [options.get(kw, name) for kw in icon_kw]
+ prefix, chars = self._get_prefix_chars(names)
+ options.update(dict(zip(*(icon_kw, chars))))
+ options.update({'prefix': prefix})
+
+ # Handle colors for states
+ color_kw = ['color_active', 'color_selected']
+ colors = [options.get(kw, options['color']) for kw in color_kw]
+ options.update(dict(zip(*(color_kw, colors))))
+
+ return options
+
+ def _get_prefix_chars(self, names):
+ """ """
+ chars = []
+ for name in names:
+ if '.' in name:
+ prefix, n = name.split('.')
+ if prefix in self.charmap:
+ if n in self.charmap[prefix]:
+ chars.append(self.charmap[prefix][n])
+ else:
+ error = 'Invalid icon name "{0}" in font "{1}"'.format(
+ n, prefix)
+ raise Exception(error)
+ else:
+ error = 'Invalid font prefix "{0}"'.format(prefix)
+ raise Exception(error)
+ else:
+ raise Exception('Invalid icon name')
+
+ return prefix, chars
+
+ def font(self, prefix, size):
+ """Returns QtGui.QFont corresponding to the given prefix and size
+
+ Arguments
+ ---------
+ prefix: str
+ prefix string of the loaded font
+ size: int
+ size for the font
+ """
+ font = QtGui.QFont(self.fontname[prefix])
+ font.setPixelSize(size)
+ return font
+
+ def set_custom_icon(self, name, painter):
+ """Associates a user-provided CharIconPainter to an icon name
+ The custom icon can later be addressed by calling
+ icon('custom.NAME') where NAME is the provided name for that icon.
+
+ Arguments
+ ---------
+ name: str
+ name of the custom icon
+ painter: CharIconPainter
+ The icon painter, implementing
+ `paint(self, iconic, painter, rect, mode, state, options)`
+ """
+ self.painters[name] = painter
+
+ def _custom_icon(self, name, **kwargs):
+ """Returns the custom icon corresponding to the given name"""
+ options = dict(_default_options, **kwargs)
+ if name in self.painters:
+ painter = self.painters[name]
+ return self._icon_by_painter(painter, options)
+ else:
+ return QtGui.QIcon()
+
+ def _icon_by_painter(self, painter, options):
+ """Returns the icon corresponding to the given painter"""
+ engine = CharIconEngine(self, painter, options)
+ return QtGui.QIcon(engine)
diff --git a/pype/tools/pyblish_pype/vendor/six.py b/pype/tools/pyblish_pype/vendor/six.py
new file mode 100644
index 0000000000..190c0239cd
--- /dev/null
+++ b/pype/tools/pyblish_pype/vendor/six.py
@@ -0,0 +1,868 @@
+"""Utilities for writing code that runs on Python 2 and 3"""
+
+# Copyright (c) 2010-2015 Benjamin Peterson
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+from __future__ import absolute_import
+
+import functools
+import itertools
+import operator
+import sys
+import types
+
+__author__ = "Benjamin Peterson "
+__version__ = "1.10.0"
+
+
+# Useful for very coarse version differentiation.
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+PY34 = sys.version_info[0:2] >= (3, 4)
+
+if PY3:
+ string_types = str,
+ integer_types = int,
+ class_types = type,
+ text_type = str
+ binary_type = bytes
+
+ MAXSIZE = sys.maxsize
+else:
+ string_types = basestring,
+ integer_types = (int, long)
+ class_types = (type, types.ClassType)
+ text_type = unicode
+ binary_type = str
+
+ if sys.platform.startswith("java"):
+ # Jython always uses 32 bits.
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
+ class X(object):
+
+ def __len__(self):
+ return 1 << 31
+ try:
+ len(X())
+ except OverflowError:
+ # 32-bit
+ MAXSIZE = int((1 << 31) - 1)
+ else:
+ # 64-bit
+ MAXSIZE = int((1 << 63) - 1)
+ del X
+
+
+def _add_doc(func, doc):
+ """Add documentation to a function."""
+ func.__doc__ = doc
+
+
+def _import_module(name):
+ """Import module, returning the module after the last dot."""
+ __import__(name)
+ return sys.modules[name]
+
+
+class _LazyDescr(object):
+
+ def __init__(self, name):
+ self.name = name
+
+ def __get__(self, obj, tp):
+ result = self._resolve()
+ setattr(obj, self.name, result) # Invokes __set__.
+ try:
+ # This is a bit ugly, but it avoids running this again by
+ # removing this descriptor.
+ delattr(obj.__class__, self.name)
+ except AttributeError:
+ pass
+ return result
+
+
+class MovedModule(_LazyDescr):
+
+ def __init__(self, name, old, new=None):
+ super(MovedModule, self).__init__(name)
+ if PY3:
+ if new is None:
+ new = name
+ self.mod = new
+ else:
+ self.mod = old
+
+ def _resolve(self):
+ return _import_module(self.mod)
+
+ def __getattr__(self, attr):
+ _module = self._resolve()
+ value = getattr(_module, attr)
+ setattr(self, attr, value)
+ return value
+
+
+class _LazyModule(types.ModuleType):
+
+ def __init__(self, name):
+ super(_LazyModule, self).__init__(name)
+ self.__doc__ = self.__class__.__doc__
+
+ def __dir__(self):
+ attrs = ["__doc__", "__name__"]
+ attrs += [attr.name for attr in self._moved_attributes]
+ return attrs
+
+ # Subclasses should override this
+ _moved_attributes = []
+
+
+class MovedAttribute(_LazyDescr):
+
+ def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
+ super(MovedAttribute, self).__init__(name)
+ if PY3:
+ if new_mod is None:
+ new_mod = name
+ self.mod = new_mod
+ if new_attr is None:
+ if old_attr is None:
+ new_attr = name
+ else:
+ new_attr = old_attr
+ self.attr = new_attr
+ else:
+ self.mod = old_mod
+ if old_attr is None:
+ old_attr = name
+ self.attr = old_attr
+
+ def _resolve(self):
+ module = _import_module(self.mod)
+ return getattr(module, self.attr)
+
+
+class _SixMetaPathImporter(object):
+
+ """
+ A meta path importer to import six.moves and its submodules.
+
+ This class implements a PEP302 finder and loader. It should be compatible
+ with Python 2.5 and all existing versions of Python3
+ """
+
+ def __init__(self, six_module_name):
+ self.name = six_module_name
+ self.known_modules = {}
+
+ def _add_module(self, mod, *fullnames):
+ for fullname in fullnames:
+ self.known_modules[self.name + "." + fullname] = mod
+
+ def _get_module(self, fullname):
+ return self.known_modules[self.name + "." + fullname]
+
+ def find_module(self, fullname, path=None):
+ if fullname in self.known_modules:
+ return self
+ return None
+
+ def __get_module(self, fullname):
+ try:
+ return self.known_modules[fullname]
+ except KeyError:
+ raise ImportError("This loader does not know module " + fullname)
+
+ def load_module(self, fullname):
+ try:
+ # in case of a reload
+ return sys.modules[fullname]
+ except KeyError:
+ pass
+ mod = self.__get_module(fullname)
+ if isinstance(mod, MovedModule):
+ mod = mod._resolve()
+ else:
+ mod.__loader__ = self
+ sys.modules[fullname] = mod
+ return mod
+
+ def is_package(self, fullname):
+ """
+ Return true, if the named module is a package.
+
+ We need this method to get correct spec objects with
+ Python 3.4 (see PEP451)
+ """
+ return hasattr(self.__get_module(fullname), "__path__")
+
+ def get_code(self, fullname):
+ """Return None
+
+ Required, if is_package is implemented"""
+ self.__get_module(fullname) # eventually raises ImportError
+ return None
+ get_source = get_code # same as get_code
+
+_importer = _SixMetaPathImporter(__name__)
+
+
+class _MovedItems(_LazyModule):
+
+ """Lazy loading of moved objects"""
+ __path__ = [] # mark as package
+
+
+_moved_attributes = [
+ MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
+ MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
+ MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"),
+ MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
+ MovedAttribute("intern", "__builtin__", "sys"),
+ MovedAttribute("map", "itertools", "builtins", "imap", "map"),
+ MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"),
+ MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"),
+ MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"),
+ MovedAttribute("reduce", "__builtin__", "functools"),
+ MovedAttribute("shlex_quote", "pipes", "shlex", "quote"),
+ MovedAttribute("StringIO", "StringIO", "io"),
+ MovedAttribute("UserDict", "UserDict", "collections"),
+ MovedAttribute("UserList", "UserList", "collections"),
+ MovedAttribute("UserString", "UserString", "collections"),
+ MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
+ MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
+ MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"),
+ MovedModule("builtins", "__builtin__"),
+ MovedModule("configparser", "ConfigParser"),
+ MovedModule("copyreg", "copy_reg"),
+ MovedModule("dbm_gnu", "gdbm", "dbm.gnu"),
+ MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"),
+ MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
+ MovedModule("http_cookies", "Cookie", "http.cookies"),
+ MovedModule("html_entities", "htmlentitydefs", "html.entities"),
+ MovedModule("html_parser", "HTMLParser", "html.parser"),
+ MovedModule("http_client", "httplib", "http.client"),
+ MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"),
+ MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"),
+ MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"),
+ MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"),
+ MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
+ MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
+ MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
+ MovedModule("cPickle", "cPickle", "pickle"),
+ MovedModule("queue", "Queue"),
+ MovedModule("reprlib", "repr"),
+ MovedModule("socketserver", "SocketServer"),
+ MovedModule("_thread", "thread", "_thread"),
+ MovedModule("tkinter", "Tkinter"),
+ MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
+ MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
+ MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
+ MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
+ MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"),
+ MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
+ MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
+ MovedModule("tkinter_colorchooser", "tkColorChooser",
+ "tkinter.colorchooser"),
+ MovedModule("tkinter_commondialog", "tkCommonDialog",
+ "tkinter.commondialog"),
+ MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
+ MovedModule("tkinter_font", "tkFont", "tkinter.font"),
+ MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
+ MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
+ "tkinter.simpledialog"),
+ MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"),
+ MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"),
+ MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"),
+ MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
+ MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"),
+ MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"),
+]
+# Add windows specific modules.
+if sys.platform == "win32":
+ _moved_attributes += [
+ MovedModule("winreg", "_winreg"),
+ ]
+
+for attr in _moved_attributes:
+ setattr(_MovedItems, attr.name, attr)
+ if isinstance(attr, MovedModule):
+ _importer._add_module(attr, "moves." + attr.name)
+del attr
+
+_MovedItems._moved_attributes = _moved_attributes
+
+moves = _MovedItems(__name__ + ".moves")
+_importer._add_module(moves, "moves")
+
+
+class Module_six_moves_urllib_parse(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_parse"""
+
+
+_urllib_parse_moved_attributes = [
+ MovedAttribute("ParseResult", "urlparse", "urllib.parse"),
+ MovedAttribute("SplitResult", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qs", "urlparse", "urllib.parse"),
+ MovedAttribute("parse_qsl", "urlparse", "urllib.parse"),
+ MovedAttribute("urldefrag", "urlparse", "urllib.parse"),
+ MovedAttribute("urljoin", "urlparse", "urllib.parse"),
+ MovedAttribute("urlparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunparse", "urlparse", "urllib.parse"),
+ MovedAttribute("urlunsplit", "urlparse", "urllib.parse"),
+ MovedAttribute("quote", "urllib", "urllib.parse"),
+ MovedAttribute("quote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("unquote", "urllib", "urllib.parse"),
+ MovedAttribute("unquote_plus", "urllib", "urllib.parse"),
+ MovedAttribute("urlencode", "urllib", "urllib.parse"),
+ MovedAttribute("splitquery", "urllib", "urllib.parse"),
+ MovedAttribute("splittag", "urllib", "urllib.parse"),
+ MovedAttribute("splituser", "urllib", "urllib.parse"),
+ MovedAttribute("uses_fragment", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_netloc", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_params", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_query", "urlparse", "urllib.parse"),
+ MovedAttribute("uses_relative", "urlparse", "urllib.parse"),
+]
+for attr in _urllib_parse_moved_attributes:
+ setattr(Module_six_moves_urllib_parse, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"),
+ "moves.urllib_parse", "moves.urllib.parse")
+
+
+class Module_six_moves_urllib_error(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_error"""
+
+
+_urllib_error_moved_attributes = [
+ MovedAttribute("URLError", "urllib2", "urllib.error"),
+ MovedAttribute("HTTPError", "urllib2", "urllib.error"),
+ MovedAttribute("ContentTooShortError", "urllib", "urllib.error"),
+]
+for attr in _urllib_error_moved_attributes:
+ setattr(Module_six_moves_urllib_error, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"),
+ "moves.urllib_error", "moves.urllib.error")
+
+
+class Module_six_moves_urllib_request(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_request"""
+
+
+_urllib_request_moved_attributes = [
+ MovedAttribute("urlopen", "urllib2", "urllib.request"),
+ MovedAttribute("install_opener", "urllib2", "urllib.request"),
+ MovedAttribute("build_opener", "urllib2", "urllib.request"),
+ MovedAttribute("pathname2url", "urllib", "urllib.request"),
+ MovedAttribute("url2pathname", "urllib", "urllib.request"),
+ MovedAttribute("getproxies", "urllib", "urllib.request"),
+ MovedAttribute("Request", "urllib2", "urllib.request"),
+ MovedAttribute("OpenerDirector", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyHandler", "urllib2", "urllib.request"),
+ MovedAttribute("BaseHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FileHandler", "urllib2", "urllib.request"),
+ MovedAttribute("FTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"),
+ MovedAttribute("UnknownHandler", "urllib2", "urllib.request"),
+ MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"),
+ MovedAttribute("urlretrieve", "urllib", "urllib.request"),
+ MovedAttribute("urlcleanup", "urllib", "urllib.request"),
+ MovedAttribute("URLopener", "urllib", "urllib.request"),
+ MovedAttribute("FancyURLopener", "urllib", "urllib.request"),
+ MovedAttribute("proxy_bypass", "urllib", "urllib.request"),
+]
+for attr in _urllib_request_moved_attributes:
+ setattr(Module_six_moves_urllib_request, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"),
+ "moves.urllib_request", "moves.urllib.request")
+
+
+class Module_six_moves_urllib_response(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_response"""
+
+
+_urllib_response_moved_attributes = [
+ MovedAttribute("addbase", "urllib", "urllib.response"),
+ MovedAttribute("addclosehook", "urllib", "urllib.response"),
+ MovedAttribute("addinfo", "urllib", "urllib.response"),
+ MovedAttribute("addinfourl", "urllib", "urllib.response"),
+]
+for attr in _urllib_response_moved_attributes:
+ setattr(Module_six_moves_urllib_response, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"),
+ "moves.urllib_response", "moves.urllib.response")
+
+
+class Module_six_moves_urllib_robotparser(_LazyModule):
+
+ """Lazy loading of moved objects in six.moves.urllib_robotparser"""
+
+
+_urllib_robotparser_moved_attributes = [
+ MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"),
+]
+for attr in _urllib_robotparser_moved_attributes:
+ setattr(Module_six_moves_urllib_robotparser, attr.name, attr)
+del attr
+
+Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes
+
+_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"),
+ "moves.urllib_robotparser", "moves.urllib.robotparser")
+
+
+class Module_six_moves_urllib(types.ModuleType):
+
+ """Create a six.moves.urllib namespace that resembles the Python 3 namespace"""
+ __path__ = [] # mark as package
+ parse = _importer._get_module("moves.urllib_parse")
+ error = _importer._get_module("moves.urllib_error")
+ request = _importer._get_module("moves.urllib_request")
+ response = _importer._get_module("moves.urllib_response")
+ robotparser = _importer._get_module("moves.urllib_robotparser")
+
+ def __dir__(self):
+ return ['parse', 'error', 'request', 'response', 'robotparser']
+
+_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"),
+ "moves.urllib")
+
+
+def add_move(move):
+ """Add an item to six.moves."""
+ setattr(_MovedItems, move.name, move)
+
+
+def remove_move(name):
+ """Remove item from six.moves."""
+ try:
+ delattr(_MovedItems, name)
+ except AttributeError:
+ try:
+ del moves.__dict__[name]
+ except KeyError:
+ raise AttributeError("no such move, %r" % (name,))
+
+
+if PY3:
+ _meth_func = "__func__"
+ _meth_self = "__self__"
+
+ _func_closure = "__closure__"
+ _func_code = "__code__"
+ _func_defaults = "__defaults__"
+ _func_globals = "__globals__"
+else:
+ _meth_func = "im_func"
+ _meth_self = "im_self"
+
+ _func_closure = "func_closure"
+ _func_code = "func_code"
+ _func_defaults = "func_defaults"
+ _func_globals = "func_globals"
+
+
+try:
+ advance_iterator = next
+except NameError:
+ def advance_iterator(it):
+ return it.next()
+next = advance_iterator
+
+
+try:
+ callable = callable
+except NameError:
+ def callable(obj):
+ return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
+
+
+if PY3:
+ def get_unbound_function(unbound):
+ return unbound
+
+ create_bound_method = types.MethodType
+
+ def create_unbound_method(func, cls):
+ return func
+
+ Iterator = object
+else:
+ def get_unbound_function(unbound):
+ return unbound.im_func
+
+ def create_bound_method(func, obj):
+ return types.MethodType(func, obj, obj.__class__)
+
+ def create_unbound_method(func, cls):
+ return types.MethodType(func, None, cls)
+
+ class Iterator(object):
+
+ def next(self):
+ return type(self).__next__(self)
+
+ callable = callable
+_add_doc(get_unbound_function,
+ """Get the function out of a possibly unbound function""")
+
+
+get_method_function = operator.attrgetter(_meth_func)
+get_method_self = operator.attrgetter(_meth_self)
+get_function_closure = operator.attrgetter(_func_closure)
+get_function_code = operator.attrgetter(_func_code)
+get_function_defaults = operator.attrgetter(_func_defaults)
+get_function_globals = operator.attrgetter(_func_globals)
+
+
+if PY3:
+ def iterkeys(d, **kw):
+ return iter(d.keys(**kw))
+
+ def itervalues(d, **kw):
+ return iter(d.values(**kw))
+
+ def iteritems(d, **kw):
+ return iter(d.items(**kw))
+
+ def iterlists(d, **kw):
+ return iter(d.lists(**kw))
+
+ viewkeys = operator.methodcaller("keys")
+
+ viewvalues = operator.methodcaller("values")
+
+ viewitems = operator.methodcaller("items")
+else:
+ def iterkeys(d, **kw):
+ return d.iterkeys(**kw)
+
+ def itervalues(d, **kw):
+ return d.itervalues(**kw)
+
+ def iteritems(d, **kw):
+ return d.iteritems(**kw)
+
+ def iterlists(d, **kw):
+ return d.iterlists(**kw)
+
+ viewkeys = operator.methodcaller("viewkeys")
+
+ viewvalues = operator.methodcaller("viewvalues")
+
+ viewitems = operator.methodcaller("viewitems")
+
+_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.")
+_add_doc(itervalues, "Return an iterator over the values of a dictionary.")
+_add_doc(iteritems,
+ "Return an iterator over the (key, value) pairs of a dictionary.")
+_add_doc(iterlists,
+ "Return an iterator over the (key, [values]) pairs of a dictionary.")
+
+
+if PY3:
+ def b(s):
+ return s.encode("latin-1")
+
+ def u(s):
+ return s
+ unichr = chr
+ import struct
+ int2byte = struct.Struct(">B").pack
+ del struct
+ byte2int = operator.itemgetter(0)
+ indexbytes = operator.getitem
+ iterbytes = iter
+ import io
+ StringIO = io.StringIO
+ BytesIO = io.BytesIO
+ _assertCountEqual = "assertCountEqual"
+ if sys.version_info[1] <= 1:
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+ else:
+ _assertRaisesRegex = "assertRaisesRegex"
+ _assertRegex = "assertRegex"
+else:
+ def b(s):
+ return s
+ # Workaround for standalone backslash
+
+ def u(s):
+ return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape")
+ unichr = unichr
+ int2byte = chr
+
+ def byte2int(bs):
+ return ord(bs[0])
+
+ def indexbytes(buf, i):
+ return ord(buf[i])
+ iterbytes = functools.partial(itertools.imap, ord)
+ import StringIO
+ StringIO = BytesIO = StringIO.StringIO
+ _assertCountEqual = "assertItemsEqual"
+ _assertRaisesRegex = "assertRaisesRegexp"
+ _assertRegex = "assertRegexpMatches"
+_add_doc(b, """Byte literal""")
+_add_doc(u, """Text literal""")
+
+
+def assertCountEqual(self, *args, **kwargs):
+ return getattr(self, _assertCountEqual)(*args, **kwargs)
+
+
+def assertRaisesRegex(self, *args, **kwargs):
+ return getattr(self, _assertRaisesRegex)(*args, **kwargs)
+
+
+def assertRegex(self, *args, **kwargs):
+ return getattr(self, _assertRegex)(*args, **kwargs)
+
+
+if PY3:
+ exec_ = getattr(moves.builtins, "exec")
+
+ def reraise(tp, value, tb=None):
+ if value is None:
+ value = tp()
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+
+else:
+ def exec_(_code_, _globs_=None, _locs_=None):
+ """Execute code in a namespace."""
+ if _globs_ is None:
+ frame = sys._getframe(1)
+ _globs_ = frame.f_globals
+ if _locs_ is None:
+ _locs_ = frame.f_locals
+ del frame
+ elif _locs_ is None:
+ _locs_ = _globs_
+ exec("""exec _code_ in _globs_, _locs_""")
+
+ exec_("""def reraise(tp, value, tb=None):
+ raise tp, value, tb
+""")
+
+
+if sys.version_info[:2] == (3, 2):
+ exec_("""def raise_from(value, from_value):
+ if from_value is None:
+ raise value
+ raise value from from_value
+""")
+elif sys.version_info[:2] > (3, 2):
+ exec_("""def raise_from(value, from_value):
+ raise value from from_value
+""")
+else:
+ def raise_from(value, from_value):
+ raise value
+
+
+print_ = getattr(moves.builtins, "print", None)
+if print_ is None:
+ def print_(*args, **kwargs):
+ """The new-style print function for Python 2.4 and 2.5."""
+ fp = kwargs.pop("file", sys.stdout)
+ if fp is None:
+ return
+
+ def write(data):
+ if not isinstance(data, basestring):
+ data = str(data)
+ # If the file has an encoding, encode unicode with it.
+ if (isinstance(fp, file) and
+ isinstance(data, unicode) and
+ fp.encoding is not None):
+ errors = getattr(fp, "errors", None)
+ if errors is None:
+ errors = "strict"
+ data = data.encode(fp.encoding, errors)
+ fp.write(data)
+ want_unicode = False
+ sep = kwargs.pop("sep", None)
+ if sep is not None:
+ if isinstance(sep, unicode):
+ want_unicode = True
+ elif not isinstance(sep, str):
+ raise TypeError("sep must be None or a string")
+ end = kwargs.pop("end", None)
+ if end is not None:
+ if isinstance(end, unicode):
+ want_unicode = True
+ elif not isinstance(end, str):
+ raise TypeError("end must be None or a string")
+ if kwargs:
+ raise TypeError("invalid keyword arguments to print()")
+ if not want_unicode:
+ for arg in args:
+ if isinstance(arg, unicode):
+ want_unicode = True
+ break
+ if want_unicode:
+ newline = unicode("\n")
+ space = unicode(" ")
+ else:
+ newline = "\n"
+ space = " "
+ if sep is None:
+ sep = space
+ if end is None:
+ end = newline
+ for i, arg in enumerate(args):
+ if i:
+ write(sep)
+ write(arg)
+ write(end)
+if sys.version_info[:2] < (3, 3):
+ _print = print_
+
+ def print_(*args, **kwargs):
+ fp = kwargs.get("file", sys.stdout)
+ flush = kwargs.pop("flush", False)
+ _print(*args, **kwargs)
+ if flush and fp is not None:
+ fp.flush()
+
+_add_doc(reraise, """Reraise an exception.""")
+
+if sys.version_info[0:2] < (3, 4):
+ def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS,
+ updated=functools.WRAPPER_UPDATES):
+ def wrapper(f):
+ f = functools.wraps(wrapped, assigned, updated)(f)
+ f.__wrapped__ = wrapped
+ return f
+ return wrapper
+else:
+ wraps = functools.wraps
+
+
+def with_metaclass(meta, *bases):
+ """Create a base class with a metaclass."""
+ # This requires a bit of explanation: the basic idea is to make a dummy
+ # metaclass for one level of class instantiation that replaces itself with
+ # the actual metaclass.
+ class metaclass(meta):
+
+ def __new__(cls, name, this_bases, d):
+ return meta(name, bases, d)
+ return type.__new__(metaclass, 'temporary_class', (), {})
+
+
+def add_metaclass(metaclass):
+ """Class decorator for creating a class with a metaclass."""
+ def wrapper(cls):
+ orig_vars = cls.__dict__.copy()
+ slots = orig_vars.get('__slots__')
+ if slots is not None:
+ if isinstance(slots, str):
+ slots = [slots]
+ for slots_var in slots:
+ orig_vars.pop(slots_var)
+ orig_vars.pop('__dict__', None)
+ orig_vars.pop('__weakref__', None)
+ return metaclass(cls.__name__, cls.__bases__, orig_vars)
+ return wrapper
+
+
+def python_2_unicode_compatible(klass):
+ """
+ A decorator that defines __unicode__ and __str__ methods under Python 2.
+ Under Python 3 it does nothing.
+
+ To support Python 2 and 3 with a single code base, define a __str__ method
+ returning text and apply this decorator to the class.
+ """
+ if PY2:
+ if '__str__' not in klass.__dict__:
+ raise ValueError("@python_2_unicode_compatible cannot be applied "
+ "to %s because it doesn't define __str__()." %
+ klass.__name__)
+ klass.__unicode__ = klass.__str__
+ klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
+ return klass
+
+
+# Complete the moves implementation.
+# This code is at the end of this module to speed up module loading.
+# Turn this module into a package.
+__path__ = [] # required for PEP 302 and PEP 451
+__package__ = __name__ # see PEP 366 @ReservedAssignment
+if globals().get("__spec__") is not None:
+ __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable
+# Remove other six meta path importers, since they cause problems. This can
+# happen if six is removed from sys.modules and then reloaded. (Setuptools does
+# this for some reason.)
+if sys.meta_path:
+ for i, importer in enumerate(sys.meta_path):
+ # Here's some real nastiness: Another "instance" of the six module might
+ # be floating around. Therefore, we can't use isinstance() to check for
+ # the six meta path importer, since the other six instance will have
+ # inserted an importer with different class.
+ if (type(importer).__name__ == "_SixMetaPathImporter" and
+ importer.name == __name__):
+ del sys.meta_path[i]
+ break
+ del i, importer
+# Finally, add the importer to the meta path import hook.
+sys.meta_path.append(_importer)
diff --git a/pype/tools/pyblish_pype/version.py b/pype/tools/pyblish_pype/version.py
new file mode 100644
index 0000000000..5f1dce8011
--- /dev/null
+++ b/pype/tools/pyblish_pype/version.py
@@ -0,0 +1,11 @@
+
+VERSION_MAJOR = 2
+VERSION_MINOR = 9
+VERSION_PATCH = 0
+
+
+version_info = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH)
+version = '%i.%i.%i' % version_info
+__version__ = version
+
+__all__ = ['version', 'version_info', '__version__']
diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py
new file mode 100644
index 0000000000..86cefd4a55
--- /dev/null
+++ b/pype/tools/pyblish_pype/view.py
@@ -0,0 +1,212 @@
+from .vendor.Qt import QtCore, QtWidgets
+from . import model
+from .constants import Roles
+
+
+class ArtistView(QtWidgets.QListView):
+ # An item is requesting to be toggled, with optional forced-state
+ toggled = QtCore.Signal(QtCore.QModelIndex, object)
+ show_perspective = QtCore.Signal(QtCore.QModelIndex)
+
+ def __init__(self, parent=None):
+ super(ArtistView, self).__init__(parent)
+
+ self.horizontalScrollBar().hide()
+ self.viewport().setAttribute(QtCore.Qt.WA_Hover, True)
+ self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.setResizeMode(QtWidgets.QListView.Adjust)
+ self.setVerticalScrollMode(QtWidgets.QListView.ScrollPerPixel)
+
+ def event(self, event):
+ if not event.type() == QtCore.QEvent.KeyPress:
+ return super(ArtistView, self).event(event)
+
+ elif event.key() == QtCore.Qt.Key_Space:
+ for index in self.selectionModel().selectedIndexes():
+ self.toggled.emit(index, None)
+
+ return True
+
+ elif event.key() == QtCore.Qt.Key_Backspace:
+ for index in self.selectionModel().selectedIndexes():
+ self.toggled.emit(index, False)
+
+ return True
+
+ elif event.key() == QtCore.Qt.Key_Return:
+ for index in self.selectionModel().selectedIndexes():
+ self.toggled.emit(index, True)
+
+ return True
+
+ return super(ArtistView, self).event(event)
+
+ def focusOutEvent(self, event):
+ self.selectionModel().clear()
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == QtCore.Qt.LeftButton:
+ indexes = self.selectionModel().selectedIndexes()
+ if len(indexes) <= 1 and event.pos().x() < 20:
+ for index in indexes:
+ self.toggled.emit(index, None)
+ if len(indexes) == 1 and event.pos().x() > self.width() - 40:
+ for index in indexes:
+ self.show_perspective.emit(index)
+
+ return super(ArtistView, self).mouseReleaseEvent(event)
+
+
+class OverviewView(QtWidgets.QTreeView):
+ # An item is requesting to be toggled, with optional forced-state
+ toggled = QtCore.Signal(QtCore.QModelIndex, object)
+ show_perspective = QtCore.Signal(QtCore.QModelIndex)
+
+ def __init__(self, parent=None):
+ super(OverviewView, self).__init__(parent)
+
+ self.horizontalScrollBar().hide()
+ self.viewport().setAttribute(QtCore.Qt.WA_Hover, True)
+ self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.setItemsExpandable(True)
+ self.setVerticalScrollMode(QtWidgets.QTreeView.ScrollPerPixel)
+ self.setHeaderHidden(True)
+ self.setRootIsDecorated(False)
+ self.setIndentation(0)
+
+ self.clicked.connect(self.item_expand)
+
+ def event(self, event):
+ if not event.type() == QtCore.QEvent.KeyPress:
+ return super(OverviewView, self).event(event)
+
+ elif event.key() == QtCore.Qt.Key_Space:
+ for index in self.selectionModel().selectedIndexes():
+ self.toggled.emit(index, None)
+
+ return True
+
+ elif event.key() == QtCore.Qt.Key_Backspace:
+ for index in self.selectionModel().selectedIndexes():
+ self.toggled.emit(index, False)
+
+ return True
+
+ elif event.key() == QtCore.Qt.Key_Return:
+ for index in self.selectionModel().selectedIndexes():
+ self.toggled.emit(index, True)
+
+ return True
+
+ return super(OverviewView, self).event(event)
+
+ def focusOutEvent(self, event):
+ self.selectionModel().clear()
+
+ def item_expand(self, index):
+ if index.data(Roles.TypeRole) == model.GroupType:
+ if self.isExpanded(index):
+ self.collapse(index)
+ else:
+ self.expand(index)
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == QtCore.Qt.LeftButton:
+ indexes = self.selectionModel().selectedIndexes()
+ if len(indexes) == 1:
+ index = indexes[0]
+ # If instance or Plugin
+ if index.data(Roles.TypeRole) in (
+ model.InstanceType, model.PluginType
+ ):
+ if event.pos().x() < 20:
+ self.toggled.emit(index, None)
+ elif event.pos().x() > self.width() - 20:
+ self.show_perspective.emit(index)
+
+ # Deselect all group labels
+ for index in indexes:
+ if index.data(Roles.TypeRole) == model.GroupType:
+ self.selectionModel().select(
+ index, QtCore.QItemSelectionModel.Deselect
+ )
+
+ return super(OverviewView, self).mouseReleaseEvent(event)
+
+
+class TerminalView(QtWidgets.QTreeView):
+ # An item is requesting to be toggled, with optional forced-state
+ def __init__(self, parent=None):
+ super(TerminalView, self).__init__(parent)
+ self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+ self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.setAutoScroll(False)
+ self.setHeaderHidden(True)
+ self.setIndentation(0)
+ self.setVerticalScrollMode(QtWidgets.QTreeView.ScrollPerPixel)
+ self.verticalScrollBar().setSingleStep(10)
+ self.setRootIsDecorated(False)
+
+ self.clicked.connect(self.item_expand)
+
+ def event(self, event):
+ if not event.type() == QtCore.QEvent.KeyPress:
+ return super(TerminalView, self).event(event)
+
+ elif event.key() == QtCore.Qt.Key_Space:
+ for index in self.selectionModel().selectedIndexes():
+ if self.isExpanded(index):
+ self.collapse(index)
+ else:
+ self.expand(index)
+
+ elif event.key() == QtCore.Qt.Key_Backspace:
+ for index in self.selectionModel().selectedIndexes():
+ self.collapse(index)
+
+ elif event.key() == QtCore.Qt.Key_Return:
+ for index in self.selectionModel().selectedIndexes():
+ self.expand(index)
+
+ return super(TerminalView, self).event(event)
+
+ def focusOutEvent(self, event):
+ self.selectionModel().clear()
+
+ def item_expand(self, index):
+ if index.data(Roles.TypeRole) == model.TerminalLabelType:
+ if self.isExpanded(index):
+ self.collapse(index)
+ else:
+ self.expand(index)
+ self.model().layoutChanged.emit()
+ self.updateGeometry()
+
+ def rowsInserted(self, parent, start, end):
+ """Automatically scroll to bottom on each new item added."""
+ super(TerminalView, self).rowsInserted(parent, start, end)
+ self.updateGeometry()
+ self.scrollToBottom()
+
+ def resizeEvent(self, event):
+ super(self.__class__, self).resizeEvent(event)
+ self.model().layoutChanged.emit()
+
+ def sizeHint(self):
+ size = super(TerminalView, self).sizeHint()
+ height = (
+ self.contentsMargins().top()
+ + self.contentsMargins().bottom()
+ )
+ for idx_i in range(self.model().rowCount()):
+ index = self.model().index(idx_i, 0)
+ height += self.rowHeight(index)
+ if self.isExpanded(index):
+ for idx_j in range(index.model().rowCount(index)):
+ child_index = index.child(idx_j, 0)
+ height += self.rowHeight(child_index)
+
+ size.setHeight(height)
+ return size
diff --git a/pype/tools/pyblish_pype/widgets.py b/pype/tools/pyblish_pype/widgets.py
new file mode 100644
index 0000000000..3a09249a86
--- /dev/null
+++ b/pype/tools/pyblish_pype/widgets.py
@@ -0,0 +1,558 @@
+import sys
+from .vendor.Qt import QtCore, QtWidgets, QtGui
+from . import model, delegate, view, awesome
+from .constants import PluginStates, InstanceStates, Roles
+
+
+class EllidableLabel(QtWidgets.QLabel):
+ def __init__(self, *args, **kwargs):
+ super(self.__class__, self).__init__(*args, **kwargs)
+ self.setObjectName("EllidableLabel")
+
+ def paintEvent(self, event):
+ painter = QtGui.QPainter(self)
+
+ metrics = QtGui.QFontMetrics(self.font())
+ elided = metrics.elidedText(
+ self.text(), QtCore.Qt.ElideRight, self.width()
+ )
+ painter.drawText(self.rect(), self.alignment(), elided)
+
+
+class PerspectiveLabel(QtWidgets.QTextEdit):
+ def __init__(self, parent=None):
+ super(self.__class__, self).__init__(parent)
+ self.setObjectName("PerspectiveLabel")
+
+ size_policy = self.sizePolicy()
+ size_policy.setHeightForWidth(True)
+ size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Preferred)
+ self.setSizePolicy(size_policy)
+
+ self.textChanged.connect(self.on_text_changed)
+
+ def on_text_changed(self, *args, **kwargs):
+ self.updateGeometry()
+
+ def hasHeightForWidth(self):
+ return True
+
+ def heightForWidth(self, width):
+ margins = self.contentsMargins()
+
+ document_width = 0
+ if width >= margins.left() + margins.right():
+ document_width = width - margins.left() - margins.right()
+
+ document = self.document().clone()
+ document.setTextWidth(document_width)
+
+ return margins.top() + document.size().height() + margins.bottom()
+
+ def sizeHint(self):
+ width = super(self.__class__, self).sizeHint().width()
+ return QtCore.QSize(width, self.heightForWidth(width))
+
+
+class PerspectiveWidget(QtWidgets.QWidget):
+ l_doc = "Documentation"
+ l_rec = "Records"
+ l_path = "Path"
+
+ def __init__(self, parent):
+ super(PerspectiveWidget, self).__init__(parent)
+
+ self.parent_widget = parent
+ main_layout = QtWidgets.QVBoxLayout(self)
+
+ header_widget = QtWidgets.QWidget()
+ toggle_button = QtWidgets.QPushButton(parent=header_widget)
+ toggle_button.setObjectName("PerspectiveToggleBtn")
+ toggle_button.setText(delegate.icons["angle-left"])
+ toggle_button.setMinimumHeight(50)
+ toggle_button.setFixedWidth(40)
+
+ indicator = QtWidgets.QLabel("", parent=header_widget)
+ indicator.setFixedWidth(30)
+ indicator.setAlignment(QtCore.Qt.AlignCenter)
+ indicator.setObjectName("PerspectiveIndicator")
+
+ name = EllidableLabel('*Name of inspected', parent=header_widget)
+
+ header_layout = QtWidgets.QHBoxLayout(header_widget)
+ header_layout.setAlignment(QtCore.Qt.AlignLeft)
+ header_layout.addWidget(toggle_button)
+ header_layout.addWidget(indicator)
+ header_layout.addWidget(name)
+ header_layout.setContentsMargins(0, 0, 0, 0)
+ header_layout.setSpacing(10)
+ header_widget.setLayout(header_layout)
+
+ main_layout.setAlignment(QtCore.Qt.AlignTop)
+ main_layout.addWidget(header_widget)
+
+ scroll_widget = QtWidgets.QScrollArea(self)
+ scroll_widget.setObjectName("PerspectiveScrollContent")
+
+ contents_widget = QtWidgets.QWidget(scroll_widget)
+ contents_widget.setObjectName("PerspectiveWidgetContent")
+
+ layout = QtWidgets.QVBoxLayout()
+ layout.setAlignment(QtCore.Qt.AlignTop)
+ layout.setContentsMargins(0, 0, 0, 0)
+
+ documentation = ExpandableWidget(self, self.l_doc)
+ doc_label = PerspectiveLabel()
+ documentation.set_content(doc_label)
+ layout.addWidget(documentation)
+
+ path = ExpandableWidget(self, self.l_path)
+ path_label = PerspectiveLabel()
+ path.set_content(path_label)
+ layout.addWidget(path)
+
+ records = ExpandableWidget(self, self.l_rec)
+ layout.addWidget(records)
+
+ contents_widget.setLayout(layout)
+
+ terminal_view = view.TerminalView()
+ terminal_view.setObjectName("TerminalView")
+ terminal_model = model.TerminalModel()
+ terminal_proxy = model.TerminalProxy(terminal_view)
+ terminal_proxy.setSourceModel(terminal_model)
+
+ terminal_view.setModel(terminal_proxy)
+ terminal_delegate = delegate.TerminalItem()
+ terminal_view.setItemDelegate(terminal_delegate)
+ records.set_content(terminal_view)
+
+ scroll_widget.setWidgetResizable(True)
+ scroll_widget.setWidget(contents_widget)
+
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.setSpacing(0)
+ main_layout.addWidget(scroll_widget)
+ self.setLayout(main_layout)
+
+ self.terminal_view = terminal_view
+ self.terminal_model = terminal_model
+ self.terminal_proxy = terminal_proxy
+
+ self.indicator = indicator
+ self.scroll_widget = scroll_widget
+ self.contents_widget = contents_widget
+ self.toggle_button = toggle_button
+ self.name_widget = name
+ self.documentation = documentation
+ self.path = path
+ self.records = records
+
+ self.toggle_button.clicked.connect(self.toggle_me)
+
+ self.last_type = None
+ self.last_item_id = None
+ self.last_id = None
+
+ def trim(self, docstring):
+ if not docstring:
+ return ""
+ # Convert tabs to spaces (following the normal Python rules)
+ # and split into a list of lines:
+ lines = docstring.expandtabs().splitlines()
+ # Determine minimum indentation (first line doesn't count):
+ try:
+ indent = sys.maxint
+ max = sys.maxint
+ except Exception:
+ indent = sys.maxsize
+ max = sys.maxsize
+
+ for line in lines[1:]:
+ stripped = line.lstrip()
+ if stripped:
+ indent = min(indent, len(line) - len(stripped))
+ # Remove indentation (first line is special):
+ trimmed = [lines[0].strip()]
+ if indent < max:
+ for line in lines[1:]:
+ trimmed.append(line[indent:].rstrip())
+ # Strip off trailing and leading blank lines:
+ while trimmed and not trimmed[-1]:
+ trimmed.pop()
+ while trimmed and not trimmed[0]:
+ trimmed.pop(0)
+ # Return a single string:
+ return "\n".join(trimmed)
+
+ def set_indicator_state(self, state):
+ self.indicator.setProperty("state", state)
+ self.indicator.style().polish(self.indicator)
+
+ def reset(self):
+ self.last_id = None
+ self.set_records(list())
+ self.set_indicator_state(None)
+
+ def update_context(self, plugin_item, instance_item):
+ if not self.last_item_id or not self.last_type:
+ return
+
+ if self.last_type == model.PluginType:
+ if not self.last_id:
+ _item_id = plugin_item.data(Roles.ObjectUIdRole)
+ if _item_id != self.last_item_id:
+ return
+ self.last_id = plugin_item.plugin.id
+
+ elif self.last_id != plugin_item.plugin.id:
+ return
+
+ self.set_context(plugin_item.index())
+ return
+
+ if self.last_type == model.InstanceType:
+ if not self.last_id:
+ _item_id = instance_item.data(Roles.ObjectUIdRole)
+ if _item_id != self.last_item_id:
+ return
+ self.last_id = instance_item.instance.id
+
+ elif self.last_id != instance_item.instance.id:
+ return
+
+ self.set_context(instance_item.index())
+ return
+
+ def set_context(self, index):
+ if not index or not index.isValid():
+ index_type = None
+ else:
+ index_type = index.data(Roles.TypeRole)
+
+ if index_type == model.InstanceType:
+ item_id = index.data(Roles.ObjectIdRole)
+ publish_states = index.data(Roles.PublishFlagsRole)
+ if publish_states & InstanceStates.ContextType:
+ type_indicator = "C"
+ else:
+ type_indicator = "I"
+
+ if publish_states & InstanceStates.InProgress:
+ self.set_indicator_state("active")
+
+ elif publish_states & InstanceStates.HasError:
+ self.set_indicator_state("error")
+
+ elif publish_states & InstanceStates.HasWarning:
+ self.set_indicator_state("warning")
+
+ elif publish_states & InstanceStates.HasFinished:
+ self.set_indicator_state("ok")
+ else:
+ self.set_indicator_state(None)
+
+ self.documentation.setVisible(False)
+ self.path.setVisible(False)
+
+ elif index_type == model.PluginType:
+ item_id = index.data(Roles.ObjectIdRole)
+ type_indicator = "P"
+
+ doc = index.data(Roles.DocstringRole)
+ doc_str = ""
+ if doc:
+ doc_str = self.trim(doc)
+
+ publish_states = index.data(Roles.PublishFlagsRole)
+ if publish_states & PluginStates.InProgress:
+ self.set_indicator_state("active")
+
+ elif publish_states & PluginStates.HasError:
+ self.set_indicator_state("error")
+
+ elif publish_states & PluginStates.HasWarning:
+ self.set_indicator_state("warning")
+
+ elif publish_states & PluginStates.WasProcessed:
+ self.set_indicator_state("ok")
+
+ else:
+ self.set_indicator_state(None)
+
+ self.documentation.toggle_content(bool(doc_str))
+ self.documentation.content.setText(doc_str)
+
+ path = index.data(Roles.PathModuleRole) or ""
+ self.path.toggle_content(path.strip() != "")
+ self.path.content.setText(path)
+
+ self.documentation.setVisible(True)
+ self.path.setVisible(True)
+
+ else:
+ self.last_type = None
+ self.last_id = None
+ self.indicator.setText("?")
+ self.set_indicator_state(None)
+ self.documentation.setVisible(False)
+ self.path.setVisible(False)
+ self.records.setVisible(False)
+ return
+
+ self.last_type = index_type
+ self.last_id = item_id
+ self.last_item_id = index.data(Roles.ObjectUIdRole)
+
+ self.indicator.setText(type_indicator)
+
+ label = index.data(QtCore.Qt.DisplayRole)
+ self.name_widget.setText(label)
+ self.records.setVisible(True)
+
+ records = index.data(Roles.LogRecordsRole) or []
+ self.set_records(records)
+
+ def set_records(self, records):
+ len_records = 0
+ if records:
+ len_records += len(records)
+
+ data = {"records": records}
+ self.terminal_model.reset()
+ self.terminal_model.update_with_result(data)
+ while not self.terminal_model.items_to_set_widget.empty():
+ item = self.terminal_model.items_to_set_widget.get()
+ widget = TerminalDetail(item.data(QtCore.Qt.DisplayRole))
+ index = self.terminal_proxy.mapFromSource(item.index())
+ self.terminal_view.setIndexWidget(index, widget)
+
+ self.records.button_toggle_text.setText(
+ "{} ({})".format(self.l_rec, len_records)
+ )
+ self.records.toggle_content(len_records > 0)
+
+ def toggle_me(self):
+ self.parent_widget.toggle_perspective_widget()
+
+
+class ClickableWidget(QtWidgets.QLabel):
+ clicked = QtCore.Signal()
+
+ def mouseReleaseEvent(self, event):
+ if event.button() == QtCore.Qt.LeftButton:
+ self.clicked.emit()
+ super(ClickableWidget, self).mouseReleaseEvent(event)
+
+
+class ExpandableWidget(QtWidgets.QWidget):
+
+ content = None
+
+ def __init__(self, parent, title):
+ super(ExpandableWidget, self).__init__(parent)
+
+ top_part = ClickableWidget(parent=self)
+ top_part.setObjectName("ExpandableHeader")
+
+ button_size = QtCore.QSize(5, 5)
+ button_toggle = QtWidgets.QToolButton(parent=top_part)
+ button_toggle.setIconSize(button_size)
+ button_toggle.setArrowType(QtCore.Qt.RightArrow)
+ button_toggle.setCheckable(True)
+ button_toggle.setChecked(False)
+
+ button_toggle_text = QtWidgets.QLabel(title, parent=top_part)
+
+ layout = QtWidgets.QHBoxLayout(top_part)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(5)
+ layout.addWidget(button_toggle)
+ layout.addWidget(button_toggle_text)
+ top_part.setLayout(layout)
+
+ main_layout = QtWidgets.QVBoxLayout(self)
+ main_layout.setContentsMargins(9, 9, 9, 0)
+
+ content = QtWidgets.QFrame(self)
+ content.setObjectName("ExpandableWidgetContent")
+ content.setVisible(False)
+
+ content_layout = QtWidgets.QVBoxLayout(content)
+
+ main_layout.addWidget(top_part)
+ main_layout.addWidget(content)
+ self.setLayout(main_layout)
+
+ self.setAttribute(QtCore.Qt.WA_StyledBackground)
+
+ self.top_part = top_part
+ self.button_toggle = button_toggle
+ self.button_toggle_text = button_toggle_text
+
+ self.content_widget = content
+ self.content_layout = content_layout
+
+ self.top_part.clicked.connect(self.top_part_clicked)
+ self.button_toggle.clicked.connect(self.toggle_content)
+
+ def top_part_clicked(self):
+ self.toggle_content(not self.button_toggle.isChecked())
+
+ def toggle_content(self, *args):
+ if len(args) > 0:
+ checked = args[0]
+ else:
+ checked = self.button_toggle.isChecked()
+ arrow_type = QtCore.Qt.RightArrow
+ if checked:
+ arrow_type = QtCore.Qt.DownArrow
+ self.button_toggle.setChecked(checked)
+ self.button_toggle.setArrowType(arrow_type)
+ self.content_widget.setVisible(checked)
+
+ def resizeEvent(self, event):
+ super(self.__class__, self).resizeEvent(event)
+ self.content.updateGeometry()
+
+ def set_content(self, in_widget):
+ if self.content:
+ self.content.hide()
+ self.content_layout.removeWidget(self.content)
+ self.content_layout.addWidget(in_widget)
+ self.content = in_widget
+
+
+class ButtonWithMenu(QtWidgets.QWidget):
+ def __init__(self, button_title, parent=None):
+ super(ButtonWithMenu, self).__init__(parent=parent)
+ self.setSizePolicy(QtWidgets.QSizePolicy(
+ QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum
+ ))
+
+ self.layout = QtWidgets.QHBoxLayout(self)
+ self.layout.setContentsMargins(0, 0, 0, 0)
+ self.layout.setSpacing(0)
+
+ self.menu = QtWidgets.QMenu()
+ # TODO move to stylesheets
+ self.menu.setStyleSheet("""
+ *{color: #fff; background-color: #555; border: 1px solid #222;}
+ ::item {background-color: transparent;padding: 5px;
+ padding-left: 10px;padding-right: 10px;}
+ ::item:selected {background-color: #666;}
+ """)
+
+ self.button = QtWidgets.QPushButton(button_title)
+ self.button.setObjectName("ButtonWithMenu")
+
+ self.layout.addWidget(self.button)
+
+ self.button.clicked.connect(self.btn_clicked)
+
+ def btn_clicked(self):
+ self.menu.popup(self.button.mapToGlobal(
+ QtCore.QPoint(0, self.button.height())
+ ))
+
+ def addItem(self, text, callback):
+ self.menu.addAction(text, callback)
+ self.button.setToolTip("Select to apply predefined presets")
+
+ def clearMenu(self):
+ self.menu.clear()
+ self.button.setToolTip("Presets not found")
+
+
+class CommentBox(QtWidgets.QLineEdit):
+
+ def __init__(self, placeholder_text, parent=None):
+ super(CommentBox, self).__init__(parent=parent)
+ self.placeholder = QtWidgets.QLabel(placeholder_text, self)
+ self.placeholder.move(2, 2)
+
+ def focusInEvent(self, event):
+ self.placeholder.setVisible(False)
+ return super(CommentBox, self).focusInEvent(event)
+
+ def focusOutEvent(self, event):
+ current_text = self.text()
+ current_text = current_text.strip(" ")
+ self.setText(current_text)
+ if not self.text():
+ self.placeholder.setVisible(True)
+ return super(CommentBox, self).focusOutEvent(event)
+
+
+class TerminalDetail(QtWidgets.QTextEdit):
+ def __init__(self, text, *args, **kwargs):
+ super(self.__class__, self).__init__(*args, **kwargs)
+
+ self.setReadOnly(True)
+ self.setHtml(text)
+ self.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
+ self.setWordWrapMode(
+ QtGui.QTextOption.WrapAtWordBoundaryOrAnywhere
+ )
+
+ def sizeHint(self):
+ content_margins = (
+ self.contentsMargins().top()
+ + self.contentsMargins().bottom()
+ )
+ size = self.document().documentLayout().documentSize().toSize()
+ size.setHeight(size.height() + content_margins)
+ return size
+
+
+class FilterButton(QtWidgets.QPushButton):
+ def __init__(self, name, *args, **kwargs):
+ self.filter_name = name
+
+ super(self.__class__, self).__init__(*args, **kwargs)
+
+ self.toggled.connect(self.on_toggle)
+
+ self.setProperty("type", name)
+ self.setObjectName("TerminalFilerBtn")
+ self.setCheckable(True)
+ self.setChecked(
+ model.TerminalProxy.filter_buttons_checks[name]
+ )
+
+ def on_toggle(self, toggle_state):
+ model.TerminalProxy.change_filter(self.filter_name, toggle_state)
+
+
+class TerminalFilterWidget(QtWidgets.QWidget):
+ # timer.timeout.connect(lambda: self._update(self.parent_widget))
+ def __init__(self, *args, **kwargs):
+ super(self.__class__, self).__init__(*args, **kwargs)
+
+ self.filter_changed = QtCore.Signal()
+
+ info_icon = awesome.tags["info"]
+ log_icon = awesome.tags["circle"]
+ error_icon = awesome.tags["exclamation-triangle"]
+
+ filter_buttons = (
+ FilterButton("info", info_icon),
+ FilterButton("log_debug", log_icon),
+ FilterButton("log_info", log_icon),
+ FilterButton("log_warning", log_icon),
+ FilterButton("log_error", log_icon),
+ FilterButton("log_critical", log_icon),
+ FilterButton("error", error_icon)
+ )
+
+ layout = QtWidgets.QHBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ # Add spacers
+ layout.addWidget(QtWidgets.QWidget(), 1)
+
+ for btn in filter_buttons:
+ layout.addWidget(btn)
+
+ self.setLayout(layout)
+
+ self.filter_buttons = filter_buttons
diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py
new file mode 100644
index 0000000000..84003a88aa
--- /dev/null
+++ b/pype/tools/pyblish_pype/window.py
@@ -0,0 +1,1055 @@
+"""Main Window
+
+States:
+ These are all possible states and their transitions.
+
+
+ reset
+ '
+ '
+ '
+ ___v__
+ | | reset
+ | Idle |--------------------.
+ | |<-------------------'
+ | |
+ | | _____________
+ | | validate | | reset # TODO
+ | |----------------->| In-progress |-----------.
+ | | |_____________| '
+ | |<-------------------------------------------'
+ | |
+ | | _____________
+ | | publish | |
+ | |----------------->| In-progress |---.
+ | | |_____________| '
+ | |<-----------------------------------'
+ |______|
+
+
+Todo:
+ There are notes spread throughout this project with the syntax:
+
+ - TODO(username)
+
+ The `username` is a quick and dirty indicator of who made the note
+ and is by no means exclusive to that person in terms of seeing it
+ done. Feel free to do, or make your own TODO's as you code. Just
+ make sure the description is sufficient for anyone reading it for
+ the first time to understand how to actually to it!
+
+"""
+from functools import partial
+
+from . import delegate, model, settings, util, view, widgets
+from .awesome import tags as awesome
+
+from .vendor.Qt import QtCore, QtGui, QtWidgets
+from .constants import (
+ PluginStates, PluginActionStates, InstanceStates, GroupStates, Roles
+)
+
+
+class Window(QtWidgets.QDialog):
+ def __init__(self, controller, parent=None):
+ super(Window, self).__init__(parent=parent)
+
+ # Use plastique style for specific ocations
+ # TODO set style name via environment variable
+ low_keys = {
+ key.lower(): key
+ for key in QtWidgets.QStyleFactory.keys()
+ }
+ if "plastique" in low_keys:
+ self.setStyle(
+ QtWidgets.QStyleFactory.create(low_keys["plastique"])
+ )
+
+ icon = QtGui.QIcon(util.get_asset("img", "logo-extrasmall.png"))
+ if parent is None:
+ on_top_flag = QtCore.Qt.WindowStaysOnTopHint
+ else:
+ on_top_flag = QtCore.Qt.Dialog
+
+ self.setWindowFlags(
+ self.windowFlags()
+ | QtCore.Qt.WindowTitleHint
+ | QtCore.Qt.WindowMaximizeButtonHint
+ | QtCore.Qt.WindowMinimizeButtonHint
+ | QtCore.Qt.WindowCloseButtonHint
+ | on_top_flag
+ )
+ self.setWindowIcon(icon)
+ self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
+
+ self.controller = controller
+
+ main_widget = QtWidgets.QWidget(self)
+
+ # General layout
+ header_widget = QtWidgets.QWidget(parent=main_widget)
+
+ header_tab_widget = QtWidgets.QWidget(header_widget)
+ header_tab_artist = QtWidgets.QRadioButton(header_tab_widget)
+ header_tab_overview = QtWidgets.QRadioButton(header_tab_widget)
+ header_tab_terminal = QtWidgets.QRadioButton(header_tab_widget)
+ header_spacer = QtWidgets.QWidget(header_tab_widget)
+
+ header_aditional_btns = QtWidgets.QWidget(header_tab_widget)
+
+ aditional_btns_layout = QtWidgets.QHBoxLayout(header_aditional_btns)
+
+ presets_button = widgets.ButtonWithMenu(awesome["filter"])
+ presets_button.setEnabled(False)
+ aditional_btns_layout.addWidget(presets_button)
+
+ layout_tab = QtWidgets.QHBoxLayout(header_tab_widget)
+ layout_tab.setContentsMargins(0, 0, 0, 0)
+ layout_tab.setSpacing(0)
+ layout_tab.addWidget(header_tab_artist, 0)
+ layout_tab.addWidget(header_tab_overview, 0)
+ layout_tab.addWidget(header_tab_terminal, 0)
+ # Compress items to the left
+ layout_tab.addWidget(header_spacer, 1)
+ layout_tab.addWidget(header_aditional_btns, 1)
+
+ layout = QtWidgets.QHBoxLayout(header_widget)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ layout.addWidget(header_tab_widget)
+
+ header_widget.setLayout(layout)
+
+ # Artist Page
+ instance_model = model.InstanceModel(controller)
+
+ artist_page = QtWidgets.QWidget()
+
+ artist_view = view.ArtistView()
+ artist_view.show_perspective.connect(self.toggle_perspective_widget)
+ artist_proxy = model.ArtistProxy()
+ artist_proxy.setSourceModel(instance_model)
+ artist_view.setModel(artist_proxy)
+
+ artist_delegate = delegate.ArtistDelegate()
+ artist_view.setItemDelegate(artist_delegate)
+
+ layout = QtWidgets.QVBoxLayout(artist_page)
+ layout.addWidget(artist_view)
+ layout.setContentsMargins(5, 5, 5, 5)
+ layout.setSpacing(0)
+
+ artist_page.setLayout(layout)
+
+ # Overview Page
+ # TODO add parent
+ overview_page = QtWidgets.QWidget()
+
+ overview_instance_view = view.OverviewView(parent=overview_page)
+ overview_instance_delegate = delegate.InstanceDelegate(
+ parent=overview_instance_view
+ )
+ overview_instance_view.setItemDelegate(overview_instance_delegate)
+ overview_instance_view.setModel(instance_model)
+
+ overview_plugin_view = view.OverviewView(parent=overview_page)
+ overview_plugin_delegate = delegate.PluginDelegate(
+ parent=overview_plugin_view
+ )
+ overview_plugin_view.setItemDelegate(overview_plugin_delegate)
+ plugin_model = model.PluginModel(controller)
+ plugin_proxy = model.PluginFilterProxy()
+ plugin_proxy.setSourceModel(plugin_model)
+ overview_plugin_view.setModel(plugin_proxy)
+
+ layout = QtWidgets.QHBoxLayout(overview_page)
+ layout.addWidget(overview_instance_view, 1)
+ layout.addWidget(overview_plugin_view, 1)
+ layout.setContentsMargins(5, 5, 5, 5)
+ layout.setSpacing(0)
+ overview_page.setLayout(layout)
+
+ # Terminal
+ terminal_container = QtWidgets.QWidget()
+
+ terminal_view = view.TerminalView()
+ terminal_model = model.TerminalModel()
+ terminal_proxy = model.TerminalProxy(terminal_view)
+ terminal_proxy.setSourceModel(terminal_model)
+
+ terminal_view.setModel(terminal_proxy)
+ terminal_delegate = delegate.TerminalItem()
+ terminal_view.setItemDelegate(terminal_delegate)
+
+ layout = QtWidgets.QVBoxLayout(terminal_container)
+ layout.addWidget(terminal_view)
+ layout.setContentsMargins(5, 5, 5, 5)
+ layout.setSpacing(0)
+
+ terminal_container.setLayout(layout)
+
+ terminal_page = QtWidgets.QWidget()
+ layout = QtWidgets.QVBoxLayout(terminal_page)
+ layout.addWidget(terminal_container)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+
+ # Add some room between window borders and contents
+ body_widget = QtWidgets.QWidget(main_widget)
+ layout = QtWidgets.QHBoxLayout(body_widget)
+ layout.setContentsMargins(5, 5, 5, 1)
+ layout.addWidget(artist_page)
+ layout.addWidget(overview_page)
+ layout.addWidget(terminal_page)
+
+ # Comment Box
+ comment_box = widgets.CommentBox("Comment...", self)
+
+ intent_box = QtWidgets.QComboBox()
+
+ intent_model = model.IntentModel()
+ intent_box.setModel(intent_model)
+ intent_box.currentIndexChanged.connect(self.on_intent_changed)
+
+ comment_intent_widget = QtWidgets.QWidget()
+ comment_intent_layout = QtWidgets.QHBoxLayout(comment_intent_widget)
+ comment_intent_layout.setContentsMargins(0, 0, 0, 0)
+ comment_intent_layout.setSpacing(5)
+ comment_intent_layout.addWidget(comment_box)
+ comment_intent_layout.addWidget(intent_box)
+
+ # Terminal filtering
+ terminal_filters_widget = widgets.TerminalFilterWidget()
+
+ # Footer
+ footer_widget = QtWidgets.QWidget(main_widget)
+
+ footer_info = QtWidgets.QLabel(footer_widget)
+ footer_spacer = QtWidgets.QWidget(footer_widget)
+ footer_button_reset = QtWidgets.QPushButton(
+ awesome["refresh"], footer_widget
+ )
+ footer_button_validate = QtWidgets.QPushButton(
+ awesome["flask"], footer_widget
+ )
+ footer_button_play = QtWidgets.QPushButton(
+ awesome["play"], footer_widget
+ )
+ footer_button_stop = QtWidgets.QPushButton(
+ awesome["stop"], footer_widget
+ )
+
+ layout = QtWidgets.QHBoxLayout()
+ layout.setContentsMargins(5, 5, 5, 5)
+ layout.addWidget(footer_info, 0)
+ layout.addWidget(footer_spacer, 1)
+ layout.addWidget(footer_button_stop, 0)
+ layout.addWidget(footer_button_reset, 0)
+ layout.addWidget(footer_button_validate, 0)
+ layout.addWidget(footer_button_play, 0)
+
+ footer_layout = QtWidgets.QVBoxLayout(footer_widget)
+ footer_layout.addWidget(comment_intent_widget)
+ footer_layout.addLayout(layout)
+
+ footer_widget.setProperty("success", -1)
+
+ # Placeholder for when GUI is closing
+ # TODO(marcus): Fade to black and the the user about what's happening
+ closing_placeholder = QtWidgets.QWidget(main_widget)
+ closing_placeholder.setSizePolicy(
+ QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding
+ )
+ closing_placeholder.hide()
+
+ perspective_widget = widgets.PerspectiveWidget(self)
+ perspective_widget.hide()
+
+ # Main layout
+ layout = QtWidgets.QVBoxLayout(main_widget)
+ layout.addWidget(header_widget, 0)
+ layout.addWidget(body_widget, 3)
+ layout.addWidget(perspective_widget, 3)
+ layout.addWidget(closing_placeholder, 1)
+ layout.addWidget(terminal_filters_widget, 0)
+ layout.addWidget(footer_widget, 0)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ main_widget.setLayout(layout)
+
+ self.main_layout = QtWidgets.QVBoxLayout(self)
+ self.main_layout.setContentsMargins(0, 0, 0, 0)
+ self.main_layout.setSpacing(0)
+ self.main_layout.addWidget(main_widget)
+
+ # Display info
+ info_effect = QtWidgets.QGraphicsOpacityEffect(footer_info)
+ footer_info.setGraphicsEffect(info_effect)
+
+ on = QtCore.QPropertyAnimation(info_effect, b"opacity")
+ on.setDuration(0)
+ on.setStartValue(0)
+ on.setEndValue(1)
+
+ off = QtCore.QPropertyAnimation(info_effect, b"opacity")
+ off.setDuration(0)
+ off.setStartValue(1)
+ off.setEndValue(0)
+
+ fade = QtCore.QPropertyAnimation(info_effect, b"opacity")
+ fade.setDuration(500)
+ fade.setStartValue(1.0)
+ fade.setEndValue(0.0)
+
+ animation_info_msg = QtCore.QSequentialAnimationGroup()
+ animation_info_msg.addAnimation(on)
+ animation_info_msg.addPause(50)
+ animation_info_msg.addAnimation(off)
+ animation_info_msg.addPause(50)
+ animation_info_msg.addAnimation(on)
+ animation_info_msg.addPause(2000)
+ animation_info_msg.addAnimation(fade)
+
+ """Setup
+
+ Widgets are referred to in CSS via their object-name. We
+ use the same mechanism internally to refer to objects; so rather
+ than storing widgets as self.my_widget, it is referred to as:
+
+ >>> my_widget = self.findChild(QtWidgets.QWidget, "MyWidget")
+
+ This way there is only ever a single method of referring to any widget.
+ """
+
+ names = {
+ # Main
+ "Header": header_widget,
+ "Body": body_widget,
+ "Footer": footer_widget,
+
+ # Pages
+ "Artist": artist_page,
+ "Overview": overview_page,
+ "Terminal": terminal_page,
+
+ # Tabs
+ "ArtistTab": header_tab_artist,
+ "OverviewTab": header_tab_overview,
+ "TerminalTab": header_tab_terminal,
+
+ # Views
+ "TerminalView": terminal_view,
+
+ # Buttons
+ "Play": footer_button_play,
+ "Validate": footer_button_validate,
+ "Reset": footer_button_reset,
+ "Stop": footer_button_stop,
+
+ # Misc
+ "HeaderSpacer": header_spacer,
+ "FooterSpacer": footer_spacer,
+ "FooterInfo": footer_info,
+ "CommentIntentWidget": comment_intent_widget,
+ "CommentBox": comment_box,
+ "CommentPlaceholder": comment_box.placeholder,
+ "ClosingPlaceholder": closing_placeholder,
+ "IntentBox": intent_box
+ }
+
+ for name, _widget in names.items():
+ _widget.setObjectName(name)
+
+ # Enable CSS on plain QWidget objects
+ for _widget in (
+ header_widget,
+ body_widget,
+ artist_page,
+ comment_box,
+ overview_page,
+ terminal_page,
+ footer_widget,
+ footer_button_play,
+ footer_button_validate,
+ footer_button_stop,
+ footer_button_reset,
+ footer_spacer,
+ closing_placeholder
+ ):
+ _widget.setAttribute(QtCore.Qt.WA_StyledBackground)
+
+ # Signals
+ header_tab_artist.toggled.connect(
+ lambda: self.on_tab_changed("artist")
+ )
+ header_tab_overview.toggled.connect(
+ lambda: self.on_tab_changed("overview")
+ )
+ header_tab_terminal.toggled.connect(
+ lambda: self.on_tab_changed("terminal")
+ )
+
+ overview_instance_view.show_perspective.connect(
+ self.toggle_perspective_widget
+ )
+ overview_plugin_view.show_perspective.connect(
+ self.toggle_perspective_widget
+ )
+
+ controller.switch_toggleability.connect(self.change_toggleability)
+
+ controller.was_reset.connect(self.on_was_reset)
+ # This is called synchronously on each process
+ controller.was_processed.connect(self.on_was_processed)
+ controller.passed_group.connect(self.on_passed_group)
+ controller.was_stopped.connect(self.on_was_stopped)
+ controller.was_finished.connect(self.on_was_finished)
+
+ controller.was_skipped.connect(self.on_was_skipped)
+ controller.was_acted.connect(self.on_was_acted)
+
+ # NOTE: Listeners to this signal are run in the main thread
+ controller.about_to_process.connect(
+ self.on_about_to_process,
+ QtCore.Qt.DirectConnection
+ )
+
+ artist_view.toggled.connect(self.on_item_toggled)
+ overview_instance_view.toggled.connect(self.on_item_toggled)
+ overview_plugin_view.toggled.connect(self.on_item_toggled)
+
+ footer_button_stop.clicked.connect(self.on_stop_clicked)
+ footer_button_reset.clicked.connect(self.on_reset_clicked)
+ footer_button_validate.clicked.connect(self.on_validate_clicked)
+ footer_button_play.clicked.connect(self.on_play_clicked)
+
+ comment_box.textChanged.connect(self.on_comment_entered)
+ comment_box.returnPressed.connect(self.on_play_clicked)
+ overview_plugin_view.customContextMenuRequested.connect(
+ self.on_plugin_action_menu_requested
+ )
+
+ instance_model.group_created.connect(
+ overview_instance_view.expand
+ )
+
+ self.main_widget = main_widget
+
+ self.header_widget = header_widget
+ self.body_widget = body_widget
+
+ self.terminal_filters_widget = terminal_filters_widget
+
+ self.footer_widget = footer_widget
+ self.footer_button_reset = footer_button_reset
+ self.footer_button_validate = footer_button_validate
+ self.footer_button_play = footer_button_play
+ self.footer_button_stop = footer_button_stop
+
+ self.overview_instance_view = overview_instance_view
+ self.overview_plugin_view = overview_plugin_view
+ self.plugin_model = plugin_model
+ self.plugin_proxy = plugin_proxy
+ self.instance_model = instance_model
+
+ self.artist_proxy = artist_proxy
+ self.artist_view = artist_view
+
+ self.presets_button = presets_button
+
+ self.animation_info_msg = animation_info_msg
+
+ self.terminal_model = terminal_model
+ self.terminal_proxy = terminal_proxy
+ self.terminal_view = terminal_view
+
+ self.comment_main_widget = comment_intent_widget
+ self.comment_box = comment_box
+ self.intent_box = intent_box
+ self.intent_model = intent_model
+
+ self.perspective_widget = perspective_widget
+
+ self.tabs = {
+ "artist": header_tab_artist,
+ "overview": header_tab_overview,
+ "terminal": header_tab_terminal
+ }
+ self.pages = {
+ "artist": artist_page,
+ "overview": overview_page,
+ "terminal": terminal_page
+ }
+
+ current_page = settings.InitialTab or "artist"
+ self.state = {
+ "is_closing": False,
+ "current_page": current_page
+ }
+
+ self.tabs[current_page].setChecked(True)
+
+ # -------------------------------------------------------------------------
+ #
+ # Event handlers
+ #
+ # -------------------------------------------------------------------------
+ def set_presets(self, key):
+ plugin_settings = self.controller.possible_presets.get(key)
+ if not plugin_settings:
+ return
+
+ for plugin_item in self.plugin_model.plugin_items.values():
+ if not plugin_item.plugin.optional:
+ continue
+
+ value = plugin_settings.get(
+ plugin_item.plugin.__name__,
+ # if plugin is not in presets then set default value
+ self.controller.optional_default.get(
+ plugin_item.plugin.__name__
+ )
+ )
+ if value is None:
+ continue
+
+ plugin_item.setData(value, QtCore.Qt.CheckStateRole)
+
+ def toggle_perspective_widget(self, index=None):
+ show = False
+ if index:
+ show = True
+ self.perspective_widget.set_context(index)
+
+ self.body_widget.setVisible(not show)
+ self.header_widget.setVisible(not show)
+
+ self.perspective_widget.setVisible(show)
+ self.terminal_filters_widget.setVisible(show)
+
+ def change_toggleability(self, enable_value):
+ for plugin_item in self.plugin_model.plugin_items.values():
+ plugin_item.setData(enable_value, Roles.IsEnabledRole)
+
+ for instance_item in (
+ self.instance_model.instance_items.values()
+ ):
+ instance_item.setData(enable_value, Roles.IsEnabledRole)
+
+ def on_item_toggled(self, index, state=None):
+ """An item is requesting to be toggled"""
+ if not index.data(Roles.IsOptionalRole):
+ return self.info("This item is mandatory")
+
+ if self.controller.collect_state != 1:
+ return self.info("Cannot toggle")
+
+ if state is None:
+ state = not index.data(QtCore.Qt.CheckStateRole)
+
+ index.model().setData(index, state, QtCore.Qt.CheckStateRole)
+ self.update_compatibility()
+
+ def on_tab_changed(self, target):
+ self.comment_main_widget.setVisible(not target == "terminal")
+ self.terminal_filters_widget.setVisible(target == "terminal")
+
+ for name, page in self.pages.items():
+ if name != target:
+ page.hide()
+
+ self.pages[target].show()
+
+ self.state["current_page"] = target
+
+ def on_validate_clicked(self):
+ self.comment_box.setEnabled(False)
+ self.intent_box.setEnabled(False)
+
+ self.validate()
+
+ def on_play_clicked(self):
+ self.comment_box.setEnabled(False)
+ self.intent_box.setEnabled(False)
+
+ self.publish()
+
+ def on_reset_clicked(self):
+ self.reset()
+
+ def on_stop_clicked(self):
+ self.info("Stopping..")
+ self.controller.stop()
+
+ # TODO checks
+ self.footer_button_reset.setEnabled(True)
+ self.footer_button_play.setEnabled(False)
+ self.footer_button_stop.setEnabled(False)
+
+ def on_comment_entered(self):
+ """The user has typed a comment."""
+ self.controller.context.data["comment"] = self.comment_box.text()
+
+ def on_intent_changed(self):
+ idx = self.intent_model.index(self.intent_box.currentIndex(), 0)
+ intent_value = self.intent_model.data(idx, Roles.IntentItemValue)
+ intent_label = self.intent_model.data(idx, QtCore.Qt.DisplayRole)
+
+ # TODO move to play
+ if self.controller.context:
+ self.controller.context.data["intent"] = {
+ "value": intent_value,
+ "label": intent_label
+ }
+
+ def on_about_to_process(self, plugin, instance):
+ """Reflect currently running pair in GUI"""
+ if instance is None:
+ instance_id = self.controller.context.id
+ else:
+ instance_id = instance.id
+
+ instance_item = (
+ self.instance_model.instance_items[instance_id]
+ )
+ instance_item.setData(
+ {InstanceStates.InProgress: True},
+ Roles.PublishFlagsRole
+ )
+
+ plugin_item = self.plugin_model.plugin_items[plugin._id]
+ plugin_item.setData(
+ {PluginStates.InProgress: True},
+ Roles.PublishFlagsRole
+ )
+
+ self.info("{} {}".format(
+ self.tr("Processing"), plugin_item.data(QtCore.Qt.DisplayRole)
+ ))
+
+ def on_plugin_action_menu_requested(self, pos):
+ """The user right-clicked on a plug-in
+ __________
+ | |
+ | Action 1 |
+ | Action 2 |
+ | Action 3 |
+ | |
+ |__________|
+
+ """
+
+ index = self.overview_plugin_view.indexAt(pos)
+ actions = index.data(Roles.PluginValidActionsRole)
+
+ if not actions:
+ return
+
+ menu = QtWidgets.QMenu(self)
+ plugin_id = index.data(Roles.ObjectIdRole)
+ plugin_item = self.plugin_model.plugin_items[plugin_id]
+ print("plugin is: %s" % plugin_item.plugin)
+
+ for action in actions:
+ qaction = QtWidgets.QAction(action.label or action.__name__, self)
+ qaction.triggered.connect(partial(self.act, plugin_item, action))
+ menu.addAction(qaction)
+
+ menu.popup(self.overview_plugin_view.viewport().mapToGlobal(pos))
+
+ def update_compatibility(self):
+ self.plugin_model.update_compatibility()
+ self.plugin_proxy.invalidateFilter()
+
+ def on_was_reset(self):
+ # Append context object to instances model
+ self.instance_model.append(self.controller.context)
+
+ for plugin in self.controller.plugins:
+ self.plugin_model.append(plugin)
+
+ self.overview_instance_view.expandAll()
+ self.overview_plugin_view.expandAll()
+
+ self.presets_button.clearMenu()
+ if self.controller.possible_presets:
+ self.presets_button.setEnabled(True)
+ for key in self.controller.possible_presets:
+ self.presets_button.addItem(
+ key, partial(self.set_presets, key)
+ )
+
+ self.instance_model.restore_checkstates()
+ self.plugin_model.restore_checkstates()
+
+ self.perspective_widget.reset()
+
+ # Append placeholder comment from Context
+ # This allows users to inject a comment from elsewhere,
+ # or to perhaps provide a placeholder comment/template
+ # for artists to fill in.
+ comment = self.controller.context.data.get("comment")
+ self.comment_box.setText(comment or None)
+ self.comment_box.setEnabled(True)
+
+ if self.intent_model.has_items:
+ self.on_intent_changed()
+ self.intent_box.setEnabled(True)
+
+ # Refresh tab
+ self.on_tab_changed(self.state["current_page"])
+ self.update_compatibility()
+
+ self.footer_button_validate.setEnabled(True)
+ self.footer_button_reset.setEnabled(True)
+ self.footer_button_stop.setEnabled(False)
+ self.footer_button_play.setEnabled(True)
+ self.footer_button_play.setFocus()
+
+ def on_passed_group(self, order):
+
+ for group_item in self.instance_model.group_items.values():
+ if self.overview_instance_view.isExpanded(group_item.index()):
+ continue
+
+ if group_item.publish_states & GroupStates.HasError:
+ self.overview_instance_view.expand(group_item.index())
+
+ for group_item in self.plugin_model.group_items.values():
+ # TODO check only plugins from the group
+ if (
+ group_item.publish_states & GroupStates.HasFinished
+ or (order is not None and group_item.order >= order)
+ ):
+ continue
+
+ if group_item.publish_states & GroupStates.HasError:
+ self.overview_plugin_view.expand(
+ self.plugin_proxy.mapFromSource(group_item.index())
+ )
+ continue
+
+ group_item.setData(
+ {GroupStates.HasFinished: True},
+ Roles.PublishFlagsRole
+ )
+ self.overview_plugin_view.collapse(
+ self.plugin_proxy.mapFromSource(group_item.index())
+ )
+
+ def on_was_stopped(self):
+ errored = self.controller.errored
+ self.footer_button_play.setEnabled(not errored)
+ self.footer_button_validate.setEnabled(
+ not errored and not self.controller.validated
+ )
+ self.footer_button_reset.setEnabled(True)
+ self.footer_button_stop.setEnabled(False)
+ if errored:
+ self.footer_widget.setProperty("success", 0)
+ self.footer_widget.style().polish(self.footer_widget)
+
+ def on_was_skipped(self, plugin):
+ plugin_item = self.plugin_model.plugin_items[plugin.id]
+ plugin_item.setData(
+ {PluginStates.WasSkipped: True},
+ Roles.PublishFlagsRole
+ )
+
+ def on_was_finished(self):
+ self.footer_button_play.setEnabled(False)
+ self.footer_button_validate.setEnabled(False)
+ self.footer_button_reset.setEnabled(True)
+ self.footer_button_stop.setEnabled(False)
+
+ if self.controller.errored:
+ success_val = 0
+ self.info(self.tr("Stopped due to error(s), see Terminal."))
+ self.comment_box.setEnabled(False)
+ self.intent_box.setEnabled(False)
+
+ else:
+ success_val = 1
+ self.info(self.tr("Finished successfully!"))
+
+ self.footer_widget.setProperty("success", success_val)
+ self.footer_widget.style().polish(self.footer_widget)
+
+ for instance_item in (
+ self.instance_model.instance_items.values()
+ ):
+ instance_item.setData(
+ {InstanceStates.HasFinished: True},
+ Roles.PublishFlagsRole
+ )
+
+ for group_item in self.instance_model.group_items.values():
+ group_item.setData(
+ {GroupStates.HasFinished: True},
+ Roles.PublishFlagsRole
+ )
+
+ self.update_compatibility()
+
+ def on_was_processed(self, result):
+ existing_ids = set(self.instance_model.instance_items.keys())
+ existing_ids.remove(self.controller.context.id)
+ for instance in self.controller.context:
+ if instance.id not in existing_ids:
+ self.instance_model.append(instance)
+ else:
+ existing_ids.remove(instance.id)
+
+ for instance_id in existing_ids:
+ self.instance_model.remove(instance_id)
+
+ if result.get("error"):
+ # Toggle from artist to overview tab on error
+ if self.tabs["artist"].isChecked():
+ self.tabs["overview"].toggle()
+
+ result["records"] = self.terminal_model.prepare_records(result)
+
+ plugin_item = self.plugin_model.update_with_result(result)
+ instance_item = self.instance_model.update_with_result(result)
+
+ self.terminal_model.update_with_result(result)
+ while not self.terminal_model.items_to_set_widget.empty():
+ item = self.terminal_model.items_to_set_widget.get()
+ widget = widgets.TerminalDetail(item.data(QtCore.Qt.DisplayRole))
+ index = self.terminal_proxy.mapFromSource(item.index())
+ self.terminal_view.setIndexWidget(index, widget)
+
+ self.update_compatibility()
+
+ if self.perspective_widget.isVisible():
+ self.perspective_widget.update_context(
+ plugin_item, instance_item
+ )
+
+ # -------------------------------------------------------------------------
+ #
+ # Functions
+ #
+ # -------------------------------------------------------------------------
+
+ def reset(self):
+ """Prepare GUI for reset"""
+ self.info(self.tr("About to reset.."))
+
+ self.presets_button.setEnabled(False)
+ self.footer_widget.setProperty("success", -1)
+ self.footer_widget.style().polish(self.footer_widget)
+
+ self.instance_model.store_checkstates()
+ self.plugin_model.store_checkstates()
+
+ # Reset current ids to secure no previous instances get mixed in.
+ self.instance_model.reset()
+ self.plugin_model.reset()
+ self.intent_model.reset()
+ self.terminal_model.reset()
+
+ self.footer_button_stop.setEnabled(False)
+ self.footer_button_reset.setEnabled(False)
+ self.footer_button_validate.setEnabled(False)
+ self.footer_button_play.setEnabled(False)
+
+ self.intent_box.setVisible(self.intent_model.has_items)
+ if self.intent_model.has_items:
+ self.intent_box.setCurrentIndex(self.intent_model.default_index)
+
+ self.comment_box.placeholder.setVisible(False)
+ self.comment_box.placeholder.setVisible(True)
+ # Launch controller reset
+ util.defer(500, self.controller.reset)
+
+ def validate(self):
+ self.info(self.tr("Preparing validate.."))
+ self.footer_button_stop.setEnabled(True)
+ self.footer_button_reset.setEnabled(False)
+ self.footer_button_validate.setEnabled(False)
+ self.footer_button_play.setEnabled(False)
+
+ util.defer(5, self.controller.validate)
+
+ def publish(self):
+ self.info(self.tr("Preparing publish.."))
+
+ self.footer_button_stop.setEnabled(True)
+ self.footer_button_reset.setEnabled(False)
+ self.footer_button_validate.setEnabled(False)
+ self.footer_button_play.setEnabled(False)
+
+ util.defer(5, self.controller.publish)
+
+ def act(self, plugin_item, action):
+ self.info("%s %s.." % (self.tr("Preparing"), action))
+
+ self.footer_button_stop.setEnabled(True)
+ self.footer_button_reset.setEnabled(False)
+ self.footer_button_validate.setEnabled(False)
+ self.footer_button_play.setEnabled(False)
+
+ # Cause view to update, but it won't visually
+ # happen until Qt is given time to idle..
+ plugin_item.setData(
+ PluginActionStates.InProgress, Roles.PluginActionProgressRole
+ )
+
+ # Give Qt time to draw
+ util.defer(100, lambda: self.controller.act(
+ plugin_item.plugin, action
+ ))
+
+ self.info(self.tr("Action prepared."))
+
+ def on_was_acted(self, result):
+ self.footer_button_reset.setEnabled(True)
+ self.footer_button_stop.setEnabled(False)
+
+ # Update action with result
+ plugin_item = self.plugin_model.plugin_items[result["plugin"].id]
+ action_state = plugin_item.data(Roles.PluginActionProgressRole)
+ action_state |= PluginActionStates.HasFinished
+
+ error = result.get("error")
+ if error:
+ records = result.get("records") or []
+ action_state |= PluginActionStates.HasFailed
+ fname, line_no, func, exc = error.traceback
+
+ records.append({
+ "label": str(error),
+ "type": "error",
+ "filename": str(fname),
+ "lineno": str(line_no),
+ "func": str(func),
+ "traceback": error.formatted_traceback
+ })
+
+ result["records"] = records
+
+ plugin_item.setData(action_state, Roles.PluginActionProgressRole)
+
+ self.plugin_model.update_with_result(result)
+ self.instance_model.update_with_result(result)
+ self.terminal_model.update_with_result(result)
+
+ def closeEvent(self, event):
+ """Perform post-flight checks before closing
+
+ Make sure processing of any kind is wrapped up before closing
+
+ """
+
+ # Make it snappy, but take care to clean it all up.
+ # TODO(marcus): Enable GUI to return on problem, such
+ # as asking whether or not the user really wants to quit
+ # given there are things currently running.
+ self.hide()
+
+ if self.state["is_closing"]:
+
+ # Explicitly clear potentially referenced data
+ self.info(self.tr("Cleaning up models.."))
+ self.intent_model.deleteLater()
+ self.plugin_model.deleteLater()
+ self.terminal_model.deleteLater()
+ self.terminal_proxy.deleteLater()
+ self.plugin_proxy.deleteLater()
+
+ self.artist_view.setModel(None)
+ self.overview_instance_view.setModel(None)
+ self.overview_plugin_view.setModel(None)
+ self.terminal_view.setModel(None)
+
+ self.info(self.tr("Cleaning up controller.."))
+ self.controller.cleanup()
+
+ self.info(self.tr("All clean!"))
+ self.info(self.tr("Good bye"))
+ return super(Window, self).closeEvent(event)
+
+ self.info(self.tr("Closing.."))
+
+ def on_problem():
+ self.heads_up(
+ "Warning", "Had trouble closing down. "
+ "Please tell someone and try again."
+ )
+ self.show()
+
+ if self.controller.is_running:
+ self.info(self.tr("..as soon as processing is finished.."))
+ self.controller.stop()
+ self.finished.connect(self.close)
+ util.defer(2000, on_problem)
+ return event.ignore()
+
+ self.state["is_closing"] = True
+
+ util.defer(200, self.close)
+ return event.ignore()
+
+ def reject(self):
+ """Handle ESC key"""
+
+ if self.controller.is_running:
+ self.info(self.tr("Stopping.."))
+ self.controller.stop()
+
+ # -------------------------------------------------------------------------
+ #
+ # Feedback
+ #
+ # -------------------------------------------------------------------------
+
+ def info(self, message):
+ """Print user-facing information
+
+ Arguments:
+ message (str): Text message for the user
+
+ """
+
+ info = self.findChild(QtWidgets.QLabel, "FooterInfo")
+ info.setText(message)
+
+ # Include message in terminal
+ self.terminal_model.append({
+ "label": message,
+ "type": "info"
+ })
+
+ self.animation_info_msg.stop()
+ self.animation_info_msg.start()
+
+ # TODO(marcus): Should this be configurable? Do we want
+ # the shell to fill up with these messages?
+ util.u_print(message)
+
+ def warning(self, message):
+ """Block processing and print warning until user hits "Continue"
+
+ Arguments:
+ message (str): Message to display
+
+ """
+
+ # TODO(marcus): Implement this.
+ self.info(message)
+
+ def heads_up(self, title, message, command=None):
+ """Provide a front-and-center message with optional command
+
+ Arguments:
+ title (str): Bold and short message
+ message (str): Extended message
+ command (optional, callable): Function is provided as a button
+
+ """
+
+ # TODO(marcus): Implement this.
+ self.info(message)