Skip to content

Commit

Permalink
🔀 Merge pull request #17 from 'feature/kernel'
Browse files Browse the repository at this point in the history
Add IPython kernel execution
  • Loading branch information
MathisFederico authored Nov 17, 2021
2 parents 1d8d869 + 5d2ee76 commit bf563a7
Show file tree
Hide file tree
Showing 13 changed files with 597 additions and 36 deletions.
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ Before doing your **pull request**, check using `pylint` and `pytest` that there
pylint .\opencodeblocks\
```

Some `pylint` issues can be fixed automatically using `autopep8`, with the following command:

```bash
autopep8 --in-place --recursive --aggressive opencodeblocks
```

```bash
pytest --cov=opencodeblocks --cov-report=html tests/unit
```
Expand Down
4 changes: 0 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@
import sys

from qtpy.QtWidgets import QApplication

from opencodeblocks.graphics.blocks.codeblock import OCBCodeBlock, OCBBlock
from opencodeblocks.graphics.edge import OCBEdge
from opencodeblocks.graphics.socket import OCBSocket
from opencodeblocks.graphics.window import OCBWindow

sys.path.insert(0, os.path.join( os.path.dirname(__file__), "..", ".." ))
Expand Down
51 changes: 42 additions & 9 deletions opencodeblocks/graphics/blocks/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from PyQt5.QtCore import QPointF, QRectF, Qt
from PyQt5.QtGui import QBrush, QPen, QColor, QFont, QPainter, QPainterPath
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsSceneMouseEvent, QGraphicsTextItem, \
QStyleOptionGraphicsItem, QWidget, QApplication
QStyleOptionGraphicsItem, QWidget, QApplication, QGraphicsSceneHoverEvent

from opencodeblocks.core.serializable import Serializable
from opencodeblocks.graphics.socket import OCBSocket
Expand Down Expand Up @@ -72,7 +72,10 @@ def __init__(self, block_type:str='base', source:str='', position:tuple=(0, 0),
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable)

self.setAcceptHoverEvents(True)

self.resizing = False
self.resizing_hover = False # Is the mouse hovering over the resizing area ?
self.moved = False
self.metadata = {
'title_metadata': {
Expand Down Expand Up @@ -130,8 +133,8 @@ def paint(self, painter: QPainter,

def _is_in_resize_area(self, pos:QPointF):
""" Return True if the given position is in the block resize_area. """
return self.width - pos.x() < 2 * self.edge_size \
and self.height - pos.y() < 2 * self.edge_size
return self.width - self.edge_size*2 < pos.x() \
and self.height - self.edge_size*2 < pos.y()

def get_socket_pos(self, socket:OCBSocket) -> Tuple[float]:
""" Get a socket position to place them on the block sides. """
Expand Down Expand Up @@ -172,21 +175,51 @@ def remove_socket(self, socket:OCBSocket):
socket.remove()
self.update_sockets()

def hoverMoveEvent(self, event:QGraphicsSceneHoverEvent):
""" Triggered when hovering over a block """
pos = event.pos()
if self._is_in_resize_area(pos):
if not self.resizing_hover:
self._start_hovering()
elif self.resizing_hover:
self._stop_hovering()
return super().hoverMoveEvent(event)

def _start_hovering(self):
self.resizing_hover = True
QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor)

def _stop_hovering(self):
self.resizing_hover = False
QApplication.restoreOverrideCursor()

def _start_resize(self,pos:QPointF):
self.resizing = True
self.resize_start = pos
QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor)

def _stop_resize(self):
self.resizing = False
QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor)

def hoverLeaveEvent(self, event:QGraphicsSceneHoverEvent):
""" Triggered when the mouse stops hovering over a block """
if self.resizing_hover:
self._stop_hovering()
return super().hoverLeaveEvent(event)

def mousePressEvent(self, event:QGraphicsSceneMouseEvent):
""" OCBBlock reaction to a mousePressEvent. """
pos = event.pos()
if self._is_in_resize_area(pos) and event.buttons() == Qt.MouseButton.LeftButton:
self.resize_start = pos
self.resizing = True
QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor)
if self.resizing_hover and event.buttons() == Qt.MouseButton.LeftButton:
self._start_resize(pos)
super().mousePressEvent(event)

def mouseReleaseEvent(self, event:QGraphicsSceneMouseEvent):
""" OCBBlock reaction to a mouseReleaseEvent. """
if self.resizing:
self.scene().history.checkpoint("Resized block", set_modified=True)
self.resizing = False
QApplication.restoreOverrideCursor()
self._stop_resize()
if self.moved:
self.moved = False
self.scene().history.checkpoint("Moved block", set_modified=True)
Expand Down
172 changes: 163 additions & 9 deletions opencodeblocks/graphics/blocks/codeblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,61 @@

""" Module for the base OCB Code Block. """

from PyQt5.QtWidgets import QGraphicsProxyWidget
from typing import Optional

from PyQt5.QtCore import Qt, QByteArray, QPointF
from PyQt5.QtGui import QPainter, QPainterPath, QPixmap
from PyQt5.QtWidgets import QStyleOptionGraphicsItem, QWidget, QGraphicsProxyWidget, QLabel, \
QGraphicsSceneMouseEvent, QApplication

from opencodeblocks.graphics.blocks.block import OCBBlock
from opencodeblocks.graphics.pyeditor import PythonEditor

class OCBCodeBlock(OCBBlock):

""" Code Block. """
"""
Code Block
Features an area to edit code as well as a panel to display the output.
The following is always true:
output_panel_height + source_panel_height + edge_size*2 + title_height == height
"""

def __init__(self, **kwargs):
super().__init__(block_type='code', **kwargs)

self.output_panel_height = self.height / 3
self._min_output_panel_height = 20
self._min_source_editor_height = 20

self.source_editor = self.init_source_editor()
self.display = self.init_display()
self.stdout = ""
self.image = ""

self.resizing_source_code = False

self.update_all() # Set the geometry of display and source_editor

def init_source_editor(self):
""" Initialize the python source code editor. """
source_editor_graphics = QGraphicsProxyWidget(self)
source_editor = PythonEditor(self)
source_editor.setGeometry(
int(self.edge_size),
int(self.edge_size + self.title_height),
int(self.width - 2*self.edge_size),
int(self.height - self.title_height - 2*self.edge_size)
)
source_editor_graphics.setWidget(source_editor)
source_editor_graphics.setZValue(-1)
return source_editor_graphics

@property
def _editor_widget_height(self):
return self.height - self.title_height - 2*self.edge_size \
- self.output_panel_height

@_editor_widget_height.setter
def _editor_widget_height(self, value: int):
self.output_panel_height = self.height - value - self.title_height - 2*self.edge_size

def update_all(self):
""" Update the code block parts. """
if hasattr(self, 'source_editor'):
Expand All @@ -38,17 +66,143 @@ def update_all(self):
int(self.edge_size),
int(self.edge_size + self.title_height),
int(self._width - 2*self.edge_size),
int(self.height - self.title_height - 2*self.edge_size)
int(self._editor_widget_height)
)
display_widget = self.display.widget()
display_widget.setGeometry(
int(self.edge_size),
int(self.height - self.output_panel_height - self.edge_size),
int(self.width - 2*self.edge_size),
int(self.output_panel_height)
)
super().update_all()

@property
def source(self) -> str:
""" Source code. """
return self._source

@source.setter
def source(self, value:str):
self._source = value
if hasattr(self, 'source_editor'):
editor_widget = self.source_editor.widget()
editor_widget.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, 'source_editor'):
# If there is a text output, erase the image output and display the text output
self.image = ""
editor_widget = self.display.widget()
editor_widget.setText(self._stdout)

@property
def image(self) -> str:
""" Code output. """
return self._image

@image.setter
def image(self, value:str):
self._image = value
if hasattr(self, 'source_editor') and self.image != "":
# If there is an image output, erase the text output and display the image output
editor_widget = self.display.widget()
editor_widget.setText("")
qlabel = editor_widget
ba = QByteArray.fromBase64(str.encode(self.image))
pixmap = QPixmap()
pixmap.loadFromData(ba)
qlabel.setPixmap(pixmap)

@source.setter
def source(self, value:str):
self._source = value
if hasattr(self, 'source_editor'):
editor_widget = self.source_editor.widget()
editor_widget.setText(self._source)

def paint(self, painter: QPainter,
option: QStyleOptionGraphicsItem, #pylint:disable=unused-argument
widget: Optional[QWidget]=None): #pylint:disable=unused-argument
""" Paint the code output panel """
super().paint(painter, option, widget)
path_title = QPainterPath()
path_title.setFillRule(Qt.FillRule.WindingFill)
path_title.addRoundedRect(0, 0, self.width, self.height,
self.edge_size, self.edge_size)
painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self._brush_background)
painter.drawPath(path_title.simplified())

def _is_in_resize_source_code_area(self, pos:QPointF):
"""
Return True if the given position is in the area
used to resize the source code widget
"""
source_editor_start = self.height - self.output_panel_height - self.edge_size

return self.width - self.edge_size/2 < pos.x() and \
source_editor_start - self.edge_size < pos.y() < source_editor_start + self.edge_size


def _is_in_resize_area(self, pos:QPointF):
""" Return True if the given position is in the block resize_area. """

# This block features 2 resizing areas with 2 different behaviors
is_in_bottom_left = super()._is_in_resize_area(pos)
return is_in_bottom_left or self._is_in_resize_source_code_area(pos)

def _start_resize(self,pos:QPointF):
self.resizing = True
self.resize_start = pos
if self._is_in_resize_source_code_area(pos):
self.resizing_source_code = True
QApplication.setOverrideCursor(Qt.CursorShape.SizeFDiagCursor)

def _stop_resize(self):
self.resizing = False
self.resizing_source_code = False
QApplication.restoreOverrideCursor()

def mouseMoveEvent(self, event:QGraphicsSceneMouseEvent):
"""
We override the default resizing behavior as the code part and the display part of the block
block can be resized independently.
"""
if self.resizing:
delta = event.pos() - self.resize_start
self.width = max(self.width + delta.x(), self._min_width)

height_delta = max(delta.y(),
# List of all the quantities that must remain negative.
# Mainly: min_height - height must be negative for all elements
self._min_output_panel_height - self.output_panel_height,
self._min_height - self.height,
self._min_source_editor_height - self._editor_widget_height
)

self.height += height_delta
if not self.resizing_source_code:
self.output_panel_height += height_delta

self.resize_start = event.pos()
self.title_graphics.setTextWidth(self.width - 2 * self.edge_size)
self.update()

self.moved = True
super().mouseMoveEvent(event)

def init_display(self):
""" Initialize the output display widget: QLabel """
display_graphics = QGraphicsProxyWidget(self)
display = QLabel()
display.setText("")
display_graphics.setWidget(display)
display_graphics.setZValue(-1)
return display_graphics
14 changes: 9 additions & 5 deletions opencodeblocks/graphics/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,12 @@ def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True):
if restore_id:
self.id = data['id']
self.path_type = data['path_type']
self.source_socket = hashmap[data['source']['socket']]
self.source_socket.add_edge(self)
self.destination_socket = hashmap[data['destination']['socket']]
self.destination_socket.add_edge(self)
self.update_path()
try:
self.source_socket = hashmap[data['source']['socket']]
self.source_socket.add_edge(self)

self.destination_socket = hashmap[data['destination']['socket']]
self.destination_socket.add_edge(self)
self.update_path()
except KeyError:
self.remove()
Loading

0 comments on commit bf563a7

Please sign in to comment.