diff --git a/Ryven/Ryven.py b/Ryven/Ryven.py index d8b5b7cc..e3121c09 100644 --- a/Ryven/Ryven.py +++ b/Ryven/Ryven.py @@ -1,9 +1,11 @@ import os import sys +import custom_src.Console.MainConsole as MainConsole from custom_src.startup_dialog.StartupDialog import StartupDialog from custom_src.MainWindow import MainWindow from PySide2.QtWidgets import QApplication +from contextlib import redirect_stdout, redirect_stderr if __name__ == '__main__': os.chdir(os.path.dirname(os.path.realpath(__file__))) @@ -13,7 +15,23 @@ sw.exec_() if not sw.editor_startup_configuration == {}: - mw = MainWindow(sw.editor_startup_configuration) - mw.show() - sys.exit(app.exec_()) \ No newline at end of file + if MainConsole.main_console_enabled: + # initialize console + MainConsole.init_main_console() + console_stdout_redirect = MainConsole.RedirectOutput(MainConsole.main_console.write) + console_errout_redirect = MainConsole.RedirectOutput(MainConsole.main_console.errorwrite) + + with redirect_stdout(console_stdout_redirect), \ + redirect_stderr(console_errout_redirect): + + # init whole UI + mw = MainWindow(sw.editor_startup_configuration) + mw.show() + sys.exit(app.exec_()) + + else: # just for some debugging + # init whole UI + mw = MainWindow(sw.editor_startup_configuration) + mw.show() + sys.exit(app.exec_()) diff --git a/Ryven/custom_src/Console/MainConsole.py b/Ryven/custom_src/Console/MainConsole.py new file mode 100644 index 00000000..8e9beffc --- /dev/null +++ b/Ryven/custom_src/Console/MainConsole.py @@ -0,0 +1,228 @@ +import code +import re +from PySide2.QtWidgets import QWidget, QLineEdit, QGridLayout, QPlainTextEdit, QLabel, QPushButton +from PySide2.QtCore import Signal, QEvent, Qt +from PySide2.QtGui import QTextCharFormat, QBrush, QColor, QFont + + +class MainConsole(QWidget): + """Complete console interpreter. + One instance will be created at the end of this file, when being imported in Ryven.py.""" + + def __init__( + self, + context=locals(), # context for interpreter + history: int = 100, # max lines in history buffer + blockcount: int = 5000 # max lines in output buffer + ): + + super(MainConsole, self).__init__() + + # CREATE UI + + self.content_layout = QGridLayout(self) + self.content_layout.setContentsMargins(0, 0, 0, 0) + self.content_layout.setSpacing(0) + + # reset scope button + self.reset_scope_button = QPushButton('reset console scope') + self.reset_scope_button.clicked.connect(self.reset_scope_clicked) + self.content_layout.addWidget(self.reset_scope_button, 0, 0, 1, 2) + self.reset_scope_button.hide() + + # display for output + self.out_display = ConsoleDisplay(blockcount, self) + self.content_layout.addWidget(self.out_display, 1, 0, 1, 2) + + # colors to differentiate input, output and stderr + self.inpfmt = self.out_display.currentCharFormat() + self.inpfmt.setForeground(QBrush(QColor('white'))) + self.outfmt = QTextCharFormat(self.inpfmt) + self.outfmt.setForeground(QBrush(QColor('#A9D5EF'))) + self.errfmt = QTextCharFormat(self.inpfmt) + self.errfmt.setForeground(QBrush(QColor('#B55730'))) + + # display input prompt left besides input edit + self.prompt_label = QLabel('> ', self) + self.prompt_label.setFixedWidth(15) + self.content_layout.addWidget(self.prompt_label, 2, 0) + + # command line + self.inpedit = LineEdit(max_history=history) + self.inpedit.returned.connect(self.push) + self.content_layout.addWidget(self.inpedit, 2, 1) + + + self.interp = None + self.reset_interpreter() + + self.buffer = [] + self.num_added_object_contexts = 0 + + + def setprompt(self, text: str): + self.prompt_label.setText(text) + + def reset_scope_clicked(self): + self.reset_interpreter() + + def add_obj_context(self, context_obj): + """adds the new context to the current context by initializing a new interpreter with both""" + + old_context = {} if self.interp is None else self.interp.locals + name = 'obj' + (str(self.num_added_object_contexts+1) if self.num_added_object_contexts > 0 else '') + new_context = {name: context_obj} + context = {**old_context, **new_context} # merge dicts + self.interp = code.InteractiveConsole(context) + print('added as ' + name) + + self.num_added_object_contexts += 1 + self.reset_scope_button.show() + + def reset_interpreter(self): + """Initializes a new plain interpreter""" + + context = locals() + self.num_added_object_contexts = 0 + self.reset_scope_button.hide() + self.interp = code.InteractiveConsole(context) + + def push(self, commands: str) -> None: + """execute entered command which may span multiple lines when code was pasted""" + + if commands == 'clear': + self.out_display.clear() + else: + lines = commands.split('\n') # usually just one entry + + # clean and print commands + for line in lines: + + # remove '> '-and '. ' prefixes which may remain from copy&paste + if re.match('^[\>\.] ', line): + line = line[2:] + + # print input + self.writeoutput(self.prompt_label.text() + line, self.inpfmt) + + # prepare for multi-line input + self.setprompt('. ') + self.buffer.append(line) + + # merge commands + source = '\n'.join(self.buffer) + more = self.interp.runsource(source, '') + + if not more: # no more input required + self.setprompt('> ') + self.buffer = [] # reset buffer + + def write(self, line: str) -> None: + """capture stdout and print to outdisplay""" + if len(line) != 1 or ord(line[0]) != 10: + self.writeoutput(line.rstrip(), self.outfmt) + + def errorwrite(self, line: str) -> None: + """capture stderr and print to outdisplay""" + self.writeoutput(line, self.errfmt) + + def writeoutput(self, line: str, fmt: QTextCharFormat = None) -> None: + """prints to outdisplay""" + if fmt is not None: + self.out_display.setCurrentCharFormat(fmt) + self.out_display.appendPlainText(line.rstrip()) + + +class LineEdit(QLineEdit): + """Input line edit with a history buffer for recalling previous lines.""" + + returned = Signal(str) + + def __init__(self, max_history: int = 100): + super().__init__() + + self.setObjectName('ConsoleInputLineEdit') + self.max_hist = max_history + self.hist_index = 0 + self.hist_list = [] + self.prompt_pattern = re.compile('^[>\.]') + self.setFont(QFont('source code pro', 11)) + + def event(self, ev: QEvent) -> bool: + """ + Tab: Insert 4 spaces + Arrow Up/Down: select a line from the history buffer + Newline: Emit returned signal + """ + if ev.type() == QEvent.KeyPress: + if ev.key() == Qt.Key_Tab: + self.insert(' '*4) + return True + elif ev.key() == Qt.Key_Up: + self.recall(self.hist_index - 1) + return True + elif ev.key() == Qt.Key_Down: + self.recall(self.hist_index + 1) + return True + elif ev.key() == Qt.Key_Return: + self.returnkey() + return True + + return super().event(ev) + + def returnkey(self) -> None: + text = self.text() + self.record(text) + self.returned.emit(text) + self.setText('') + + def recall(self, index: int) -> None: + """select a line from the history list""" + + if len(self.hist_list) > 0 and 0 <= index < len(self.hist_list): + self.setText(self.hist_list[index]) + self.hist_index = index + + def record(self, line: str) -> None: + """store line in history buffer and update hist_index""" + + while len(self.hist_list) >= self.max_hist - 1: + self.hist_list.pop() + self.hist_list.append(line) + + if self.hist_index == len(self.hist_list)-1 or line != self.hist_list[self.hist_index]: + self.hist_index = len(self.hist_list) + + + +class ConsoleDisplay(QPlainTextEdit): + def __init__(self, max_block_count, parent=None): + super(ConsoleDisplay, self).__init__(parent) + + self.setObjectName('ConsoleDisplay') + self.setMaximumBlockCount(max_block_count) + self.setReadOnly(True) + self.setFont(QFont('Consolas', 8)) + + +class RedirectOutput: + """Just redirects 'write()'-calls to a specified method.""" + + def __init__(self, func): + self.func = func + + def write(self, line): + self.func(line) + + +# CREATING ONE MAIN CONSOLE INSTANCE + +# note that, for some reason idk, I need to access this variable using MainConsole.main_console. Otherwise all +# references made when it was None will still hold value None... +main_console = None +main_console_enabled = True + + +def init_main_console(): + global main_console + main_console = MainConsole() diff --git a/Ryven/custom_src/MainWindow.py b/Ryven/custom_src/MainWindow.py index 2717c1fd..ded05480 100644 --- a/Ryven/custom_src/MainWindow.py +++ b/Ryven/custom_src/MainWindow.py @@ -4,6 +4,7 @@ from PySide2.QtWidgets import QMainWindow, QFileDialog, QShortcut, QAction, QActionGroup, QMenu, QMessageBox # parent UI +import custom_src.Console.MainConsole as MainConsole from custom_src.builtin_nodes.Result_Node import Result_Node from custom_src.builtin_nodes.Result_NodeInstance import Result_NodeInstance from custom_src.builtin_nodes.Val_Node import Val_Node @@ -32,6 +33,9 @@ def __init__(self, config): self.ui = Ui_MainWindow() self.ui.setupUi(self) + if MainConsole.main_console is not None: + self.ui.scripts_console_splitter.addWidget(MainConsole.main_console) + self.ui.scripts_console_splitter.setSizes([350, 350]) self.ui.splitter.setSizes([120, 800]) self.setWindowTitle('Ryven') self.setWindowIcon(QIcon('../resources/pics/program_icon2.png')) @@ -89,11 +93,11 @@ def __init__(self, config): print('finished') print(''' - CONTROLS - placing nodes: right mouse - selecting components: left mouse - panning: middle mouse - saving: ctrl+s +CONTROLS +placing: right mouse +selecting: left mouse +panning: middle mouse +saving: ctrl+s ''') diff --git a/Ryven/custom_src/NodeInstance.py b/Ryven/custom_src/NodeInstance.py index 43a8dec2..225e8b82 100644 --- a/Ryven/custom_src/NodeInstance.py +++ b/Ryven/custom_src/NodeInstance.py @@ -3,6 +3,7 @@ from PySide2.QtCore import Qt, QRectF, QPointF from PySide2.QtGui import QColor +import custom_src.Console.MainConsole as MainConsole from custom_src.GlobalAttributes import ViewportUpdateMode from custom_src.NodeInstanceAction import NodeInstanceAction from custom_src.NodeInstanceAnimator import NodeInstanceAnimator @@ -41,7 +42,8 @@ def __init__(self, params): # self.node_instance_painter = NodeInstancePainter(self) self.default_actions = {'remove': {'method': self.action_remove}, - 'update shape': {'method': self.update_shape}} # for context menus + 'update shape': {'method': self.update_shape}, + 'console ref': {'method': self.set_console_scope}} # for context menus self.special_actions = {} # only gets written in custom NodeInstance-subclasses self.personal_logs = [] @@ -214,7 +216,7 @@ def update(self, input_called=-1, output_called=-1): try: self.update_event(input_called) except Exception as e: - Debugger.debug('EXCEPTION IN', self.parent_node.title, 'NI:', e) + Debugger.debugerr('EXCEPTION IN', self.parent_node.title, 'NI:', e) def update_event(self, input_called=-1): """Gets called when an input received a signal. This is where the magic begins in subclasses.""" @@ -489,6 +491,12 @@ def unregister_var_receiver(self, name): # -------------------------------------------------------------------------------------- # UI STUFF ---------------------------------------- + def set_console_scope(self): + # extensive_dict = {} # unlike self.__dict__, it also includes methods to call! :) + # for att in dir(self): + # extensive_dict[att] = getattr(self, att) + MainConsole.main_console.add_obj_context(self) + def theme_changed(self, new_theme): self.title_label.theme_changed(new_theme) self.update_design() diff --git a/Ryven/custom_src/builtin_nodes/GetVar_NodeInstance.py b/Ryven/custom_src/builtin_nodes/GetVar_NodeInstance.py index 438bfea3..eb676186 100644 --- a/Ryven/custom_src/builtin_nodes/GetVar_NodeInstance.py +++ b/Ryven/custom_src/builtin_nodes/GetVar_NodeInstance.py @@ -12,19 +12,17 @@ def __init__(self, params): def update_event(self, input_called=-1): if self.input(0) != self.var_name: - vars_handler = self.flow.parent_script.variables_handler - if self.var_name != '': # disconnect old var val update connection - vars_handler.unregister_receiver(self, self.var_name) + self.unregister_var_receiver(self.var_name) self.var_name = self.input(0) # create new var update connection - vars_handler.register_receiver(self, self.var_name, M(self.var_val_changed)) + self.register_var_receiver(self.var_name, M(self.var_val_changed)) - var = vars_handler.get_var(self.input(0)) - if var is not None: - self.set_output_val(0, var.val) + val = self.get_var_val(self.input(0)) + if val is not None: + self.set_output_val(0, val) else: self.set_output_val(0, None) diff --git a/Ryven/custom_src/global_tools/Debugger.py b/Ryven/custom_src/global_tools/Debugger.py index ecfbfa9b..459ac359 100644 --- a/Ryven/custom_src/global_tools/Debugger.py +++ b/Ryven/custom_src/global_tools/Debugger.py @@ -1,9 +1,14 @@ +import sys + + class Debugger: enabled = False + @staticmethod def enable(): Debugger.enabled = True + @staticmethod def disable(): Debugger.enabled = False @@ -14,4 +19,28 @@ def debug(*args): s = '' for arg in args: s += ' '+str(arg) - print(' --> DEBUG:', s) \ No newline at end of file + print('--> DEBUG:', s) + + def debugerr(*args): + if not Debugger.enabled: + return + + s = '' + for arg in args: + s += ' '+str(arg) + + sys.stderr.write(s) + + # print(DEBUG_COLORS.WARNING + s + DEBUG_COLORS.ENDC) + + +class DEBUG_COLORS: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' diff --git a/Ryven/ui/ui_main_window.py b/Ryven/ui/ui_main_window.py index 3ee530d8..6f3cdbcf 100644 --- a/Ryven/ui/ui_main_window.py +++ b/Ryven/ui/ui_main_window.py @@ -53,26 +53,36 @@ def setupUi(self, MainWindow): self.splitter.setOrientation(Qt.Horizontal) self.scripts_groupBox = QGroupBox(self.splitter) self.scripts_groupBox.setObjectName(u"scripts_groupBox") - self.gridLayout = QGridLayout(self.scripts_groupBox) + self.gridLayout_3 = QGridLayout(self.scripts_groupBox) + self.gridLayout_3.setSpacing(6) + self.gridLayout_3.setContentsMargins(11, 11, 11, 11) + self.gridLayout_3.setObjectName(u"gridLayout_3") + self.scripts_console_splitter = QSplitter(self.scripts_groupBox) + self.scripts_console_splitter.setObjectName(u"scripts_console_splitter") + self.scripts_console_splitter.setOrientation(Qt.Vertical) + self.widget = QWidget(self.scripts_console_splitter) + self.widget.setObjectName(u"widget") + self.gridLayout = QGridLayout(self.widget) self.gridLayout.setSpacing(6) self.gridLayout.setContentsMargins(11, 11, 11, 11) self.gridLayout.setObjectName(u"gridLayout") - self.new_script_name_lineEdit = QLineEdit(self.scripts_groupBox) - self.new_script_name_lineEdit.setObjectName(u"new_script_name_lineEdit") - - self.gridLayout.addWidget(self.new_script_name_lineEdit, 1, 0, 1, 1) - - self.scripts_scrollArea = QScrollArea(self.scripts_groupBox) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.scripts_scrollArea = QScrollArea(self.widget) self.scripts_scrollArea.setObjectName(u"scripts_scrollArea") self.scripts_scrollArea.setWidgetResizable(True) self.scrollAreaWidgetContents = QWidget() self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") - self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 950, 750)) + self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 935, 727)) self.scripts_scrollArea.setWidget(self.scrollAreaWidgetContents) self.gridLayout.addWidget(self.scripts_scrollArea, 0, 0, 1, 1) - self.add_new_script_pushButton = QPushButton(self.scripts_groupBox) + self.new_script_name_lineEdit = QLineEdit(self.widget) + self.new_script_name_lineEdit.setObjectName(u"new_script_name_lineEdit") + + self.gridLayout.addWidget(self.new_script_name_lineEdit, 1, 0, 1, 1) + + self.add_new_script_pushButton = QPushButton(self.widget) self.add_new_script_pushButton.setObjectName(u"add_new_script_pushButton") sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) sizePolicy1.setHorizontalStretch(0) @@ -82,6 +92,10 @@ def setupUi(self, MainWindow): self.gridLayout.addWidget(self.add_new_script_pushButton, 2, 0, 1, 1) + self.scripts_console_splitter.addWidget(self.widget) + + self.gridLayout_3.addWidget(self.scripts_console_splitter, 0, 0, 1, 1) + self.splitter.addWidget(self.scripts_groupBox) self.scripts_tab_widget = QTabWidget(self.splitter) self.scripts_tab_widget.setObjectName(u"scripts_tab_widget") @@ -100,7 +114,7 @@ def setupUi(self, MainWindow): MainWindow.setCentralWidget(self.centralWidget) self.menuBar = QMenuBar(MainWindow) self.menuBar.setObjectName(u"menuBar") - self.menuBar.setGeometry(QRect(0, 0, 1368, 20)) + self.menuBar.setGeometry(QRect(0, 0, 1368, 26)) self.menuFile = QMenu(self.menuBar) self.menuFile.setObjectName(u"menuFile") self.menuView = QMenu(self.menuBar) diff --git a/resources/stylesheets/dark.txt b/resources/stylesheets/dark.txt index 1cefcdcd..16331205 100644 --- a/resources/stylesheets/dark.txt +++ b/resources/stylesheets/dark.txt @@ -33,17 +33,25 @@ QPlainTextEdit { background-color: #333333; } +QPlainTextEdit#ConsoleDisplay { + background-color: #2b2b2b; +} + QSplitter { border: none; } QSplitter::handle:vertical { - margin: 1px 4px; + margin: 1px 4px; } QLineEdit { padding: 3px; } +QLineEdit#ConsoleInputLineEdit { + border: none; +} + QLabel, QCheckBox, QRadioButton { border: none; } @@ -68,11 +76,11 @@ QRadioButton:indicator:checked { background: rgba(33, 118, 171, 200); } -QPushButton:disabled, -QSpinBox:disabled, -QComboBox:disabled, +QPushButton:disabled, +QSpinBox:disabled, +QComboBox:disabled, QRadioButton:disabled, -QGroupBox:disabled, +QGroupBox:disabled, QLabel:disabled { color: grey; } @@ -197,4 +205,4 @@ QTabBar::tab:!selected { QTabBar::tab:selected { border-bottom: 2px solid rgb(33, 118, 171); -} \ No newline at end of file +}