From b606e522170c17f41f268daef8e0ef7a886dbf19 Mon Sep 17 00:00:00 2001 From: Alexandre Sajus Date: Sun, 28 Nov 2021 01:37:08 +0100 Subject: [PATCH 1/8] :beetle: hides output panel without output --- opencodeblocks/graphics/blocks/block.py | 2 ++ opencodeblocks/graphics/blocks/codeblock.py | 28 +++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 0d13dd0f..87bf69c4 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -328,6 +328,8 @@ def deserialize(self, data: dict, hashmap: dict = None, if hashmap is not None: hashmap.update({socket_data['id']: socket}) + self.update_all() + class OCBSplitterHandle(QSplitterHandle): """ A handle for splitters with undoable events """ diff --git a/opencodeblocks/graphics/blocks/codeblock.py b/opencodeblocks/graphics/blocks/codeblock.py index 976ac50e..f8dda625 100644 --- a/opencodeblocks/graphics/blocks/codeblock.py +++ b/opencodeblocks/graphics/blocks/codeblock.py @@ -35,6 +35,9 @@ def __init__(self, **kwargs): self._min_output_panel_height = 20 self._min_source_editor_height = 20 + self.output_closed = False + self.previous_splitter_size = [0, 0] + self.source_editor = self.init_source_editor() self.output_panel = self.init_output_panel() self.run_button = self.init_run_button() @@ -63,6 +66,12 @@ def update_all(self): int(2.5 * self.edge_size) ) + # Close output panel if no output + if self.stdout == "" and self.image == "": + self.previous_splitter_size = self.splitter.sizes() + self.output_closed = True + self.splitter.setSizes([1, 0]) + @property def source(self) -> str: """ Source code. """ @@ -82,6 +91,16 @@ def stdout(self) -> str: @stdout.setter def stdout(self, value: str): self._stdout = value + if hasattr(self, 'output_closed'): + # If output panel is closed and there is output, open it + if self.output_closed == True and value != "": + self.output_closed = False + self.splitter.setSizes(self.previous_splitter_size) + # If output panel is open and there is no output, close it + elif self.output_closed == False and value == "": + self.previous_splitter_size = self.splitter.sizes() + self.output_closed = True + self.splitter.setSizes([1, 0]) if hasattr(self, 'source_editor'): # If there is a text output, erase the image output and display the # text output @@ -105,6 +124,15 @@ def image(self) -> str: @image.setter def image(self, value: str): self._image = value + # open or close output panel, same as stdout + if hasattr(self, 'output_closed') and value != "": + if self.output_closed == True and value != "": + self.output_closed = False + self.splitter.setSizes(self.previous_splitter_size) + elif self.output_closed == False and value == "": + self.previous_splitter_size = self.splitter.sizes() + self.output_closed = True + self.splitter.setSizes([1, 0]) if hasattr(self, 'source_editor') and self.image != "": # If there is an image output, erase the text output and display # the image output From 537cdb51ece0616390b4f079a2bc8e7d70730e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Sun, 28 Nov 2021 17:05:10 +0100 Subject: [PATCH 2/8] :hammer: Refactor OCBView Prevent drag_scene on blocks --- opencodeblocks/graphics/view.py | 43 +++++++++++++++++---------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index aebd6ef0..11881192 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -18,25 +18,25 @@ from opencodeblocks.graphics.edge import OCBEdge from opencodeblocks.graphics.blocks import OCBBlock -MODE_NOOP = 0 -MODE_EDGE_DRAG = 1 -MODE_EDITING = 2 - -MODES = { - 'MODE_NOOP': MODE_NOOP, - 'MODE_EDGE_DRAG': MODE_EDGE_DRAG, - 'MODE_EDITING': MODE_EDITING, -} - class OCBView(QGraphicsView): """ View for the OCB Window. """ + MODE_NOOP = 0 + MODE_EDGE_DRAG = 1 + MODE_EDITING = 2 + + MODES = { + 'NOOP': MODE_NOOP, + 'EDGE_DRAG': MODE_EDGE_DRAG, + 'EDITING': MODE_EDITING, + } + def __init__(self, scene: OCBScene, parent=None, zoom_step: float = 1.25, zoom_min: float = 0.2, zoom_max: float = 5): super().__init__(parent=parent) - self.mode = MODE_NOOP + self.mode = self.MODE_NOOP self.zoom = 1 self.zoom_step, self.zoom_min, self.zoom_max = zoom_step, zoom_min, zoom_max @@ -125,7 +125,8 @@ def leftMouseButtonRelease(self, event: QMouseEvent): def middleMouseButtonPress(self, event: QMouseEvent): """ OCBView reaction to middleMouseButtonPress event. """ - event = self.drag_scene(event, "press") + if self.itemAt(event.pos()) is None: + event = self.drag_scene(event, "press") super().mousePressEvent(event) def middleMouseButtonRelease(self, event: QMouseEvent): @@ -219,9 +220,9 @@ def drag_edge(self, event: QMouseEvent, action="press"): scene = self.scene() if action == "press": if isinstance(item_at_click, OCBSocket) \ - and self.mode != MODE_EDGE_DRAG\ + and self.mode != self.MODE_EDGE_DRAG\ and item_at_click.socket_type != 'input': - self.mode = MODE_EDGE_DRAG + self.mode = self.MODE_EDGE_DRAG self.edge_drag = OCBEdge( source_socket=item_at_click, destination=self.mapToScene(event.pos()) @@ -229,7 +230,7 @@ def drag_edge(self, event: QMouseEvent, action="press"): scene.addItem(self.edge_drag) return elif action == "release": - if self.mode == MODE_EDGE_DRAG: + if self.mode == self.MODE_EDGE_DRAG: if isinstance(item_at_click, OCBSocket) \ and item_at_click is not self.edge_drag.source_socket \ and item_at_click.socket_type != 'output': @@ -239,9 +240,9 @@ def drag_edge(self, event: QMouseEvent, action="press"): else: self.edge_drag.remove() self.edge_drag = None - self.mode = MODE_NOOP + self.mode = self.MODE_NOOP elif action == "move": - if self.mode == MODE_EDGE_DRAG: + if self.mode == self.MODE_EDGE_DRAG: self.edge_drag.destination = self.mapToScene(event.pos()) return event @@ -249,16 +250,16 @@ def set_mode(self, mode: str): """ Change the view mode. Args: - mode: Mode key to change to, must in present in knowed MODES. + mode: Mode key to change to, must in present in MODES. """ - self.mode = MODES[mode] + self.mode = self.MODES[mode] def is_mode(self, mode: str): """ Return True if the view is in the given mode. Args: - mode: Mode key to compare to, must in present in knowed MODES. + mode: Mode key to compare to, must in present in MODES. """ - return self.mode == MODES[mode] + return self.mode == self.MODES[mode] From 209b4b2348b9d98cbf221b42def986a724b7664c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Sun, 28 Nov 2021 17:05:28 +0100 Subject: [PATCH 3/8] :wrench: Refactor pyeditor to reduce duplications --- opencodeblocks/graphics/window.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/opencodeblocks/graphics/window.py b/opencodeblocks/graphics/window.py index 7e0b134e..d50f1b7e 100644 --- a/opencodeblocks/graphics/window.py +++ b/opencodeblocks/graphics/window.py @@ -10,8 +10,6 @@ from PyQt5.QtWidgets import QDockWidget, QListWidget, QWidget, QAction, QFileDialog, QMainWindow,\ QMessageBox, QMdiArea -from opencodeblocks import __appname__ as application_name -from opencodeblocks.graphics.view import MODE_EDITING from opencodeblocks.graphics.widget import OCBWidget from opencodeblocks.graphics.theme_manager import theme_manager @@ -269,40 +267,45 @@ def onFileSaveAs(self) -> bool: return True return False + @staticmethod + def is_not_editing(current_window: OCBWidget): + """ True if current_window exists and is not in editing mode. """ + return current_window is not None and not current_window.view.is_mode('EDITING') + def onEditUndo(self): """ Undo last operation if not in edit mode. """ current_window = self.activeMdiChild() - if current_window is not None and current_window.view.mode != MODE_EDITING: + if self.is_not_editing(current_window): current_window.scene.history.undo() def onEditRedo(self): """ Redo last operation if not in edit mode. """ current_window = self.activeMdiChild() - if current_window is not None and current_window.view.mode != MODE_EDITING: + if self.is_not_editing(current_window): current_window.scene.history.redo() def onEditCut(self): """ Cut the selected items if not in edit mode. """ current_window = self.activeMdiChild() - if current_window is not None and current_window.view.mode != MODE_EDITING: + if self.is_not_editing(current_window): current_window.scene.clipboard.cut() def onEditCopy(self): """ Copy the selected items if not in edit mode. """ current_window = self.activeMdiChild() - if current_window is not None and current_window.view.mode != MODE_EDITING: + if self.is_not_editing(current_window): current_window.scene.clipboard.copy() def onEditPaste(self): """ Paste the selected items if not in edit mode. """ current_window = self.activeMdiChild() - if current_window is not None and current_window.view.mode != MODE_EDITING: + if self.is_not_editing(current_window): current_window.scene.clipboard.paste() def onEditDelete(self): """ Delete the selected items if not in edit mode. """ current_window = self.activeMdiChild() - if current_window is not None and current_window.view.mode != MODE_EDITING: + if self.is_not_editing(current_window): current_window.view.deleteSelected() # def closeEvent(self, event:QEvent): From c093ee40576907b4b03495ab8e81e8ee38c86aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Sun, 28 Nov 2021 17:12:52 +0100 Subject: [PATCH 4/8] :wrench: Forced 'Empty' block to be first in list --- opencodeblocks/graphics/view.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/opencodeblocks/graphics/view.py b/opencodeblocks/graphics/view.py index 11881192..df5211e6 100644 --- a/opencodeblocks/graphics/view.py +++ b/opencodeblocks/graphics/view.py @@ -136,6 +136,7 @@ def middleMouseButtonRelease(self, event: QMouseEvent): self.setDragMode(QGraphicsView.DragMode.RubberBandDrag) def retreiveBlockTypes(self) -> List[Tuple[str]]: + """ Retreive the list of stored blocks. """ block_type_files = os.listdir("blocks") block_types = [] for b in block_type_files: @@ -145,7 +146,10 @@ def retreiveBlockTypes(self) -> List[Tuple[str]]: title = "New Block" if "title" in data: title = f"New {data['title']} Block" - block_types.append((filepath, title)) + if data['title'] == "Empty": + block_types[:0] = [(filepath, title)] + else: + block_types.append((filepath, title)) return block_types def contextMenuEvent(self, event: QContextMenuEvent): From f3606992cc8526c473cf1cb2e22733369737c1b3 Mon Sep 17 00:00:00 2001 From: AlexandreSajus Date: Mon, 29 Nov 2021 22:09:01 +0100 Subject: [PATCH 5/8] :sparkles: Codacy issue --- opencodeblocks/graphics/blocks/codeblock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opencodeblocks/graphics/blocks/codeblock.py b/opencodeblocks/graphics/blocks/codeblock.py index f219a6cd..e4ff8361 100644 --- a/opencodeblocks/graphics/blocks/codeblock.py +++ b/opencodeblocks/graphics/blocks/codeblock.py @@ -95,11 +95,11 @@ def stdout(self, value: str): if hasattr(self, 'output_closed'): # If output panel is closed and there is output, open it - if self.output_closed == True and value != "": + if self.output_closed is True and value != "": self.output_closed = False self.splitter.setSizes(self.previous_splitter_size) # If output panel is open and there is no output, close it - elif self.output_closed == False and value == "": + elif self.output_closed is False and value == "": self.previous_splitter_size = self.splitter.sizes() self.output_closed = True self.splitter.setSizes([1, 0]) From fd4dbeb181c28e15e81c348299ce766305d8de1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Mon, 29 Nov 2021 23:53:44 +0100 Subject: [PATCH 6/8] :hammer: Refactor codeblock output handling --- opencodeblocks/graphics/blocks/block.py | 5 +- opencodeblocks/graphics/blocks/codeblock.py | 155 ++++++++------------ opencodeblocks/graphics/pyeditor.py | 2 - 3 files changed, 61 insertions(+), 101 deletions(-) diff --git a/opencodeblocks/graphics/blocks/block.py b/opencodeblocks/graphics/blocks/block.py index 87bf69c4..5dd9852a 100644 --- a/opencodeblocks/graphics/blocks/block.py +++ b/opencodeblocks/graphics/blocks/block.py @@ -50,7 +50,6 @@ def __init__(self, block_type: str = 'base', source: str = '', position: tuple = self.block_type = block_type self.source = source self.stdout = "" - self.image = "" self.setPos(QPointF(*position)) self.sockets_in = [] self.sockets_out = [] @@ -296,7 +295,6 @@ def serialize(self) -> OrderedDict: ('block_type', self.block_type), ('source', self.source), ('stdout', self.stdout), - ('image', self.image), ('splitter_pos', self.splitter.sizes()), ('position', [self.pos().x(), self.pos().y()]), ('width', self.width), @@ -310,8 +308,7 @@ def deserialize(self, data: dict, hashmap: dict = None, restore_id=True) -> None: if restore_id: self.id = data['id'] - for dataname in ('title', 'block_type', 'source', 'stdout', - 'image', 'width', 'height'): + for dataname in ('title', 'block_type', 'source', 'stdout', 'width', 'height'): setattr(self, dataname, data[dataname]) self.setPos(QPointF(*data['position'])) diff --git a/opencodeblocks/graphics/blocks/codeblock.py b/opencodeblocks/graphics/blocks/codeblock.py index e4ff8361..e87526bc 100644 --- a/opencodeblocks/graphics/blocks/codeblock.py +++ b/opencodeblocks/graphics/blocks/codeblock.py @@ -35,15 +35,13 @@ def __init__(self, **kwargs): self._min_output_panel_height = 20 self._min_source_editor_height = 20 - self.output_closed = False - self.previous_splitter_size = [0, 0] + self.output_closed = True + self._splitter_size = [0, 0] + self._cached_stdout = "" self.source_editor = self.init_source_editor() self.output_panel = self.init_output_panel() self.run_button = self.init_run_button() - self.previous_stdout = "" - self.stdout = "" - self.image = "" self.title_left_offset = 3 * self.edge_size self.holder.setWidget(self.root) @@ -68,7 +66,7 @@ def update_all(self): ) # Close output panel if no output - if self.stdout == "" and self.image == "": + if self.stdout == "": self.previous_splitter_size = self.splitter.sizes() self.output_closed = True self.splitter.setSizes([1, 0]) @@ -76,93 +74,11 @@ def update_all(self): @property def source(self) -> str: """ Source code. """ - return self._source + return self.source_editor.text() @source.setter def source(self, value: str): - self._source = value - if hasattr(self, 'source_editor'): - self.source_editor.setText(self._source) - - @property - def stdout(self) -> str: - """ Code output. Be careful, this also includes stderr """ - return self._stdout - - @stdout.setter - def stdout(self, value: str): - self._stdout = value - - if hasattr(self, 'output_closed'): - # If output panel is closed and there is output, open it - if self.output_closed is True and value != "": - self.output_closed = False - self.splitter.setSizes(self.previous_splitter_size) - # If output panel is open and there is no output, close it - elif self.output_closed is False and value == "": - self.previous_splitter_size = self.splitter.sizes() - self.output_closed = True - self.splitter.setSizes([1, 0]) - - if hasattr(self, 'source_editor'): - # If there is a text output, erase the image output and display the - # text output - self.image = "" - - # If there is a new line - # Save every line but the last one - if value.find('\n') != -1: - lines = value.split('\n') - self.previous_stdout += '\n'.join(lines[:-1]) + '\n' - value = lines[-1] - - # Update the last line only - if hasattr(self, 'previous_stdout'): - to_display = self.previous_stdout + value - else: - to_display = value - - # If there is a text output, erase the image output - self.image = "" - - if hasattr(self, 'output_panel'): - # Remove carriage returns and backspaces - text = to_display.replace("\x08", "") - text = text.replace("\r", "") - # Convert ANSI escape codes to HTML - text = conv.convert(text) - # Replace background color - text = text.replace('background-color: #000000', - 'background-color: #434343') - - self.output_panel.setText(text) - - @property - def image(self) -> str: - """ Code output. """ - return self._image - - @image.setter - def image(self, value: str): - self._image = value - # open or close output panel, same as stdout - if hasattr(self, 'output_closed') and value != "": - if self.output_closed == True and value != "": - self.output_closed = False - self.splitter.setSizes(self.previous_splitter_size) - elif self.output_closed == False and value == "": - self.previous_splitter_size = self.splitter.sizes() - self.output_closed = True - self.splitter.setSizes([1, 0]) - if hasattr(self, 'source_editor') and self.image != "": - # If there is an image output, erase the text output and display - # the image output - text = "" - ba = QByteArray.fromBase64(str.encode(self.image)) - pixmap = QPixmap() - pixmap.loadFromData(ba) - text = f'' - self.output_panel.setText(text) + self.source_editor.setText(value) @source.setter def source(self, value: str): @@ -191,7 +107,8 @@ def init_run_button(self): def run_code(self): """Run the code in the block""" # Erase previous output - self.previous_stdout = "" + self.stdout = "" + self._cached_stdout = "" self.source = self.source_editor.text() # Create a worker to handle execution worker = Worker(self.source_editor.kernel, self.source) @@ -199,10 +116,58 @@ def run_code(self): worker.signals.image.connect(self.handle_image) self.source_editor.threadpool.start(worker) - def handle_stdout(self, stdout): + @property + def stdout(self) -> str: + return self._stdout + + @stdout.setter + def stdout(self, value: str): + self._stdout = value + if hasattr(self, 'output_panel'): + if value.startswith(''): + display_text = self.b64_to_html(value[5:]) + else: + display_text = self.str_to_html(value) + self.output_panel.setText(display_text) + # If output panel is closed and there is output, open it + if self.output_closed and value != "": + self.output_closed = False + self.splitter.setSizes(self._splitter_size) + # If output panel is open and there is no output, close it + elif not self.output_closed and value == "": + self._splitter_size = self.splitter.sizes() + self.output_closed = True + self.splitter.setSizes([1, 0]) + + @staticmethod + def str_to_html(text: str): + # Remove carriage returns and backspaces + text = text.replace("\x08", "") + text = text.replace("\r", "") + # Convert ANSI escape codes to HTML + text = conv.convert(text) + # Replace background color + text = text.replace('background-color: #000000', + 'background-color: transparent') + return text + + def handle_stdout(self, value: str): """ Handle the stdout signal """ - self.stdout = stdout + # If there is a new line + # Save every line but the last one + + if value.find('\n') != -1: + lines = value.split('\n') + self._cached_stdout += '\n'.join(lines[:-1]) + '\n' + value = lines[-1] + + # Update the last line only + self.stdout = self._cached_stdout + value + + @staticmethod + def b64_to_html(image: str): + return f'' - def handle_image(self, image): + def handle_image(self, image: str): """ Handle the image signal """ - self.image = image + self.stdout = '' + image diff --git a/opencodeblocks/graphics/pyeditor.py b/opencodeblocks/graphics/pyeditor.py index 4cad260d..0ab39a94 100644 --- a/opencodeblocks/graphics/pyeditor.py +++ b/opencodeblocks/graphics/pyeditor.py @@ -35,7 +35,6 @@ def __init__(self, block: OCBBlock): self.block = block self.kernel = kernel self.threadpool = threadpool - self.setText(self.block.source) self.update_theme() theme_manager().themeChanged.connect(self.update_theme) @@ -117,5 +116,4 @@ def mousePressEvent(self, event: QMouseEvent) -> None: def focusOutEvent(self, event: QFocusEvent): """ PythonEditor reaction to PyQt focusOut events. """ self.mode = "NOOP" - self.block.source = self.text() return super().focusOutEvent(event) From 12f94ee2232da88f9097a191b8310cdc2e9bbfea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Mon, 29 Nov 2021 23:54:00 +0100 Subject: [PATCH 7/8] :memo: Update mnist example --- examples/mnist.ipyg | 43 ++++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/examples/mnist.ipyg b/examples/mnist.ipyg index d2abf3ee..35c2b1f9 100644 --- a/examples/mnist.ipyg +++ b/examples/mnist.ipyg @@ -6,8 +6,7 @@ "title": "Model Train", "block_type": "code", "source": "model.fit(x=x_train,y=y_train, epochs=4)\r\n", - "stdout": "", - "image": "", + "stdout": "Epoch 1/4\n\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r1875/1875 [==============================] - 10s 4ms/step - loss: 0.2116 - accuracy: 0.9367\nEpoch 2/4\n\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r1875/1875 [==============================] - 7s 4ms/step - loss: 0.0853 - accuracy: 0.9738\nEpoch 3/4\n\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r1875/1875 [==============================] - 7s 4ms/step - loss: 0.0597 - accuracy: 0.9813\nEpoch 4/4\n\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r1875/1875 [==============================] - 7s 4ms/step - loss: 0.0472 - accuracy: 0.9848\n", "splitter_pos": [ 85, 259 @@ -62,15 +61,14 @@ "title": "Keras Model Predict", "block_type": "code", "source": "rd_index = np.random.randint(len(x_test))\r\nprediction = np.argmax(model.predict(x_test[rd_index].reshape(1, 28, 28, 1)))\r\nplt.imshow(x_test[rd_index], cmap='gray')\r\nplt.title(\"Predicted: \" + str(prediction))", - "stdout": "Text(0.5, 1.0, 'Predicted: 3')", - "image": "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\n", + "stdout": "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\n", "splitter_pos": [ 0, 278 ], "position": [ - 2244.066406249998, - -594.3554687499998 + 2330.066406249998, + -595.3554687499998 ], "width": 301, "height": 333, @@ -104,18 +102,17 @@ "title": "Keras Model eval", "block_type": "code", "source": "metrics = model.evaluate(x_test, y_test)\r\nprint(f\"mean_loss:{metrics[0]:.2f}, mean_acc:{metrics[1]:.2f}\")\r\n", - "stdout": "mean_loss:0.05184061825275421, mean_acc:0.9848999977111816\n", - "image": "", + "stdout": "\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\r313/313 [==============================] - 1s 2ms/step - loss: 0.0552 - accuracy: 0.9823\nmean_loss:0.06, mean_acc:0.98\n", "splitter_pos": [ - 79, - 79 + 76, + 76 ], "position": [ - 2156.17578125, - -146.1249999999996 + 2295.17578125, + -188.1249999999996 ], - "width": 914, - "height": 213, + "width": 1037, + "height": 207, "metadata": { "title_metadata": { "color": "white", @@ -147,17 +144,16 @@ "block_type": "code", "source": "from tensorflow.keras.datasets import mnist\r\n(x_train, y_train), (x_test, y_test) = mnist.load_data()\r\n", "stdout": "", - "image": "", "splitter_pos": [ - 85, + 86, 0 ], "position": [ -877.3242187500001, -354.52734375000006 ], - "width": 834, - "height": 112, + "width": 850, + "height": 141, "metadata": { "title_metadata": { "color": "white", @@ -171,7 +167,7 @@ "id": 2443478910728, "type": "output", "position": [ - 834.0, + 850.0, 42.0 ], "metadata": { @@ -189,7 +185,6 @@ "block_type": "code", "source": "x_train = x_train.astype('float32') / 255.0\r\nx_test = x_test.astype('float32') / 255.0\r\n\r\nx_train = x_train.reshape(x_train.shape[0], 28, 28, 1)\r\nx_test = x_test.reshape(x_test.shape[0], 28, 28, 1)\r\n\r\nprint('train:', x_train.shape, '|test:', x_test.shape)\r\n", "stdout": "train: (60000, 28, 28, 1) |test: (10000, 28, 28, 1)\n", - "image": "", "splitter_pos": [ 206, 85 @@ -245,7 +240,6 @@ "block_type": "code", "source": "import tensorflow as tf\r\nfrom tensorflow.keras.layers import (Dense, Flatten,\r\nConv2D, MaxPooling2D, Dropout)\r\nfrom tensorflow.keras.models import Sequential\r\n\r\nmodel = Sequential()\r\nmodel.add(Conv2D(28, kernel_size=(3,3), input_shape=x_train.shape[1:]))\r\nmodel.add(MaxPooling2D(pool_size=(2, 2)))\r\nmodel.add(Flatten())\r\nmodel.add(Dense(128, activation=tf.nn.relu))\r\nmodel.add(Dropout(0.2))\r\nmodel.add(Dense(10,activation=tf.nn.softmax))\r\n\r\nmodel.compile(optimizer='adam', \r\n loss='sparse_categorical_crossentropy', \r\n metrics=['accuracy'])\r\n", "stdout": "", - "image": "", "splitter_pos": [ 418, 0 @@ -286,15 +280,14 @@ "title": "Plot Image Dataset Example", "block_type": "code", "source": "import matplotlib.pyplot as plt\r\nimport numpy as np\r\n\r\n# Display an example from the dataset\r\nrd_index = np.random.randint(len(x_train))\r\nplt.imshow(x_train[rd_index], cmap='gray')\r\nplt.title('Class '+ str(y_train[rd_index]))\r\n", - "stdout": "Text(0.5, 1.0, 'Class 9')", - "image": "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\n", + "stdout": "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\n", "splitter_pos": [ 0, 274 ], "position": [ - 31.609375000000114, - -738.9375 + 103.60937500000011, + -734.9375 ], "width": 300, "height": 329, From 039e0da7af608eaa84000486c93341545dfcaffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Math=C3=AFs=20F=C3=A9d=C3=A9rico?= Date: Tue, 30 Nov 2021 00:03:16 +0100 Subject: [PATCH 8/8] :beetle: Fix codeblock source property --- opencodeblocks/graphics/blocks/codeblock.py | 75 +++++++++------------ 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/opencodeblocks/graphics/blocks/codeblock.py b/opencodeblocks/graphics/blocks/codeblock.py index e87526bc..aba18bb7 100644 --- a/opencodeblocks/graphics/blocks/codeblock.py +++ b/opencodeblocks/graphics/blocks/codeblock.py @@ -29,6 +29,9 @@ class OCBCodeBlock(OCBBlock): """ def __init__(self, **kwargs): + + self.source_editor = PythonEditor(self) + super().__init__(block_type='code', **kwargs) self.output_panel_height = self.height / 3 @@ -39,60 +42,23 @@ def __init__(self, **kwargs): self._splitter_size = [0, 0] self._cached_stdout = "" - self.source_editor = self.init_source_editor() self.output_panel = self.init_output_panel() self.run_button = self.init_run_button() + + self.splitter.addWidget(self.source_editor) + self.splitter.addWidget(self.output_panel) + self.title_left_offset = 3 * self.edge_size self.holder.setWidget(self.root) self.update_all() # Set the geometry of display and source_editor - def init_source_editor(self): - """ Initialize the python source code editor. """ - source_editor = PythonEditor(self) - self.splitter.addWidget(source_editor) - return source_editor - - def update_all(self): - """ Update the code block parts. """ - super().update_all() - if hasattr(self, 'run_button'): - self.run_button.setGeometry( - int(self.edge_size), - int(self.edge_size / 2), - int(2.5 * self.edge_size), - int(2.5 * self.edge_size) - ) - - # Close output panel if no output - if self.stdout == "": - self.previous_splitter_size = self.splitter.sizes() - self.output_closed = True - self.splitter.setSizes([1, 0]) - - @property - def source(self) -> str: - """ Source code. """ - return self.source_editor.text() - - @source.setter - def source(self, value: str): - self.source_editor.setText(value) - - @source.setter - def source(self, value: str): - self._source = value - if hasattr(self, 'source_editor'): - editor_widget = self.source_editor - editor_widget.setText(self._source) - def init_output_panel(self): """ Initialize the output display widget: QLabel """ output_panel = QTextEdit() output_panel.setReadOnly(True) output_panel.setFont(self.source_editor.font()) - self.splitter.addWidget(output_panel) return output_panel def init_run_button(self): @@ -101,7 +67,6 @@ def init_run_button(self): run_button.setMinimumWidth(int(self.edge_size)) run_button.clicked.connect(self.run_code) run_button.raise_() - return run_button def run_code(self): @@ -116,6 +81,32 @@ def run_code(self): worker.signals.image.connect(self.handle_image) self.source_editor.threadpool.start(worker) + def update_all(self): + """ Update the code block parts. """ + super().update_all() + if hasattr(self, 'run_button'): + self.run_button.setGeometry( + int(self.edge_size), + int(self.edge_size / 2), + int(2.5 * self.edge_size), + int(2.5 * self.edge_size) + ) + + # Close output panel if no output + if self.stdout == "": + self.previous_splitter_size = self.splitter.sizes() + self.output_closed = True + self.splitter.setSizes([1, 0]) + + @property + def source(self) -> str: + """ Source code. """ + return self.source_editor.text() + + @source.setter + def source(self, value: str): + self.source_editor.setText(value) + @property def stdout(self) -> str: return self._stdout