diff --git a/openpype/lib/log.py b/openpype/lib/log.py index 39b6c67080..85cbc733ba 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -72,6 +72,8 @@ class PypeStreamHandler(logging.StreamHandler): msg = self.format(record) msg = Terminal.log(msg) stream = self.stream + if stream is None: + return fs = "%s\n" # if no unicode support... if not USE_UNICODE: diff --git a/openpype/modules/__init__.py b/openpype/modules/__init__.py index d6fb9c0aef..068aeb98af 100644 --- a/openpype/modules/__init__.py +++ b/openpype/modules/__init__.py @@ -38,6 +38,7 @@ from .muster import MusterModule from .deadline import DeadlineModule from .project_manager_action import ProjectManagerAction from .standalonepublish_action import StandAlonePublishAction +from .python_console_interpreter import PythonInterpreterAction from .sync_server import SyncServerModule from .slack import SlackIntegrationModule @@ -77,6 +78,7 @@ __all__ = ( "DeadlineModule", "ProjectManagerAction", "StandAlonePublishAction", + "PythonInterpreterAction", "SyncServerModule", diff --git a/openpype/modules/python_console_interpreter/__init__.py b/openpype/modules/python_console_interpreter/__init__.py new file mode 100644 index 0000000000..5f54ac497b --- /dev/null +++ b/openpype/modules/python_console_interpreter/__init__.py @@ -0,0 +1,8 @@ +from .module import ( + PythonInterpreterAction +) + + +__all__ = ( + "PythonInterpreterAction", +) diff --git a/openpype/modules/python_console_interpreter/module.py b/openpype/modules/python_console_interpreter/module.py new file mode 100644 index 0000000000..b37f35dfe0 --- /dev/null +++ b/openpype/modules/python_console_interpreter/module.py @@ -0,0 +1,45 @@ +from .. import PypeModule, ITrayAction + + +class PythonInterpreterAction(PypeModule, ITrayAction): + label = "Console" + name = "python_interpreter" + admin_action = True + + def initialize(self, modules_settings): + self.enabled = True + self._interpreter_window = None + + def tray_init(self): + self.create_interpreter_window() + + def tray_exit(self): + if self._interpreter_window is not None: + self._interpreter_window.save_registry() + + def connect_with_modules(self, *args, **kwargs): + pass + + def create_interpreter_window(self): + """Initializa Settings Qt window.""" + if self._interpreter_window: + return + + from openpype.modules.python_console_interpreter.window import ( + PythonInterpreterWidget + ) + + self._interpreter_window = PythonInterpreterWidget() + + def on_action_trigger(self): + self.show_interpreter_window() + + def show_interpreter_window(self): + self.create_interpreter_window() + + if self._interpreter_window.isVisible(): + self._interpreter_window.activateWindow() + self._interpreter_window.raise_() + return + + self._interpreter_window.show() diff --git a/openpype/modules/python_console_interpreter/window/__init__.py b/openpype/modules/python_console_interpreter/window/__init__.py new file mode 100644 index 0000000000..92fd6f1df2 --- /dev/null +++ b/openpype/modules/python_console_interpreter/window/__init__.py @@ -0,0 +1,8 @@ +from .widgets import ( + PythonInterpreterWidget +) + + +__all__ = ( + "PythonInterpreterWidget", +) diff --git a/openpype/modules/python_console_interpreter/window/widgets.py b/openpype/modules/python_console_interpreter/window/widgets.py new file mode 100644 index 0000000000..975decf4f4 --- /dev/null +++ b/openpype/modules/python_console_interpreter/window/widgets.py @@ -0,0 +1,583 @@ +import os +import re +import sys +import collections +from code import InteractiveInterpreter + +import appdirs +from Qt import QtCore, QtWidgets, QtGui + +from openpype import resources +from openpype.style import load_stylesheet +from openpype.lib import JSONSettingRegistry + + +openpype_art = """ + . . .. . .. + _oOOP3OPP3Op_. . + .PPpo~. .. ~2p. .. .... . . + .Ppo . .pPO3Op.. . O:. . . . + .3Pp . oP3'. 'P33. . 4 .. . . . .. . . . + .~OP 3PO. .Op3 : . .. _____ _____ _____ + .P3O . oP3oP3O3P' . . . . / /./ /./ / + O3:. O3p~ . .:. . ./____/./____/ /____/ + 'P . 3p3. oP3~. ..P:. . . .. . . .. . . . + . ': . Po' .Opo'. .3O. . o[ by Pype Club ]]]==- - - . . + . '_ .. . . _OP3.. . .https://openpype.io.. . + ~P3.OPPPO3OP~ . .. . + . ' '. . .. . . . .. . + + +""" + + +class PythonInterpreterRegistry(JSONSettingRegistry): + """Class handling OpenPype general settings registry. + + Attributes: + vendor (str): Name used for path construction. + product (str): Additional name used for path construction. + + """ + + def __init__(self): + self.vendor = "pypeclub" + self.product = "openpype" + name = "python_interpreter_tool" + path = appdirs.user_data_dir(self.product, self.vendor) + super(PythonInterpreterRegistry, self).__init__(name, path) + + +class StdOEWrap: + def __init__(self): + self._origin_stdout_write = None + self._origin_stderr_write = None + self._listening = False + self.lines = collections.deque() + + if not sys.stdout: + sys.stdout = open(os.devnull, "w") + + if not sys.stderr: + sys.stderr = open(os.devnull, "w") + + if self._origin_stdout_write is None: + self._origin_stdout_write = sys.stdout.write + + if self._origin_stderr_write is None: + self._origin_stderr_write = sys.stderr.write + + self._listening = True + sys.stdout.write = self._stdout_listener + sys.stderr.write = self._stderr_listener + + def stop_listen(self): + self._listening = False + + def _stdout_listener(self, text): + if self._listening: + self.lines.append(text) + if self._origin_stdout_write is not None: + self._origin_stdout_write(text) + + def _stderr_listener(self, text): + if self._listening: + self.lines.append(text) + if self._origin_stderr_write is not None: + self._origin_stderr_write(text) + + +class PythonCodeEditor(QtWidgets.QPlainTextEdit): + execute_requested = QtCore.Signal() + + def __init__(self, parent): + super(PythonCodeEditor, self).__init__(parent) + + self.setObjectName("PythonCodeEditor") + + self._indent = 4 + + def _tab_shift_right(self): + cursor = self.textCursor() + selected_text = cursor.selectedText() + if not selected_text: + cursor.insertText(" " * self._indent) + return + + sel_start = cursor.selectionStart() + sel_end = cursor.selectionEnd() + cursor.setPosition(sel_end) + end_line = cursor.blockNumber() + cursor.setPosition(sel_start) + while True: + cursor.movePosition(QtGui.QTextCursor.StartOfLine) + text = cursor.block().text() + spaces = len(text) - len(text.lstrip(" ")) + new_spaces = spaces % self._indent + if not new_spaces: + new_spaces = self._indent + + cursor.insertText(" " * new_spaces) + if cursor.blockNumber() == end_line: + break + + cursor.movePosition(QtGui.QTextCursor.NextBlock) + + def _tab_shift_left(self): + tmp_cursor = self.textCursor() + sel_start = tmp_cursor.selectionStart() + sel_end = tmp_cursor.selectionEnd() + + cursor = QtGui.QTextCursor(self.document()) + cursor.setPosition(sel_end) + end_line = cursor.blockNumber() + cursor.setPosition(sel_start) + while True: + cursor.movePosition(QtGui.QTextCursor.StartOfLine) + text = cursor.block().text() + spaces = len(text) - len(text.lstrip(" ")) + if spaces: + spaces_to_remove = (spaces % self._indent) or self._indent + if spaces_to_remove > spaces: + spaces_to_remove = spaces + + cursor.setPosition( + cursor.position() + spaces_to_remove, + QtGui.QTextCursor.KeepAnchor + ) + cursor.removeSelectedText() + + if cursor.blockNumber() == end_line: + break + + cursor.movePosition(QtGui.QTextCursor.NextBlock) + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Backtab: + self._tab_shift_left() + event.accept() + return + + if event.key() == QtCore.Qt.Key_Tab: + if event.modifiers() == QtCore.Qt.NoModifier: + self._tab_shift_right() + event.accept() + return + + if ( + event.key() == QtCore.Qt.Key_Return + and event.modifiers() == QtCore.Qt.ControlModifier + ): + self.execute_requested.emit() + event.accept() + return + + super(PythonCodeEditor, self).keyPressEvent(event) + + +class PythonTabWidget(QtWidgets.QWidget): + before_execute = QtCore.Signal(str) + + def __init__(self, parent): + super(PythonTabWidget, self).__init__(parent) + + code_input = PythonCodeEditor(self) + + self.setFocusProxy(code_input) + + execute_btn = QtWidgets.QPushButton("Execute", self) + execute_btn.setToolTip("Execute command (Ctrl + Enter)") + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(execute_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(code_input, 1) + layout.addLayout(btns_layout, 0) + + execute_btn.clicked.connect(self._on_execute_clicked) + code_input.execute_requested.connect(self.execute) + + self._code_input = code_input + self._interpreter = InteractiveInterpreter() + + def _on_execute_clicked(self): + self.execute() + + def get_code(self): + return self._code_input.toPlainText() + + def set_code(self, code_text): + self._code_input.setPlainText(code_text) + + def execute(self): + code_text = self._code_input.toPlainText() + self.before_execute.emit(code_text) + self._interpreter.runcode(code_text) + + +class TabNameDialog(QtWidgets.QDialog): + default_width = 330 + default_height = 85 + + def __init__(self, parent): + super(TabNameDialog, self).__init__(parent) + + self.setWindowTitle("Enter tab name") + + name_label = QtWidgets.QLabel("Tab name:", self) + name_input = QtWidgets.QLineEdit(self) + + inputs_layout = QtWidgets.QHBoxLayout() + inputs_layout.addWidget(name_label) + inputs_layout.addWidget(name_input) + + ok_btn = QtWidgets.QPushButton("Ok", self) + cancel_btn = QtWidgets.QPushButton("Cancel", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + btns_layout.addWidget(cancel_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addLayout(inputs_layout) + layout.addStretch(1) + layout.addLayout(btns_layout) + + ok_btn.clicked.connect(self._on_ok_clicked) + cancel_btn.clicked.connect(self._on_cancel_clicked) + + self._name_input = name_input + self._ok_btn = ok_btn + self._cancel_btn = cancel_btn + + self._result = None + + self.resize(self.default_width, self.default_height) + + def set_tab_name(self, name): + self._name_input.setText(name) + + def result(self): + return self._result + + def showEvent(self, event): + super(TabNameDialog, self).showEvent(event) + btns_width = max( + self._ok_btn.width(), + self._cancel_btn.width() + ) + + self._ok_btn.setMinimumWidth(btns_width) + self._cancel_btn.setMinimumWidth(btns_width) + + def _on_ok_clicked(self): + self._result = self._name_input.text() + self.accept() + + def _on_cancel_clicked(self): + self._result = None + self.reject() + + +class OutputTextWidget(QtWidgets.QTextEdit): + v_max_offset = 4 + + def vertical_scroll_at_max(self): + v_scroll = self.verticalScrollBar() + return v_scroll.value() > v_scroll.maximum() - self.v_max_offset + + def scroll_to_bottom(self): + v_scroll = self.verticalScrollBar() + return v_scroll.setValue(v_scroll.maximum()) + + +class EnhancedTabBar(QtWidgets.QTabBar): + double_clicked = QtCore.Signal(QtCore.QPoint) + right_clicked = QtCore.Signal(QtCore.QPoint) + mid_clicked = QtCore.Signal(QtCore.QPoint) + + def __init__(self, parent): + super(EnhancedTabBar, self).__init__(parent) + + self.setDrawBase(False) + + def mouseDoubleClickEvent(self, event): + self.double_clicked.emit(event.globalPos()) + event.accept() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.right_clicked.emit(event.globalPos()) + event.accept() + return + + elif event.button() == QtCore.Qt.MidButton: + self.mid_clicked.emit(event.globalPos()) + event.accept() + + else: + super(EnhancedTabBar, self).mouseReleaseEvent(event) + + +class PythonInterpreterWidget(QtWidgets.QWidget): + default_width = 1000 + default_height = 600 + + def __init__(self, parent=None): + super(PythonInterpreterWidget, self).__init__(parent) + + self.setWindowTitle("OpenPype Console") + self.setWindowIcon(QtGui.QIcon(resources.pype_icon_filepath())) + + self.ansi_escape = re.compile( + r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]" + ) + + self._tabs = [] + + self._stdout_err_wrapper = StdOEWrap() + + output_widget = OutputTextWidget(self) + output_widget.setObjectName("PythonInterpreterOutput") + output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) + output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + + tab_widget = QtWidgets.QTabWidget(self) + tab_bar = EnhancedTabBar(tab_widget) + tab_widget.setTabBar(tab_bar) + tab_widget.setTabsClosable(False) + tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + add_tab_btn = QtWidgets.QPushButton("+", tab_widget) + tab_widget.setCornerWidget(add_tab_btn, QtCore.Qt.TopLeftCorner) + + widgets_splitter = QtWidgets.QSplitter(self) + widgets_splitter.setOrientation(QtCore.Qt.Vertical) + widgets_splitter.addWidget(output_widget) + widgets_splitter.addWidget(tab_widget) + widgets_splitter.setStretchFactor(0, 1) + widgets_splitter.setStretchFactor(1, 1) + height = int(self.default_height / 2) + widgets_splitter.setSizes([height, self.default_height - height]) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(widgets_splitter) + + line_check_timer = QtCore.QTimer() + line_check_timer.setInterval(200) + + line_check_timer.timeout.connect(self._on_timer_timeout) + add_tab_btn.clicked.connect(self._on_add_clicked) + tab_bar.right_clicked.connect(self._on_tab_right_click) + tab_bar.double_clicked.connect(self._on_tab_double_click) + tab_bar.mid_clicked.connect(self._on_tab_mid_click) + tab_widget.tabCloseRequested.connect(self._on_tab_close_req) + + self._widgets_splitter = widgets_splitter + self._add_tab_btn = add_tab_btn + self._output_widget = output_widget + self._tab_widget = tab_widget + self._line_check_timer = line_check_timer + + self._append_lines([openpype_art]) + + self.setStyleSheet(load_stylesheet()) + + self.resize(self.default_width, self.default_height) + + self._init_from_registry() + + if self._tab_widget.count() < 1: + self.add_tab("Python") + + def _init_from_registry(self): + setting_registry = PythonInterpreterRegistry() + + try: + width = setting_registry.get_item("width") + height = setting_registry.get_item("height") + if width is not None and height is not None: + self.resize(width, height) + + except ValueError: + pass + + try: + sizes = setting_registry.get_item("splitter_sizes") + if len(sizes) == len(self._widgets_splitter.sizes()): + self._widgets_splitter.setSizes(sizes) + + except ValueError: + pass + + try: + tab_defs = setting_registry.get_item("tabs") or [] + for tab_def in tab_defs: + widget = self.add_tab(tab_def["name"]) + widget.set_code(tab_def["code"]) + + except ValueError: + pass + + def save_registry(self): + setting_registry = PythonInterpreterRegistry() + + setting_registry.set_item("width", self.width()) + setting_registry.set_item("height", self.height()) + + setting_registry.set_item( + "splitter_sizes", self._widgets_splitter.sizes() + ) + + tabs = [] + for tab_idx in range(self._tab_widget.count()): + widget = self._tab_widget.widget(tab_idx) + tab_code = widget.get_code() + tab_name = self._tab_widget.tabText(tab_idx) + tabs.append({ + "name": tab_name, + "code": tab_code + }) + + setting_registry.set_item("tabs", tabs) + + def _on_tab_right_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + menu = QtWidgets.QMenu(self._tab_widget) + menu.addAction("Rename") + result = menu.exec_(global_point) + if result is None: + return + + if result.text() == "Rename": + self._rename_tab_req(tab_idx) + + def _rename_tab_req(self, tab_idx): + dialog = TabNameDialog(self) + dialog.set_tab_name(self._tab_widget.tabText(tab_idx)) + dialog.exec_() + tab_name = dialog.result() + if tab_name: + self._tab_widget.setTabText(tab_idx, tab_name) + + def _on_tab_mid_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + self._on_tab_close_req(tab_idx) + + def _on_tab_double_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + self._rename_tab_req(tab_idx) + + def _on_tab_close_req(self, tab_index): + if self._tab_widget.count() == 1: + return + + widget = self._tab_widget.widget(tab_index) + if widget in self._tabs: + self._tabs.remove(widget) + self._tab_widget.removeTab(tab_index) + + if self._tab_widget.count() == 1: + self._tab_widget.setTabsClosable(False) + + def _append_lines(self, lines): + at_max = self._output_widget.vertical_scroll_at_max() + tmp_cursor = QtGui.QTextCursor(self._output_widget.document()) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + for line in lines: + tmp_cursor.insertText(line) + + if at_max: + self._output_widget.scroll_to_bottom() + + def _on_timer_timeout(self): + if self._stdout_err_wrapper.lines: + lines = [] + while self._stdout_err_wrapper.lines: + line = self._stdout_err_wrapper.lines.popleft() + lines.append(self.ansi_escape.sub("", line)) + self._append_lines(lines) + + def _on_add_clicked(self): + dialog = TabNameDialog(self) + dialog.exec_() + tab_name = dialog.result() + if tab_name: + self.add_tab(tab_name) + + def _on_before_execute(self, code_text): + at_max = self._output_widget.vertical_scroll_at_max() + document = self._output_widget.document() + tmp_cursor = QtGui.QTextCursor(document) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + tmp_cursor.insertText("{}\nExecuting command:\n".format(20 * "-")) + + code_block_format = QtGui.QTextFrameFormat() + code_block_format.setBackground(QtGui.QColor(27, 27, 27)) + code_block_format.setPadding(4) + + tmp_cursor.insertFrame(code_block_format) + char_format = tmp_cursor.charFormat() + char_format.setForeground( + QtGui.QBrush(QtGui.QColor(114, 224, 198)) + ) + tmp_cursor.setCharFormat(char_format) + tmp_cursor.insertText(code_text) + + # Create new cursor + tmp_cursor = QtGui.QTextCursor(document) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + tmp_cursor.insertText("{}\n".format(20 * "-")) + + if at_max: + self._output_widget.scroll_to_bottom() + + def add_tab(self, tab_name, index=None): + widget = PythonTabWidget(self) + widget.before_execute.connect(self._on_before_execute) + if index is None: + if self._tab_widget.count() > 0: + index = self._tab_widget.currentIndex() + 1 + else: + index = 0 + + self._tabs.append(widget) + self._tab_widget.insertTab(index, widget, tab_name) + self._tab_widget.setCurrentIndex(index) + + if self._tab_widget.count() > 1: + self._tab_widget.setTabsClosable(True) + widget.setFocus() + return widget + + def showEvent(self, event): + self._line_check_timer.start() + super(PythonInterpreterWidget, self).showEvent(event) + self._output_widget.scroll_to_bottom() + + def closeEvent(self, event): + self.save_registry() + super(PythonInterpreterWidget, self).closeEvent(event) + self._line_check_timer.stop() diff --git a/openpype/style/__init__.py b/openpype/style/__init__.py index 89a210bee9..87547b1a90 100644 --- a/openpype/style/__init__.py +++ b/openpype/style/__init__.py @@ -65,6 +65,7 @@ def _load_font(): font_dirs = [] font_dirs.append(os.path.join(fonts_dirpath, "Montserrat")) font_dirs.append(os.path.join(fonts_dirpath, "Spartan")) + font_dirs.append(os.path.join(fonts_dirpath, "RobotoMono", "static")) loaded_fonts = [] for font_dir in font_dirs: diff --git a/openpype/style/fonts/RobotoMono/LICENSE.txt b/openpype/style/fonts/RobotoMono/LICENSE.txt new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/openpype/style/fonts/RobotoMono/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/openpype/style/fonts/RobotoMono/README.txt b/openpype/style/fonts/RobotoMono/README.txt new file mode 100644 index 0000000000..1bc1b1cfa2 --- /dev/null +++ b/openpype/style/fonts/RobotoMono/README.txt @@ -0,0 +1,77 @@ +Roboto Mono Variable Font +========================= + +This download contains Roboto Mono as both variable fonts and static fonts. + +Roboto Mono is a variable font with this axis: + wght + +This means all the styles are contained in these files: + RobotoMono-VariableFont_wght.ttf + RobotoMono-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Roboto Mono: + static/RobotoMono-Thin.ttf + static/RobotoMono-ExtraLight.ttf + static/RobotoMono-Light.ttf + static/RobotoMono-Regular.ttf + static/RobotoMono-Medium.ttf + static/RobotoMono-SemiBold.ttf + static/RobotoMono-Bold.ttf + static/RobotoMono-ThinItalic.ttf + static/RobotoMono-ExtraLightItalic.ttf + static/RobotoMono-LightItalic.ttf + static/RobotoMono-Italic.ttf + static/RobotoMono-MediumItalic.ttf + static/RobotoMono-SemiBoldItalic.ttf + static/RobotoMono-BoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (LICENSE.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them freely in your products & projects - print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/openpype/style/fonts/RobotoMono/RobotoMono-Italic-VariableFont_wght.ttf b/openpype/style/fonts/RobotoMono/RobotoMono-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000000..d30055a9e8 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/RobotoMono-Italic-VariableFont_wght.ttf differ diff --git a/openpype/style/fonts/RobotoMono/RobotoMono-VariableFont_wght.ttf b/openpype/style/fonts/RobotoMono/RobotoMono-VariableFont_wght.ttf new file mode 100644 index 0000000000..d2b4746196 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/RobotoMono-VariableFont_wght.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Bold.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Bold.ttf new file mode 100644 index 0000000000..900fce6848 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-Bold.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-BoldItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-BoldItalic.ttf new file mode 100644 index 0000000000..4bfe29ae89 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-BoldItalic.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLight.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLight.ttf new file mode 100644 index 0000000000..d535884553 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLight.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLightItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLightItalic.ttf new file mode 100644 index 0000000000..b28960a0ee Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-ExtraLightItalic.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Italic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Italic.ttf new file mode 100644 index 0000000000..4ee4dc49b4 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-Italic.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Light.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Light.ttf new file mode 100644 index 0000000000..276af4c55a Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-Light.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-LightItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-LightItalic.ttf new file mode 100644 index 0000000000..a2801c2168 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-LightItalic.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Medium.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Medium.ttf new file mode 100644 index 0000000000..8461be77a3 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-Medium.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-MediumItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-MediumItalic.ttf new file mode 100644 index 0000000000..a3bfaa115a Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-MediumItalic.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Regular.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Regular.ttf new file mode 100644 index 0000000000..7c4ce36a44 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-Regular.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBold.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBold.ttf new file mode 100644 index 0000000000..15ee6c6e40 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBold.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBoldItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBoldItalic.ttf new file mode 100644 index 0000000000..8e21497793 Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-SemiBoldItalic.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-Thin.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-Thin.ttf new file mode 100644 index 0000000000..ee8a3fd41a Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-Thin.ttf differ diff --git a/openpype/style/fonts/RobotoMono/static/RobotoMono-ThinItalic.ttf b/openpype/style/fonts/RobotoMono/static/RobotoMono-ThinItalic.ttf new file mode 100644 index 0000000000..40b01e40de Binary files /dev/null and b/openpype/style/fonts/RobotoMono/static/RobotoMono-ThinItalic.ttf differ diff --git a/openpype/style/style.css b/openpype/style/style.css index b955bdc2a6..830ed85f9b 100644 --- a/openpype/style/style.css +++ b/openpype/style/style.css @@ -271,37 +271,38 @@ QTabWidget::tab-bar { } QTabBar::tab { - border-top-left-radius: 4px; - border-top-right-radius: 4px; padding: 5px; - + border-left: 3px solid transparent; + border-top: 1px solid {color:border}; + border-right: 1px solid {color:border}; + background: qlineargradient( + x1: 0, y1: 1, x2: 0, y2: 0, + stop: 0.5 {color:bg}, stop: 1.0 {color:bg-inputs} + ); } QTabBar::tab:selected { background: {color:grey-lighter}; - /* background: qradialgradient( - cx:0.5, cy:0.5, radius: 2, - fx:0.5, fy:1, - stop:0.3 {color:bg}, stop:1 white - ) */ - /* background: qlineargradient( - x1: 0, y1: 0, x2: 0, y2: 1, - stop: 0 {color:bg-inputs}, stop: 1.0 {color:bg} - ); */ + border-left: 3px solid {color:border-focus}; + background: qlineargradient( + x1: 0, y1: 1, x2: 0, y2: 0, + stop: 0.5 {color:bg}, stop: 1.0 {color:border} + ); } QTabBar::tab:!selected { - /* Make it smaller*/ - margin-top: 3px; background: {color:grey-light}; } QTabBar::tab:!selected:hover { background: {color:grey-lighter}; } - +QTabBar::tab:first { + border-left: 1px solid {color:border}; +} QTabBar::tab:first:selected { margin-left: 0; + border-left: 3px solid {color:border-focus}; } QTabBar::tab:last:selected { @@ -623,3 +624,8 @@ QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { border: 1px solid {color:border}; border-radius: 0.1em; } + +/* Python console interpreter */ +#PythonInterpreterOutput, #PythonCodeEditor { + font-family: "Roboto Mono"; +}