diff --git a/igniter/Poppins/OFL.txt b/igniter/Poppins/OFL.txt new file mode 100644 index 0000000000..76df3b5656 --- /dev/null +++ b/igniter/Poppins/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/igniter/Poppins/Poppins-Black.ttf b/igniter/Poppins/Poppins-Black.ttf new file mode 100644 index 0000000000..a9520b78ac Binary files /dev/null and b/igniter/Poppins/Poppins-Black.ttf differ diff --git a/igniter/Poppins/Poppins-BlackItalic.ttf b/igniter/Poppins/Poppins-BlackItalic.ttf new file mode 100644 index 0000000000..ebfdd707e5 Binary files /dev/null and b/igniter/Poppins/Poppins-BlackItalic.ttf differ diff --git a/igniter/Poppins/Poppins-Bold.ttf b/igniter/Poppins/Poppins-Bold.ttf new file mode 100644 index 0000000000..b94d47f3af Binary files /dev/null and b/igniter/Poppins/Poppins-Bold.ttf differ diff --git a/igniter/Poppins/Poppins-BoldItalic.ttf b/igniter/Poppins/Poppins-BoldItalic.ttf new file mode 100644 index 0000000000..e2e64456c7 Binary files /dev/null and b/igniter/Poppins/Poppins-BoldItalic.ttf differ diff --git a/igniter/Poppins/Poppins-ExtraBold.ttf b/igniter/Poppins/Poppins-ExtraBold.ttf new file mode 100644 index 0000000000..8f008c3684 Binary files /dev/null and b/igniter/Poppins/Poppins-ExtraBold.ttf differ diff --git a/igniter/Poppins/Poppins-ExtraBoldItalic.ttf b/igniter/Poppins/Poppins-ExtraBoldItalic.ttf new file mode 100644 index 0000000000..b2a9bf557a Binary files /dev/null and b/igniter/Poppins/Poppins-ExtraBoldItalic.ttf differ diff --git a/igniter/Poppins/Poppins-ExtraLight.ttf b/igniter/Poppins/Poppins-ExtraLight.ttf new file mode 100644 index 0000000000..ee6238251f Binary files /dev/null and b/igniter/Poppins/Poppins-ExtraLight.ttf differ diff --git a/igniter/Poppins/Poppins-ExtraLightItalic.ttf b/igniter/Poppins/Poppins-ExtraLightItalic.ttf new file mode 100644 index 0000000000..e392492abd Binary files /dev/null and b/igniter/Poppins/Poppins-ExtraLightItalic.ttf differ diff --git a/igniter/Poppins/Poppins-Italic.ttf b/igniter/Poppins/Poppins-Italic.ttf new file mode 100644 index 0000000000..46203996d3 Binary files /dev/null and b/igniter/Poppins/Poppins-Italic.ttf differ diff --git a/igniter/Poppins/Poppins-Light.ttf b/igniter/Poppins/Poppins-Light.ttf new file mode 100644 index 0000000000..2ab022196b Binary files /dev/null and b/igniter/Poppins/Poppins-Light.ttf differ diff --git a/igniter/Poppins/Poppins-LightItalic.ttf b/igniter/Poppins/Poppins-LightItalic.ttf new file mode 100644 index 0000000000..6f9279daef Binary files /dev/null and b/igniter/Poppins/Poppins-LightItalic.ttf differ diff --git a/igniter/Poppins/Poppins-Medium.ttf b/igniter/Poppins/Poppins-Medium.ttf new file mode 100644 index 0000000000..e90e87ed69 Binary files /dev/null and b/igniter/Poppins/Poppins-Medium.ttf differ diff --git a/igniter/Poppins/Poppins-MediumItalic.ttf b/igniter/Poppins/Poppins-MediumItalic.ttf new file mode 100644 index 0000000000..d8a251c7c4 Binary files /dev/null and b/igniter/Poppins/Poppins-MediumItalic.ttf differ diff --git a/igniter/Poppins/Poppins-Regular.ttf b/igniter/Poppins/Poppins-Regular.ttf new file mode 100644 index 0000000000..be06e7fdca Binary files /dev/null and b/igniter/Poppins/Poppins-Regular.ttf differ diff --git a/igniter/Poppins/Poppins-SemiBold.ttf b/igniter/Poppins/Poppins-SemiBold.ttf new file mode 100644 index 0000000000..dabf7c242e Binary files /dev/null and b/igniter/Poppins/Poppins-SemiBold.ttf differ diff --git a/igniter/Poppins/Poppins-SemiBoldItalic.ttf b/igniter/Poppins/Poppins-SemiBoldItalic.ttf new file mode 100644 index 0000000000..29d5f7419b Binary files /dev/null and b/igniter/Poppins/Poppins-SemiBoldItalic.ttf differ diff --git a/igniter/Poppins/Poppins-Thin.ttf b/igniter/Poppins/Poppins-Thin.ttf new file mode 100644 index 0000000000..f5c0fdd531 Binary files /dev/null and b/igniter/Poppins/Poppins-Thin.ttf differ diff --git a/igniter/Poppins/Poppins-ThinItalic.ttf b/igniter/Poppins/Poppins-ThinItalic.ttf new file mode 100644 index 0000000000..b910089316 Binary files /dev/null and b/igniter/Poppins/Poppins-ThinItalic.ttf differ diff --git a/igniter/__init__.py b/igniter/__init__.py index c2442ad57f..20bf9be106 100644 --- a/igniter/__init__.py +++ b/igniter/__init__.py @@ -10,29 +10,22 @@ from .bootstrap_repos import BootstrapRepos from .version import __version__ as version -RESULT = 0 - - -def get_result(res: int): - """Sets result returned from dialog.""" - global RESULT - RESULT = res - - def open_dialog(): """Show Igniter dialog.""" - from Qt import QtWidgets + from Qt import QtWidgets, QtCore from .install_dialog import InstallDialog + scale_attr = getattr(QtCore.Qt, "AA_EnableHighDpiScaling", None) + if scale_attr is not None: + QtWidgets.QApplication.setAttribute(scale_attr) + app = QtWidgets.QApplication(sys.argv) d = InstallDialog() - d.finished.connect(get_result) d.open() - app.exec() - - return RESULT + app.exec_() + return d.result() __all__ = [ diff --git a/igniter/install_dialog.py b/igniter/install_dialog.py index 27b2d1fe37..e6439b5129 100644 --- a/igniter/install_dialog.py +++ b/igniter/install_dialog.py @@ -2,14 +2,15 @@ """Show dialog for choosing central pype repository.""" import os import sys +import re +import collections from Qt import QtCore, QtGui, QtWidgets # noqa from Qt.QtGui import QValidator # noqa from Qt.QtCore import QTimer # noqa -from .install_thread import InstallThread, InstallResult +from .install_thread import InstallThread from .tools import ( - validate_path_string, validate_mongo_connection, get_openpype_path_from_db ) @@ -17,504 +18,480 @@ from .user_settings import OpenPypeSecureRegistry from .version import __version__ -class FocusHandlingLineEdit(QtWidgets.QLineEdit): - """Handling focus in/out on QLineEdit.""" - focusIn = QtCore.Signal() - focusOut = QtCore.Signal() +def load_stylesheet(): + stylesheet_path = os.path.join( + os.path.dirname(__file__), + "stylesheet.css" + ) + with open(stylesheet_path, "r") as file_stream: + stylesheet = file_stream.read() - def focusOutEvent(self, event): # noqa - """For emitting signal on focus out.""" - self.focusOut.emit() - super().focusOutEvent(event) + return stylesheet - def focusInEvent(self, event): # noqa - """For emitting signal on focus in.""" - self.focusIn.emit() - super().focusInEvent(event) + +class ButtonWithOptions(QtWidgets.QFrame): + option_clicked = QtCore.Signal(str) + + def __init__(self, commands, parent=None): + super(ButtonWithOptions, self).__init__(parent) + + self.setObjectName("ButtonWithOptions") + + options_btn = QtWidgets.QToolButton(self) + options_btn.setArrowType(QtCore.Qt.DownArrow) + options_btn.setIconSize(QtCore.QSize(12, 12)) + + default = None + default_label = None + options_menu = QtWidgets.QMenu(self) + for option, option_label in commands.items(): + if default is None: + default = option + default_label = option_label + continue + action = QtWidgets.QAction(option_label, options_menu) + action.setData(option) + options_menu.addAction(action) + + main_btn = QtWidgets.QPushButton(default_label, self) + main_btn.setFlat(True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(1) + + main_layout.addWidget(main_btn, 1, QtCore.Qt.AlignVCenter) + main_layout.addWidget(options_btn, 0, QtCore.Qt.AlignVCenter) + + main_btn.clicked.connect(self._on_main_button) + options_btn.clicked.connect(self._on_options_click) + options_menu.triggered.connect(self._on_trigger) + + self.main_btn = main_btn + self.options_btn = options_btn + self.options_menu = options_menu + + options_btn.setEnabled(not options_menu.isEmpty()) + + self._default_value = default + + def resizeEvent(self, event): + super(ButtonWithOptions, self).resizeEvent(event) + self.options_btn.setFixedHeight(self.main_btn.height()) + + def _on_options_click(self): + pos = self.main_btn.rect().bottomLeft() + point = self.main_btn.mapToGlobal(pos) + self.options_menu.popup(point) + + def _on_trigger(self, action): + self.option_clicked.emit(action.data()) + + def _on_main_button(self): + self.option_clicked.emit(self._default_value) + + +class NiceProgressBar(QtWidgets.QProgressBar): + def __init__(self, parent=None): + super(NiceProgressBar, self).__init__(parent) + self._real_value = 0 + + def setValue(self, value): + self._real_value = value + if value != 0 and value < 11: + value = 11 + + super(NiceProgressBar, self).setValue(value) + + def value(self): + return self._real_value + + def text(self): + return "{} %".format(self._real_value) + + +class ConsoleWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super(ConsoleWidget, self).__init__(parent) + + # style for normal and error console text + default_console_style = QtGui.QTextCharFormat() + error_console_style = QtGui.QTextCharFormat() + default_console_style.setForeground( + QtGui.QColor.fromRgb(72, 200, 150) + ) + error_console_style.setForeground( + QtGui.QColor.fromRgb(184, 54, 19) + ) + + label = QtWidgets.QLabel("Console:", self) + + console_output = QtWidgets.QPlainTextEdit(self) + console_output.setMinimumSize(QtCore.QSize(300, 200)) + console_output.setReadOnly(True) + console_output.setCurrentCharFormat(default_console_style) + console_output.setObjectName("Console") + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(label, 0) + main_layout.addWidget(console_output, 1) + + self.default_console_style = default_console_style + self.error_console_style = error_console_style + + self.label = label + self.console_output = console_output + + self.hide_console() + + def hide_console(self): + self.label.setVisible(False) + self.console_output.setVisible(False) + + self.updateGeometry() + + def show_console(self): + self.label.setVisible(True) + self.console_output.setVisible(True) + + self.updateGeometry() + + def update_console(self, msg: str, error: bool = False) -> None: + if not error: + self.console_output.setCurrentCharFormat( + self.default_console_style + ) + else: + self.console_output.setCurrentCharFormat( + self.error_console_style + ) + self.console_output.appendPlainText(msg) + + +class MongoUrlInput(QtWidgets.QLineEdit): + """Widget to input mongodb URL.""" + + def set_valid(self): + """Set valid state on mongo url input.""" + self.setProperty("state", "valid") + self.style().polish(self) + + def remove_state(self): + """Set invalid state on mongo url input.""" + self.setProperty("state", "") + self.style().polish(self) + + def set_invalid(self): + """Set invalid state on mongo url input.""" + self.setProperty("state", "invalid") + self.style().polish(self) class InstallDialog(QtWidgets.QDialog): """Main Igniter dialog window.""" - _size_w = 400 - _size_h = 600 - path = "" - _controls_disabled = False + + mongo_url_regex = re.compile(r"^(mongodb|mongodb\+srv)://.*?") + + _width = 500 + _height = 200 + commands = collections.OrderedDict([ + ("run", "Start"), + ("run_from_code", "Run from code") + ]) def __init__(self, parent=None): super(InstallDialog, self).__init__(parent) - self.secure_registry = OpenPypeSecureRegistry("mongodb") - self.mongo_url = "" + self.setWindowTitle( + f"OpenPype Igniter {__version__}" + ) + self.setWindowFlags( + QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowMinimizeButtonHint + ) + + current_dir = os.path.dirname(os.path.abspath(__file__)) + roboto_font_path = os.path.join(current_dir, "RobotoMono-Regular.ttf") + poppins_font_path = os.path.join(current_dir, "Poppins") + icon_path = os.path.join(current_dir, "openpype_icon.png") + + # Install roboto font + QtGui.QFontDatabase.addApplicationFont(roboto_font_path) + for filename in os.listdir(poppins_font_path): + if os.path.splitext(filename)[1] == ".ttf": + QtGui.QFontDatabase.addApplicationFont(filename) + + # Load logo + pixmap_openpype_logo = QtGui.QPixmap(icon_path) + # Set logo as icon of window + self.setWindowIcon(QtGui.QIcon(pixmap_openpype_logo)) + + secure_registry = OpenPypeSecureRegistry("mongodb") + mongo_url = "" try: - self.mongo_url = ( + mongo_url = ( os.getenv("OPENPYPE_MONGO", "") - or self.secure_registry.get_item("openPypeMongo") + or secure_registry.get_item("openPypeMongo") ) except ValueError: pass - self.setWindowTitle( - f"OpenPype Igniter {__version__} - OpenPype installation") - self._icon_path = os.path.join( - os.path.dirname(__file__), 'openpype_icon.png') - icon = QtGui.QIcon(self._icon_path) - self.setWindowIcon(icon) - self.setWindowFlags( - QtCore.Qt.WindowCloseButtonHint | - QtCore.Qt.WindowMinimizeButtonHint - ) + self.mongo_url = mongo_url + self._pixmap_openpype_logo = pixmap_openpype_logo - self.setMinimumSize( - QtCore.QSize(self._size_w, self._size_h)) - self.setMaximumSize( - QtCore.QSize(self._size_w + 100, self._size_h + 500)) - - # style for normal console text - self.default_console_style = QtGui.QTextCharFormat() - # self.default_console_style.setFontPointSize(0.1) - self.default_console_style.setForeground( - QtGui.QColor.fromRgb(72, 200, 150)) - - # style for error console text - self.error_console_style = QtGui.QTextCharFormat() - # self.error_console_style.setFontPointSize(0.1) - self.error_console_style.setForeground( - QtGui.QColor.fromRgb(184, 54, 19)) - - QtGui.QFontDatabase.addApplicationFont( - os.path.join( - os.path.dirname(__file__), 'RobotoMono-Regular.ttf') - ) - self._openpype_run_ready = False + self._secure_registry = secure_registry + self._controls_disabled = False + self._install_thread = None + self.resize(QtCore.QSize(self._width, self._height)) self._init_ui() + # Set stylesheet + self.setStyleSheet(load_stylesheet()) + + # Trigger Mongo URL validation + self._mongo_input.setText(self.mongo_url) + def _init_ui(self): # basic visual style - dark background, light text - self.setStyleSheet(""" - color: rgb(200, 200, 200); - background-color: rgb(23, 23, 23); - """) - - main = QtWidgets.QVBoxLayout(self) # Main info # -------------------------------------------------------------------- - self.main_label = QtWidgets.QLabel( - """Welcome to OpenPype -
- We've detected OpenPype is not configured yet. But don't worry, - this is as easy as setting one or two things. -
- """) - self.main_label.setWordWrap(True) - self.main_label.setStyleSheet("color: rgb(200, 200, 200);") - - # OpenPype path info - # -------------------------------------------------------------------- - - self.openpype_path_label = QtWidgets.QLabel( - """This is Path to studio location where OpenPype versions - are stored. It will be pre-filled if your MongoDB connection is - already set and your studio defined this location. -
- Leave it empty if you want to install OpenPype version that - comes with this installation. -
-- If you want to just try OpenPype without installing, hit the - middle button that states "run without installation". -
- """ - ) - - self.openpype_path_label.setWordWrap(True) - self.openpype_path_label.setStyleSheet("color: rgb(150, 150, 150);") - - # Path/Url box | Select button - # -------------------------------------------------------------------- - - input_layout = QtWidgets.QHBoxLayout() - - input_layout.setContentsMargins(0, 10, 0, 10) - self.user_input = FocusHandlingLineEdit() - - self.user_input.setPlaceholderText("Path to OpenPype versions") - self.user_input.textChanged.connect(self._path_changed) - self.user_input.setStyleSheet( - ("color: rgb(233, 233, 233);" - "background-color: rgb(64, 64, 64);" - "padding: 0.5em;" - "border: 1px solid rgb(32, 32, 32);") - ) - - self.user_input.setValidator(PathValidator(self.user_input)) - - self._btn_select = QtWidgets.QPushButton("Select") - self._btn_select.setToolTip( - "Select OpenPype repository" - ) - self._btn_select.setStyleSheet( - ("color: rgb(64, 64, 64);" - "background-color: rgb(72, 200, 150);" - "padding: 0.5em;") - ) - self._btn_select.setMaximumSize(100, 140) - self._btn_select.clicked.connect(self._on_select_clicked) - - input_layout.addWidget(self.user_input) - input_layout.addWidget(self._btn_select) + main_label = QtWidgets.QLabel("Welcome to OpenPype", self) + main_label.setWordWrap(True) + main_label.setObjectName("MainLabel") # Mongo box | OK button # -------------------------------------------------------------------- - - self.mongo_label = QtWidgets.QLabel( - """Enter URL for running MongoDB instance:""" + mongo_input = MongoUrlInput(self) + mongo_input.setPlaceholderText( + "Enter your database Address. Example: mongodb://192.168.1.10:2707" ) - self.mongo_label.setWordWrap(True) - self.mongo_label.setStyleSheet("color: rgb(150, 150, 150);") + mongo_messages_widget = QtWidgets.QWidget(self) - class MongoWidget(QtWidgets.QWidget): - """Widget to input mongodb URL.""" - - def __init__(self, parent=None): - self._btn_mongo = None - super(MongoWidget, self).__init__(parent) - mongo_layout = QtWidgets.QHBoxLayout() - mongo_layout.setContentsMargins(0, 0, 0, 0) - self._mongo_input = FocusHandlingLineEdit() - self._mongo_input.setPlaceholderText("Mongo URL") - self._mongo_input.textChanged.connect(self._mongo_changed) - self._mongo_input.focusIn.connect(self._focus_in) - self._mongo_input.focusOut.connect(self._focus_out) - self._mongo_input.setValidator( - MongoValidator(self._mongo_input)) - self._mongo_input.setStyleSheet( - ("color: rgb(233, 233, 233);" - "background-color: rgb(64, 64, 64);" - "padding: 0.5em;" - "border: 1px solid rgb(32, 32, 32);") - ) - - mongo_layout.addWidget(self._mongo_input) - self.setLayout(mongo_layout) - - def _focus_out(self): - self.validate_url() - - def _focus_in(self): - self._mongo_input.setStyleSheet( - """ - background-color: rgb(32, 32, 19); - color: rgb(255, 190, 15); - padding: 0.5em; - border: 1px solid rgb(64, 64, 32); - """ - ) - - def _mongo_changed(self, mongo: str): - self.parent().mongo_url = mongo - - def get_mongo_url(self) -> str: - """Helper to get url from parent.""" - return self.parent().mongo_url - - def set_mongo_url(self, mongo: str): - """Helper to set url to parent. - - Args: - mongo (str): mongodb url string. - - """ - self._mongo_input.setText(mongo) - - def set_valid(self): - """Set valid state on mongo url input.""" - self._mongo_input.setStyleSheet( - """ - background-color: rgb(19, 19, 19); - color: rgb(64, 230, 132); - padding: 0.5em; - border: 1px solid rgb(32, 64, 32); - """ - ) - self.parent().install_button.setEnabled(True) - - def set_invalid(self): - """Set invalid state on mongo url input.""" - self._mongo_input.setStyleSheet( - """ - background-color: rgb(32, 19, 19); - color: rgb(255, 69, 0); - padding: 0.5em; - border: 1px solid rgb(64, 32, 32); - """ - ) - self.parent().install_button.setEnabled(False) - - def set_read_only(self, state: bool): - """Set input read-only.""" - self._mongo_input.setReadOnly(state) - - def validate_url(self) -> bool: - """Validate if entered url is ok. - - Returns: - True if url is valid monogo string. - - """ - if self.parent().mongo_url == "": - return False - - is_valid, reason_str = validate_mongo_connection( - self.parent().mongo_url - ) - if not is_valid: - self.set_invalid() - self.parent().update_console(f"!!! {reason_str}", True) - return False - else: - self.set_valid() - return True - - self._mongo = MongoWidget(self) - if self.mongo_url: - self._mongo.set_mongo_url(self.mongo_url) - - # Bottom button bar - # -------------------------------------------------------------------- - bottom_widget = QtWidgets.QWidget() - bottom_layout = QtWidgets.QHBoxLayout() - openpype_logo_label = QtWidgets.QLabel("openpype logo") - openpype_logo = QtGui.QPixmap(self._icon_path) - # openpype_logo.scaled( - # openpype_logo_label.width(), - # openpype_logo_label.height(), QtCore.Qt.KeepAspectRatio) - openpype_logo_label.setPixmap(openpype_logo) - openpype_logo_label.setContentsMargins(10, 0, 0, 10) - - # install button - - - - - - - - - - - - - - - - - - - - - - - - - - - - self.install_button = QtWidgets.QPushButton("Install") - self.install_button.setStyleSheet( - ("color: rgb(64, 64, 64);" - "background-color: rgb(72, 200, 150);" - "padding: 0.5em;") + mongo_connection_msg = QtWidgets.QLabel(mongo_messages_widget) + mongo_connection_msg.setVisible(True) + mongo_connection_msg.setTextInteractionFlags( + QtCore.Qt.TextSelectableByMouse ) - self.install_button.setMinimumSize(64, 24) - self.install_button.setToolTip("Install OpenPype") - self.install_button.clicked.connect(self._on_ok_clicked) - # run from current button - - - - - - - - - - - - - - - - - - - - - - - self.run_button = QtWidgets.QPushButton("Run without installation") - self.run_button.setStyleSheet( - ("color: rgb(64, 64, 64);" - "background-color: rgb(200, 164, 64);" - "padding: 0.5em;") - ) - self.run_button.setMinimumSize(64, 24) - self.run_button.setToolTip("Run without installing Pype") - self.run_button.clicked.connect(self._on_run_clicked) - - # install button - - - - - - - - - - - - - - - - - - - - - - - - - - - - self._exit_button = QtWidgets.QPushButton("Exit") - self._exit_button.setStyleSheet( - ("color: rgb(64, 64, 64);" - "background-color: rgb(128, 128, 128);" - "padding: 0.5em;") - ) - self._exit_button.setMinimumSize(64, 24) - self._exit_button.setToolTip("Exit") - self._exit_button.clicked.connect(self._on_exit_clicked) - - bottom_layout.setContentsMargins(0, 10, 10, 0) - bottom_layout.setAlignment(QtCore.Qt.AlignVCenter) - bottom_layout.addWidget(openpype_logo_label, 0, QtCore.Qt.AlignVCenter) - bottom_layout.addStretch(1) - bottom_layout.addWidget(self.install_button, 0, QtCore.Qt.AlignVCenter) - bottom_layout.addWidget(self.run_button, 0, QtCore.Qt.AlignVCenter) - bottom_layout.addWidget(self._exit_button, 0, QtCore.Qt.AlignVCenter) - - bottom_widget.setLayout(bottom_layout) - bottom_widget.setStyleSheet("background-color: rgb(32, 32, 32);") - - # Console label - # -------------------------------------------------------------------- - self._status_label = QtWidgets.QLabel("Console:") - self._status_label.setContentsMargins(0, 10, 0, 10) - self._status_label.setStyleSheet("color: rgb(61, 115, 97);") - - # Console - # -------------------------------------------------------------------- - self._status_box = QtWidgets.QPlainTextEdit() - self._status_box.setReadOnly(True) - self._status_box.setCurrentCharFormat(self.default_console_style) - self._status_box.setStyleSheet( - """QPlainTextEdit { - background-color: rgb(32, 32, 32); - color: rgb(72, 200, 150); - font-family: "Roboto Mono"; - font-size: 0.5em; - border: 1px solid rgb(48, 48, 48); - } - QScrollBar:vertical { - border: 1px solid rgb(61, 115, 97); - background: #000; - width:5px; - margin: 0px 0px 0px 0px; - } - QScrollBar::handle:vertical { - background: rgb(72, 200, 150); - min-height: 0px; - } - QScrollBar::sub-page:vertical { - background: rgb(31, 62, 50); - } - QScrollBar::add-page:vertical { - background: rgb(31, 62, 50); - } - QScrollBar::add-line:vertical { - background: rgb(72, 200, 150); - height: 0px; - subcontrol-position: bottom; - subcontrol-origin: margin; - } - QScrollBar::sub-line:vertical { - background: rgb(72, 200, 150); - height: 0 px; - subcontrol-position: top; - subcontrol-origin: margin; - } - """ - ) + mongo_messages_layout = QtWidgets.QVBoxLayout(mongo_messages_widget) + mongo_messages_layout.setContentsMargins(0, 0, 0, 0) + mongo_messages_layout.addWidget(mongo_connection_msg) # Progress bar # -------------------------------------------------------------------- - self._progress_bar = QtWidgets.QProgressBar() - self._progress_bar.setValue(0) - self._progress_bar.setAlignment(QtCore.Qt.AlignCenter) - self._progress_bar.setTextVisible(False) - # setting font and the size - self._progress_bar.setFont(QtGui.QFont('Arial', 7)) - self._progress_bar.setStyleSheet( - """QProgressBar:horizontal { - height: 5px; - border: 1px solid rgb(31, 62, 50); - color: rgb(72, 200, 150); - } - QProgressBar::chunk:horizontal { - background-color: rgb(72, 200, 150); - } - """ + progress_bar = NiceProgressBar(self) + progress_bar.setAlignment(QtCore.Qt.AlignCenter) + progress_bar.setTextVisible(False) + + # Console + # -------------------------------------------------------------------- + console_widget = ConsoleWidget(self) + + # Bottom button bar + # -------------------------------------------------------------------- + bottom_widget = QtWidgets.QWidget(self) + + btns_widget = QtWidgets.QWidget(bottom_widget) + + openpype_logo_label = QtWidgets.QLabel("openpype logo", bottom_widget) + openpype_logo_label.setPixmap(self._pixmap_openpype_logo) + + run_button = ButtonWithOptions( + self.commands, + btns_widget ) + run_button.setMinimumSize(64, 24) + run_button.setToolTip("Run OpenPype") + + # install button - - - - - - - - - - - - - - - - - - - - - - - - - - - + exit_button = QtWidgets.QPushButton("Exit", btns_widget) + exit_button.setObjectName("ExitBtn") + exit_button.setFlat(True) + exit_button.setMinimumSize(64, 24) + exit_button.setToolTip("Exit") + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(run_button, 0) + btns_layout.addWidget(exit_button, 0) + + bottom_layout = QtWidgets.QHBoxLayout(bottom_widget) + bottom_layout.setContentsMargins(0, 0, 0, 0) + bottom_layout.setAlignment(QtCore.Qt.AlignHCenter) + bottom_layout.addWidget(openpype_logo_label, 0) + bottom_layout.addStretch(1) + bottom_layout.addWidget(btns_widget, 0) + # add all to main - main.addWidget(self.main_label, 0) - main.addWidget(self.openpype_path_label, 0) - main.addLayout(input_layout, 0) - main.addWidget(self.mongo_label, 0) - main.addWidget(self._mongo, 0) + main = QtWidgets.QVBoxLayout(self) + main.addSpacing(15) + main.addWidget(main_label, 0) + main.addSpacing(15) + main.addWidget(mongo_input, 0) + main.addWidget(mongo_messages_widget, 0) - main.addWidget(self._status_label, 0) - main.addWidget(self._status_box, 1) + main.addWidget(progress_bar, 0) + main.addSpacing(15) + + main.addWidget(console_widget, 1) - main.addWidget(self._progress_bar, 0) main.addWidget(bottom_widget, 0) - self.setLayout(main) + run_button.option_clicked.connect(self._on_run_btn_click) + exit_button.clicked.connect(self._on_exit_clicked) + mongo_input.textChanged.connect(self._on_mongo_url_change) - # if mongo url is ok, try to get openpype path from there - if self._mongo.validate_url() and len(self.path) == 0: - self.path = get_openpype_path_from_db(self.mongo_url) - self.user_input.setText(self.path) + self._console_widget = console_widget - def _on_select_clicked(self): - """Show directory dialog.""" - options = QtWidgets.QFileDialog.Options() - options |= QtWidgets.QFileDialog.DontUseNativeDialog - options |= QtWidgets.QFileDialog.ShowDirsOnly + self.main_label = main_label - result = QtWidgets.QFileDialog.getExistingDirectory( - parent=self, - caption='Select path', - directory=os.getcwd(), - options=options) + self._mongo_input = mongo_input - if not result: + self._mongo_connection_msg = mongo_connection_msg + + self._run_button = run_button + self._exit_button = exit_button + self._progress_bar = progress_bar + + def _on_run_btn_click(self, option): + # Disable buttons + self._disable_buttons() + # Set progress to any value + self._update_progress(1) + self._progress_bar.repaint() + # Add label to show that is connecting to mongo + self.set_invalid_mongo_connection(self.mongo_url, True) + + # Process events to repaint changes + QtWidgets.QApplication.processEvents() + + if not self.validate_url(): + self._enable_buttons() + self._update_progress(0) + # Update any messages + self._mongo_input.setText(self.mongo_url) return - filename = QtCore.QDir.toNativeSeparators(result) - - if os.path.isdir(filename): - self.path = filename - self.user_input.setText(filename) - - def _on_run_clicked(self): - valid, reason = validate_mongo_connection( - self._mongo.get_mongo_url() - ) - if not valid: - self._mongo.set_invalid() - self.update_console(f"!!! {reason}", True) - return + if option == "run": + self._run_openpype() + elif option == "run_from_code": + self._run_openpype_from_code() else: - self._mongo.set_valid() + raise AssertionError("BUG: Unknown variant \"{}\"".format(option)) + + self._enable_buttons() + + def _run_openpype_from_code(self): + self._secure_registry.set_item("openPypeMongo", self.mongo_url) self.done(2) - def _on_ok_clicked(self): + def _run_openpype(self): """Start install process. This will once again validate entered path and mongo if ok, start working thread that will do actual job. """ - valid, reason = validate_mongo_connection( - self._mongo.get_mongo_url() - ) - if not valid: - self._mongo.set_invalid() - self.update_console(f"!!! {reason}", True) - return - else: - self._mongo.set_valid() - - if self._openpype_run_ready: - self.done(3) + # Check if install thread is not already running + if self._install_thread and self._install_thread.isRunning(): return - if self.path and len(self.path) > 0: - valid, reason = validate_path_string(self.path) + self._mongo_input.set_valid() - if not valid: - self.update_console(f"!!! {reason}", True) - return + install_thread = InstallThread(self) + install_thread.message.connect(self.update_console) + install_thread.progress.connect(self._update_progress) + install_thread.finished.connect(self._installation_finished) + install_thread.set_mongo(self.mongo_url) - self._disable_buttons() - self._install_thread = InstallThread( - self.install_result_callback_handler, self) - self._install_thread.message.connect(self.update_console) - self._install_thread.progress.connect(self._update_progress) - self._install_thread.finished.connect(self._enable_buttons) - self._install_thread.set_path(self.path) - self._install_thread.set_mongo(self._mongo.get_mongo_url()) - self._install_thread.start() + self._install_thread = install_thread - def install_result_callback_handler(self, result: InstallResult): - """Change button behaviour based on installation outcome.""" - status = result.status + install_thread.start() + + def _installation_finished(self): + status = self._install_thread.result() if status >= 0: - self.install_button.setText("Run installed OpenPype") - self._openpype_run_ready = True + self._update_progress(100) + QtWidgets.QApplication.processEvents() + self.done(3) + else: + self._show_console() def _update_progress(self, progress: int): self._progress_bar.setValue(progress) + text_visible = self._progress_bar.isTextVisible() + if progress == 0: + if text_visible: + self._progress_bar.setTextVisible(False) + elif not text_visible: + self._progress_bar.setTextVisible(True) def _on_exit_clicked(self): self.reject() - def _path_changed(self, path: str) -> str: - """Set path.""" - self.path = path - return path + def _on_mongo_url_change(self, new_value): + # Strip the value + new_value = new_value.strip() + # Store new mongo url to variable + self.mongo_url = new_value + + msg = None + # Change style of input + if not new_value: + self._mongo_input.remove_state() + elif not self.mongo_url_regex.match(new_value): + self._mongo_input.set_invalid() + msg = ( + "Mongo URL should start with" + " \"mongodb://\" or \"mongodb+srv://\"" + ) + else: + self._mongo_input.set_valid() + + self.set_invalid_mongo_url(msg) + + def validate_url(self): + """Validate if entered url is ok. + + Returns: + True if url is valid monogo string. + + """ + if self.mongo_url == "": + return False + + is_valid, reason_str = validate_mongo_connection(self.mongo_url) + if not is_valid: + self.set_invalid_mongo_connection(self.mongo_url) + self._mongo_input.set_invalid() + self.update_console(f"!!! {reason_str}", True) + return False + + self.set_invalid_mongo_connection(None) + self._mongo_input.set_valid() + return True + + def set_invalid_mongo_url(self, reason): + if reason is None: + self._mongo_connection_msg.setText("") + else: + self._mongo_connection_msg.setText("- {}".format(reason)) + + def set_invalid_mongo_connection(self, mongo_url, connecting=False): + if mongo_url is None: + self.set_invalid_mongo_url(mongo_url) + return + + if connecting: + msg = "Connecting to: {}".format(mongo_url) + else: + msg = "Can't connect to: {}".format(mongo_url) + + self.set_invalid_mongo_url(msg) def update_console(self, msg: str, error: bool = False) -> None: """Display message in console. @@ -523,26 +500,22 @@ class InstallDialog(QtWidgets.QDialog): msg (str): message. error (bool): if True, print it red. """ - if not error: - self._status_box.setCurrentCharFormat(self.default_console_style) - else: - self._status_box.setCurrentCharFormat(self.error_console_style) - self._status_box.appendPlainText(msg) + self._console_widget.update_console(msg, error) + + def _show_console(self): + self._console_widget.show_console() + self.updateGeometry() def _disable_buttons(self): """Disable buttons so user interaction doesn't interfere.""" - self._btn_select.setEnabled(False) - self.run_button.setEnabled(False) self._exit_button.setEnabled(False) - self.install_button.setEnabled(False) + self._run_button.setEnabled(False) self._controls_disabled = True def _enable_buttons(self): """Enable buttons after operation is complete.""" - self._btn_select.setEnabled(True) - self.run_button.setEnabled(True) self._exit_button.setEnabled(True) - self.install_button.setEnabled(True) + self._run_button.setEnabled(True) self._controls_disabled = False def closeEvent(self, event): # noqa @@ -552,212 +525,6 @@ class InstallDialog(QtWidgets.QDialog): return super(InstallDialog, self).closeEvent(event) -class MongoValidator(QValidator): - """Validate mongodb url for Qt widgets.""" - - def __init__(self, parent=None, intermediate=False): - self.parent = parent - self.intermediate = intermediate - self._validate_lock = False - self.timer = QTimer() - self.timer.timeout.connect(self._unlock_validator) - super().__init__(parent) - - def _unlock_validator(self): - self._validate_lock = False - - def _return_state( - self, state: QValidator.State, reason: str, mongo: str): - """Set stylesheets and actions on parent based on state. - - Warning: - This will always return `QValidator.State.Acceptable` as - anything different will stop input to `QLineEdit` - - """ - - if state == QValidator.State.Invalid: - self.parent.setToolTip(reason) - self.parent.setStyleSheet( - """ - background-color: rgb(32, 19, 19); - color: rgb(255, 69, 0); - padding: 0.5em; - border: 1px solid rgb(64, 32, 32); - """ - ) - elif state == QValidator.State.Intermediate and self.intermediate: - self.parent.setToolTip(reason) - self.parent.setStyleSheet( - """ - background-color: rgb(32, 32, 19); - color: rgb(255, 190, 15); - padding: 0.5em; - border: 1px solid rgb(64, 64, 32); - """ - ) - else: - self.parent.setToolTip(reason) - self.parent.setStyleSheet( - """ - background-color: rgb(19, 19, 19); - color: rgb(64, 230, 132); - padding: 0.5em; - border: 1px solid rgb(32, 64, 32); - """ - ) - - return QValidator.State.Acceptable, mongo, len(mongo) - - def validate(self, mongo: str, pos: int) -> (QValidator.State, str, int): # noqa - """Validate entered mongodb connection string. - - As url (it should start with `mongodb://` or - `mongodb+srv:// url schema. - - Args: - mongo (str): connection string url. - pos (int): current position. - - Returns: - (QValidator.State.Acceptable, str, int): - Indicate input state with color and always return - Acceptable state as we need to be able to edit input further. - - """ - if not mongo.startswith("mongodb"): - return self._return_state( - QValidator.State.Invalid, "need mongodb schema", mongo) - - return self._return_state( - QValidator.State.Intermediate, "", mongo) - - -class PathValidator(MongoValidator): - """Validate mongodb url for Qt widgets.""" - - def validate(self, path: str, pos: int) -> (QValidator.State, str, int): # noqa - """Validate path to be accepted by Igniter. - - Args: - path (str): path to OpenPype. - pos (int): current position. - - Returns: - (QValidator.State.Acceptable, str, int): - Indicate input state with color and always return - Acceptable state as we need to be able to edit input further. - - """ - # allow empty path as that will use current version coming with - # OpenPype Igniter - if len(path) == 0: - return self._return_state( - QValidator.State.Acceptable, "Use version with Igniter", path) - - if len(path) > 3: - valid, reason = validate_path_string(path) - if not valid: - return self._return_state( - QValidator.State.Invalid, reason, path) - else: - return self._return_state( - QValidator.State.Acceptable, reason, path) - - -class CollapsibleWidget(QtWidgets.QWidget): - """Collapsible widget to hide mongo url in necessary.""" - - def __init__(self, parent=None, title: str = "", animation: int = 300): - self._mainLayout = QtWidgets.QGridLayout(parent) - self._toggleButton = QtWidgets.QToolButton(parent) - self._headerLine = QtWidgets.QFrame(parent) - self._toggleAnimation = QtCore.QParallelAnimationGroup(parent) - self._contentArea = QtWidgets.QScrollArea(parent) - self._animation = animation - self._title = title - super(CollapsibleWidget, self).__init__(parent) - self._init_ui() - - def _init_ui(self): - self._toggleButton.setStyleSheet( - """QToolButton { - border: none; - } - """) - self._toggleButton.setToolButtonStyle( - QtCore.Qt.ToolButtonTextBesideIcon) - - self._toggleButton.setArrowType(QtCore.Qt.ArrowType.RightArrow) - self._toggleButton.setText(self._title) - self._toggleButton.setCheckable(True) - self._toggleButton.setChecked(False) - - self._headerLine.setFrameShape(QtWidgets.QFrame.HLine) - self._headerLine.setFrameShadow(QtWidgets.QFrame.Sunken) - self._headerLine.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Maximum) - - self._contentArea.setStyleSheet( - """QScrollArea { - background-color: rgb(32, 32, 32); - border: none; - } - """) - self._contentArea.setSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Fixed) - self._contentArea.setMaximumHeight(0) - self._contentArea.setMinimumHeight(0) - - self._toggleAnimation.addAnimation( - QtCore.QPropertyAnimation(self, b"minimumHeight")) - self._toggleAnimation.addAnimation( - QtCore.QPropertyAnimation(self, b"maximumHeight")) - self._toggleAnimation.addAnimation( - QtCore.QPropertyAnimation(self._contentArea, b"maximumHeight")) - - self._mainLayout.setVerticalSpacing(0) - self._mainLayout.setContentsMargins(0, 0, 0, 0) - - row = 0 - - self._mainLayout.addWidget( - self._toggleButton, row, 0, 1, 1, QtCore.Qt.AlignCenter) - self._mainLayout.addWidget( - self._headerLine, row, 2, 1, 1) - row += row - self._mainLayout.addWidget(self._contentArea, row, 0, 1, 3) - self.setLayout(self._mainLayout) - - self._toggleButton.toggled.connect(self._toggle_action) - - def _toggle_action(self, collapsed: bool): - arrow = QtCore.Qt.ArrowType.DownArrow if collapsed else QtCore.Qt.ArrowType.RightArrow # noqa: E501 - direction = QtCore.QAbstractAnimation.Forward if collapsed else QtCore.QAbstractAnimation.Backward # noqa: E501 - self._toggleButton.setArrowType(arrow) - self._toggleAnimation.setDirection(direction) - self._toggleAnimation.start() - - def setContentLayout(self, content_layout: QtWidgets.QLayout): # noqa - self._contentArea.setLayout(content_layout) - collapsed_height = \ - self.sizeHint().height() - self._contentArea.maximumHeight() - content_height = self._contentArea.sizeHint().height() - - for i in range(self._toggleAnimation.animationCount() - 1): - sec_anim = self._toggleAnimation.animationAt(i) - sec_anim.setDuration(self._animation) - sec_anim.setStartValue(collapsed_height) - sec_anim.setEndValue(collapsed_height + content_height) - - con_anim = self._toggleAnimation.animationAt( - self._toggleAnimation.animationCount() - 1) - - con_anim.setDuration(self._animation) - con_anim.setStartValue(0) - con_anim.setEndValue(collapsed_height + content_height) - - if __name__ == "__main__": app = QtWidgets.QApplication(sys.argv) d = InstallDialog() diff --git a/igniter/install_thread.py b/igniter/install_thread.py index df8b830209..383012b88b 100644 --- a/igniter/install_thread.py +++ b/igniter/install_thread.py @@ -17,12 +17,6 @@ from .bootstrap_repos import ( from .tools import validate_mongo_connection -class InstallResult(QObject): - """Used to pass results back.""" - def __init__(self, value): - self.status = value - - class InstallThread(QThread): """Install Worker thread. @@ -36,15 +30,22 @@ class InstallThread(QThread): """ progress = Signal(int) message = Signal((str, bool)) - finished = Signal(object) - def __init__(self, callback, parent=None,): + def __init__(self, parent=None,): self._mongo = None self._path = None - self.result_callback = callback + self._result = None QThread.__init__(self, parent) - self.finished.connect(callback) + + def result(self): + """Result of finished installation.""" + return self._result + + def _set_result(self, value): + if self._result is not None: + raise AssertionError("BUG: Result was set more than once!") + self._result = value def run(self): """Thread entry point. @@ -76,7 +77,7 @@ class InstallThread(QThread): except ValueError: self.message.emit( "!!! We need MongoDB URL to proceed.", True) - self.finished.emit(InstallResult(-1)) + self._set_result(-1) return else: self._mongo = os.getenv("OPENPYPE_MONGO") @@ -101,7 +102,7 @@ class InstallThread(QThread): self.message.emit("Skipping OpenPype install ...", False) if detected[-1].path.suffix.lower() == ".zip": bs.extract_openpype(detected[-1]) - self.finished.emit(InstallResult(0)) + self._set_result(0) return if OpenPypeVersion(version=local_version).get_main_version() == detected[-1].get_main_version(): # noqa @@ -110,7 +111,7 @@ class InstallThread(QThread): f"currently running {local_version}" ), False) self.message.emit("Skipping OpenPype install ...", False) - self.finished.emit(InstallResult(0)) + self._set_result(0) return self.message.emit(( @@ -126,13 +127,13 @@ class InstallThread(QThread): if not openpype_version: self.message.emit( f"!!! Install failed - {openpype_version}", True) - self.finished.emit(InstallResult(-1)) + self._set_result(-1) return self.message.emit(f"Using: {openpype_version}", False) bs.install_version(openpype_version) self.message.emit(f"Installed as {openpype_version}", False) self.progress.emit(100) - self.finished.emit(InstallResult(1)) + self._set_result(1) return else: self.message.emit("None detected.", False) @@ -144,7 +145,7 @@ class InstallThread(QThread): if not local_openpype: self.message.emit( f"!!! Install failed - {local_openpype}", True) - self.finished.emit(InstallResult(-1)) + self._set_result(-1) return try: @@ -154,11 +155,12 @@ class InstallThread(QThread): OpenPypeVersionIOError) as e: self.message.emit(f"Installed failed: ", True) self.message.emit(str(e), True) - self.finished.emit(InstallResult(-1)) + self._set_result(-1) return self.message.emit(f"Installed as {local_openpype}", False) self.progress.emit(100) + self._set_result(1) return else: # if we have mongo connection string, validate it, set it to @@ -167,7 +169,7 @@ class InstallThread(QThread): if not validate_mongo_connection(self._mongo): self.message.emit( f"!!! invalid mongo url {self._mongo}", True) - self.finished.emit(InstallResult(-1)) + self._set_result(-1) return bs.secure_registry.set_item("openPypeMongo", self._mongo) os.environ["OPENPYPE_MONGO"] = self._mongo @@ -177,11 +179,11 @@ class InstallThread(QThread): if not repo_file: self.message.emit("!!! Cannot install", True) - self.finished.emit(InstallResult(-1)) + self._set_result(-1) return self.progress.emit(100) - self.finished.emit(InstallResult(1)) + self._set_result(1) return def set_path(self, path: str) -> None: diff --git a/igniter/stylesheet.css b/igniter/stylesheet.css new file mode 100644 index 0000000000..8df2621d83 --- /dev/null +++ b/igniter/stylesheet.css @@ -0,0 +1,280 @@ +*{ + font-size: 10pt; + font-family: "Poppins"; +} + +QWidget { + color: #bfccd6; + background-color: #282C34; + border-radius: 0px; +} + +QMenu { + border: 1px solid #555555; + background-color: #21252B; +} + +QMenu::item { + padding: 5px 10px 5px 10px; + border-left: 5px solid #313741;; +} + +QMenu::item:selected { + border-left-color: rgb(84, 209, 178); + background-color: #222d37; +} + +QLineEdit, QPlainTextEdit { + border: 1px solid #464b54; + border-radius: 3px; + background-color: #21252B; + padding: 0.5em; +} + +QLineEdit[state="valid"] { + background-color: rgb(19, 19, 19); + color: rgb(64, 230, 132); + border-color: rgb(32, 64, 32); +} + +QLineEdit[state="invalid"] { + background-color: rgb(32, 19, 19); + color: rgb(255, 69, 0); + border-color: rgb(64, 32, 32); +} + +QLabel { + background: transparent; + color: #969b9e; +} + +QLabel:hover {color: #b8c1c5;} + +QPushButton { + border: 1px solid #aaaaaa; + border-radius: 3px; + padding: 5px; +} + +QPushButton:hover { + background-color: #333840; + border: 1px solid #fff; + color: #fff; +} + +QTableView { + border: 1px solid #444; + gridline-color: #6c6c6c; + background-color: #201F1F; + alternate-background-color:#21252B; +} + +QTableView::item:pressed, QListView::item:pressed, QTreeView::item:pressed { + background: #78879b; + color: #FFFFFF; +} + +QTableView::item:selected:active, QTreeView::item:selected:active, QListView::item:selected:active { + background: #3d8ec9; +} + +QProgressBar { + border: 1px solid grey; + border-radius: 10px; + color: #222222; + font-weight: bold; +} +QProgressBar:horizontal { + height: 20px; +} + +QProgressBar::chunk { + border-radius: 10px; + background-color: qlineargradient( + x1: 0, + y1: 0.5, + x2: 1, + y2: 0.5, + stop: 0 rgb(72, 200, 150), + stop: 1 rgb(82, 172, 215) + ); +} + + +QScrollBar:horizontal { + height: 15px; + margin: 3px 15px 3px 15px; + border: 1px transparent #21252B; + border-radius: 4px; + background-color: #21252B; +} + +QScrollBar::handle:horizontal { + background-color: #4B5362; + min-width: 5px; + border-radius: 4px; +} + +QScrollBar::add-line:horizontal { + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/right_arrow_disabled.png); + width: 10px; + height: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal { + margin: 0px 3px 0px 3px; + border-image: url(:/qss_icons/rc/left_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::add-line:horizontal:hover,QScrollBar::add-line:horizontal:on { + border-image: url(:/qss_icons/rc/right_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: right; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:horizontal:hover, QScrollBar::sub-line:horizontal:on { + border-image: url(:/qss_icons/rc/left_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: left; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:horizontal, QScrollBar::down-arrow:horizontal { + background: none; +} + +QScrollBar::add-page:horizontal, QScrollBar::sub-page:horizontal { + background: none; +} + +QScrollBar:vertical { + background-color: #21252B; + width: 15px; + margin: 15px 3px 15px 3px; + border: 1px transparent #21252B; + border-radius: 4px; +} + +QScrollBar::handle:vertical { + background-color: #4B5362; + min-height: 5px; + border-radius: 4px; +} + +QScrollBar::sub-line:vertical { + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/up_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + +QScrollBar::add-line:vertical { + margin: 3px 0px 3px 0px; + border-image: url(:/qss_icons/rc/down_arrow_disabled.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical:hover,QScrollBar::sub-line:vertical:on { + + border-image: url(:/qss_icons/rc/up_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: top; + subcontrol-origin: margin; +} + + +QScrollBar::add-line:vertical:hover, QScrollBar::add-line:vertical:on { + border-image: url(:/qss_icons/rc/down_arrow.png); + height: 10px; + width: 10px; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical { + background: none; +} + + +QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; +} + +#MainLabel { + color: rgb(200, 200, 200); + font-size: 12pt; +} + +#Console { + background-color: #21252B; + color: rgb(72, 200, 150); + font-family: "Roboto Mono"; + font-size: 8pt; +} + +#ExitBtn { + /* `border` must be set to background of flat button is painted .*/ + border: none; + color: rgb(39, 39, 39); + background-color: #828a97; + padding: 0.5em; + font-weight: 400; +} + +#ExitBtn:hover{ + background-color: #b2bece +} +#ExitBtn:disabled { + background-color: rgba(185, 185, 185, 31); + color: rgba(64, 64, 64, 63); +} + +#ButtonWithOptions QPushButton{ + border-top-right-radius: 0px; + border-bottom-right-radius: 0px; + border: none; + background-color: rgb(84, 209, 178); + color: rgb(39, 39, 39); + font-weight: 400; + padding: 0.5em; +} +#ButtonWithOptions QPushButton:hover{ + background-color: rgb(85, 224, 189) +} +#ButtonWithOptions QPushButton:disabled { + background-color: rgba(72, 200, 150, 31); + color: rgba(64, 64, 64, 63); +} + +#ButtonWithOptions QToolButton{ + border: none; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + background-color: rgb(84, 209, 178); + color: rgb(39, 39, 39); +} +#ButtonWithOptions QToolButton:hover{ + background-color: rgb(85, 224, 189) +} +#ButtonWithOptions QToolButton:disabled { + background-color: rgba(72, 200, 150, 31); + color: rgba(64, 64, 64, 63); +} diff --git a/igniter/tools.py b/igniter/tools.py index 368e9a2b3d..529d535c25 100644 --- a/igniter/tools.py +++ b/igniter/tools.py @@ -14,7 +14,12 @@ from pathlib import Path import platform from pymongo import MongoClient -from pymongo.errors import ServerSelectionTimeoutError, InvalidURI +from pymongo.errors import ( + ServerSelectionTimeoutError, + InvalidURI, + ConfigurationError, + OperationFailure +) def decompose_url(url: str) -> Dict: @@ -115,30 +120,20 @@ def validate_mongo_connection(cnx: str) -> (bool, str): parsed = urlparse(cnx) if parsed.scheme not in ["mongodb", "mongodb+srv"]: return False, "Not mongodb schema" - # we have mongo connection string. Let's try if we can connect. - try: - components = decompose_url(cnx) - except RuntimeError: - return False, f"Invalid port specified." - - mongo_args = { - "host": compose_url(**components), - "serverSelectionTimeoutMS": 2000 - } - port = components.get("port") - if port is not None: - mongo_args["port"] = int(port) try: - client = MongoClient(cnx) + client = MongoClient( + cnx, + serverSelectionTimeoutMS=2000 + ) client.server_info() client.close() except ServerSelectionTimeoutError as e: return False, f"Cannot connect to server {cnx} - {e}" except ValueError: return False, f"Invalid port specified {parsed.port}" - except InvalidURI as e: - return False, str(e) + except (ConfigurationError, OperationFailure, InvalidURI) as exc: + return False, str(exc) else: return True, "Connection is successful" diff --git a/openpype/__init__.py b/openpype/__init__.py index edd48a018d..f63d534e08 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -9,6 +9,7 @@ from .settings import get_project_settings from .lib import ( Anatomy, filter_pyblish_plugins, + set_plugin_attributes_from_settings, change_timer_to_current_context ) @@ -58,38 +59,8 @@ def patched_discover(superclass): # run original discover and get plugins plugins = _original_discover(superclass) - # determine host application to use for finding presets - if avalon.registered_host() is None: - return plugins - host = avalon.registered_host().__name__.split(".")[-1] + set_plugin_attributes_from_settings(plugins, superclass) - # map plugin superclass to preset json. Currenly suppoted is load and - # create (avalon.api.Loader and avalon.api.Creator) - plugin_type = "undefined" - if superclass.__name__.split(".")[-1] == "Loader": - plugin_type = "load" - elif superclass.__name__.split(".")[-1] == "Creator": - plugin_type = "create" - - print(">>> Finding presets for {}:{} ...".format(host, plugin_type)) - try: - settings = ( - get_project_settings(os.environ['AVALON_PROJECT']) - [host][plugin_type] - ) - except KeyError: - print("*** no presets found.") - else: - for plugin in plugins: - if plugin.__name__ in settings: - print(">>> We have preset for {}".format(plugin.__name__)) - for option, value in settings[plugin.__name__].items(): - if option == "enabled" and value is False: - setattr(plugin, "active", False) - print(" - is disabled by preset") - else: - setattr(plugin, option, value) - print(" - setting `{}`: `{}`".format(option, value)) return plugins diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 895d11601f..1df89dbb21 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -113,6 +113,7 @@ from .plugin_tools import ( TaskNotSetError, get_subset_name, filter_pyblish_plugins, + set_plugin_attributes_from_settings, source_hash, get_unique_layer_name, get_background_layers, @@ -207,6 +208,7 @@ __all__ = [ "TaskNotSetError", "get_subset_name", "filter_pyblish_plugins", + "set_plugin_attributes_from_settings", "source_hash", "get_unique_layer_name", "get_background_layers", diff --git a/openpype/lib/plugin_tools.py b/openpype/lib/plugin_tools.py index 9a2d30d1a7..44c688456e 100644 --- a/openpype/lib/plugin_tools.py +++ b/openpype/lib/plugin_tools.py @@ -150,6 +150,95 @@ def filter_pyblish_plugins(plugins): setattr(plugin, option, value) +def set_plugin_attributes_from_settings( + plugins, superclass, host_name=None, project_name=None +): + """Change attribute values on Avalon plugins by project settings. + + This function should be used only in host context. Modify + behavior of plugins. + + Args: + plugins (list): Plugins discovered by origin avalon discover method. + superclass (object): Superclass of plugin type (e.g. Cretor, Loader). + host_name (str): Name of host for which plugins are loaded and from. + Value from environment `AVALON_APP` is used if not entered. + project_name (str): Name of project for which settings will be loaded. + Value from environment `AVALON_PROJECT` is used if not entered. + """ + + # determine host application to use for finding presets + if host_name is None: + host_name = os.environ.get("AVALON_APP") + + if project_name is None: + project_name = os.environ.get("AVALON_PROJECT") + + # map plugin superclass to preset json. Currenly suppoted is load and + # create (avalon.api.Loader and avalon.api.Creator) + plugin_type = None + if superclass.__name__.split(".")[-1] == "Loader": + plugin_type = "load" + elif superclass.__name__.split(".")[-1] == "Creator": + plugin_type = "create" + + if not host_name or not project_name or plugin_type is None: + msg = "Skipped attributes override from settings." + if not host_name: + msg += " Host name is not defined." + + if not project_name: + msg += " Project name is not defined." + + if plugin_type is None: + msg += " Plugin type is unsupported for class {}.".format( + superclass.__name__ + ) + + print(msg) + return + + print(">>> Finding presets for {}:{} ...".format(host_name, plugin_type)) + + project_settings = get_project_settings(project_name) + plugin_type_settings = ( + project_settings + .get(host_name, {}) + .get(plugin_type, {}) + ) + global_type_settings = ( + project_settings + .get("global", {}) + .get(plugin_type, {}) + ) + if not global_type_settings and not plugin_type_settings: + return + + for plugin in plugins: + plugin_name = plugin.__name__ + + plugin_settings = None + # Look for plugin settings in host specific settings + if plugin_name in plugin_type_settings: + plugin_settings = plugin_type_settings[plugin_name] + + # Look for plugin settings in global settings + elif plugin_name in global_type_settings: + plugin_settings = global_type_settings[plugin_name] + + if not plugin_settings: + continue + + print(">>> We have preset for {}".format(plugin_name)) + for option, value in plugin_settings.items(): + if option == "enabled" and value is False: + setattr(plugin, "active", False) + print(" - is disabled by preset") + else: + setattr(plugin, option, value) + print(" - setting `{}`: `{}`".format(option, value)) + + def source_hash(filepath, *args): """Generate simple identifier for a source file. This is used to identify whether a source file has previously been diff --git a/openpype/tools/settings/settings/widgets/dict_mutable_widget.py b/openpype/tools/settings/settings/widgets/dict_mutable_widget.py index 9bea89c0d6..ff4905c480 100644 --- a/openpype/tools/settings/settings/widgets/dict_mutable_widget.py +++ b/openpype/tools/settings/settings/widgets/dict_mutable_widget.py @@ -36,6 +36,7 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): super(ModifiableDictEmptyItem, self).__init__(parent) self.entity_widget = entity_widget self.collapsible_key = entity_widget.entity.collapsible_key + self.ignore_input_changes = entity_widget.ignore_input_changes self.is_duplicated = False self.key_is_valid = False @@ -101,6 +102,10 @@ class ModifiableDictEmptyItem(QtWidgets.QWidget): def _on_key_change(self): key = self.key_input.text() self.key_is_valid = KEY_REGEX.match(key) + + if self.ignore_input_changes: + return + self.is_duplicated = self.entity_widget.is_key_duplicated(key) key_input_state = "" # Collapsible key and empty key are not invalid @@ -355,6 +360,7 @@ class ModifiableDictItem(QtWidgets.QWidget): def set_label(self, label): if self.key_label_input and label is not None: self.key_label_input.setText(label) + self.update_key_label() def set_as_required(self, key): self.key_input.setText(key) @@ -386,6 +392,9 @@ class ModifiableDictItem(QtWidgets.QWidget): self.set_edit_mode(False) def _on_key_label_change(self): + if self.ignore_input_changes: + return + label = self.key_label_value() self.entity_widget.change_label(label, self) self.update_key_label() @@ -393,6 +402,10 @@ class ModifiableDictItem(QtWidgets.QWidget): def _on_key_change(self): key = self.key_value() self.key_is_valid = KEY_REGEX.match(key) + + if self.ignore_input_changes: + return + is_key_duplicated = self.entity_widget.validate_key_duplication( self.temp_key, key, self ) @@ -422,7 +435,7 @@ class ModifiableDictItem(QtWidgets.QWidget): self.wrapper_widget.label_widget.setText(label) def on_add_clicked(self): - widget = self.entity_widget.add_new_key(None, None, self) + widget = self.entity_widget.add_new_key(None, None) widget.key_input.setFocus(True) def on_edit_pressed(self): @@ -621,7 +634,7 @@ class DictMutableKeysWidget(BaseWidget): # TODO implement pass - def add_new_key(self, key, label=None, after_widget=None): + def add_new_key(self, key, label=None): uuid_key = None entity_key = key if not key: @@ -641,7 +654,7 @@ class DictMutableKeysWidget(BaseWidget): # Backup solution (for testing) if input_field is None: - input_field = self.add_widget_for_child(child_entity, after_widget) + input_field = self.add_widget_for_child(child_entity) if key: # Happens when created from collapsible key items where key @@ -719,29 +732,16 @@ class DictMutableKeysWidget(BaseWidget): return self.entity.set_child_label(entity, label) - def add_widget_for_child( - self, child_entity, after_widget=None, first=False - ): - if first: - new_widget_index = 0 - else: - new_widget_index = len(self.input_fields) - - if self.input_fields and not first: - if not after_widget: - after_widget = self.input_fields[-1] - - for idx in range(self.content_layout.count()): - item = self.content_layout.itemAt(idx) - if item.widget() is after_widget: - new_widget_index = idx + 1 - break - + def add_widget_for_child(self, child_entity): input_field = ModifiableDictItem( self.entity.collapsible_key, child_entity, self ) self.input_fields.append(input_field) + + new_widget_index = self.content_layout.count() - 1 + self.content_layout.insertWidget(new_widget_index, input_field) + return input_field def remove_row(self, widget): @@ -810,21 +810,15 @@ class DictMutableKeysWidget(BaseWidget): for key, child_entity in self.entity.items(): found = False - previous_input = None for input_field in self.input_fields: - if input_field.entity is not child_entity: - previous_input = input_field - else: + if input_field.entity is child_entity: found = True break if not found: changed = True - args = [previous_input] - if previous_input is None: - args.append(True) - _input_field = self.add_widget_for_child(child_entity, *args) + _input_field = self.add_widget_for_child(child_entity) _input_field.origin_key = key _input_field.set_key(key) if self.entity.collapsible_key: @@ -855,9 +849,8 @@ class DictMutableKeysWidget(BaseWidget): if keys_order: last_required = keys_order[-1] for key in self.entity.keys(): - if key in keys_order: - continue - keys_order.append(key) + if key not in keys_order: + keys_order.append(key) for key in keys_order: child_entity = self.entity[key] diff --git a/start.py b/start.py index 05069862bf..0295d0ca62 100644 --- a/start.py +++ b/start.py @@ -289,6 +289,10 @@ def _process_arguments() -> tuple: if return_code not in [2, 3]: sys.exit(return_code) + idx = sys.argv.index("igniter") + sys.argv.pop(idx) + sys.argv.insert(idx, "tray") + return use_version, use_staging