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)