Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🎉 🎨 Add persistent colors #276

Merged
merged 21 commits into from
Feb 19, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b917251
:tada: :art: Add persitent colors
MathisFederico Feb 16, 2022
8fc2034
:hammer: Refactor ExecutableBlock.run_state using Enum
MathisFederico Feb 16, 2022
793b9fb
:hammer: Add Executable class
MathisFederico Feb 16, 2022
532b9ab
:wrench: Update mnist example
MathisFederico Feb 16, 2022
dbd1073
:art: Make the outline more visible
FabienRoger Feb 17, 2022
cad6fa2
:beetle: Don't mark blocks as executed after execution was canceled
FabienRoger Feb 17, 2022
2665ce2
:sparkles: Correct typing
FabienRoger Feb 17, 2022
51d8c6d
:sparkles: Remove useless import
FabienRoger Feb 17, 2022
e404261
:beetle: Fix multiple execution bug
FabienRoger Feb 18, 2022
4d01d8d
:sparkles: Remove unused variable
FabienRoger Feb 18, 2022
ceee345
:art: Changed selection and running/pending colors
MathisFederico Feb 19, 2022
63beaf0
:beetle: Run right now relauch next blocks even if they were done
MathisFederico Feb 19, 2022
f145bd0
:memo: Add executable docstrings
MathisFederico Feb 19, 2022
56ddc56
:sparkles: Fix pylint issues
MathisFederico Feb 19, 2022
746c450
:wrench: Refactor custom_bfs
MathisFederico Feb 19, 2022
3fbfc68
:wrench: Refactor right_traversal
MathisFederico Feb 19, 2022
c6dfbf2
Merge remote-tracking branch 'origin/dev' into feature/persistent_colors
MathisFederico Feb 19, 2022
0d58e55
:memo: Add missing docstrings
MathisFederico Feb 19, 2022
25a5c05
:sparkles: Fix some pylint issues
MathisFederico Feb 19, 2022
0d78e96
:art: Make the pending outline more visible
FabienRoger Feb 19, 2022
a2824b4
Merge remote-tracking branch 'origin/feature/persistent_colors' into …
FabienRoger Feb 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 35 additions & 91 deletions examples/mnist.ipyg

Large diffs are not rendered by default.

27 changes: 22 additions & 5 deletions pyflow/blocks/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ def __init__(
self.sockets_in: List[Socket] = []
self.sockets_out: List[Socket] = []

self._pen_outline = QPen(QColor("#7F000000"))
self._pen_outline_selected = QPen(QColor("#FFFFA637"))
self.pen_width = 3
self._pen_outline = QPen(QColor("#00000000"))
self._pen_outline.setWidth(self.pen_width)
self._pen_outline_selected = QPen(QColor("#800030FF"))
self._pen_outline_selected.setWidth(self.pen_width)
self._brush_background = QBrush(BACKGROUND_COLOR)

self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
Expand Down Expand Up @@ -138,12 +141,26 @@ def paint(
path_outline.addRoundedRect(
0, 0, self.width, self.height, self.edge_size, self.edge_size
)
painter.setPen(
self._pen_outline_selected if self.isSelected() else self.pen_outline
)
painter.setPen(self.pen_outline)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawPath(path_outline.simplified())

# selection inner outline
if self.isSelected():
path_in_outline = QPainterPath()
outline_width = self.pen_outline.widthF()
path_in_outline.addRoundedRect(
-2 * outline_width,
-2 * outline_width,
self.width + 4 * outline_width,
self.height + 4 * outline_width,
self.edge_size + 2 * outline_width,
self.edge_size + 2 * outline_width,
)
painter.setPen(self._pen_outline_selected)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawPath(path_in_outline.simplified())

def add_socket(self, socket: Socket):
"""Add a socket to the block."""
if socket.socket_type == "input":
Expand Down
33 changes: 21 additions & 12 deletions pyflow/blocks/codeblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from pyflow.blocks.block import Block
from pyflow.core.edge import Edge
from pyflow.blocks.executableblock import ExecutableBlock
from pyflow.blocks.executableblock import ExecutableBlock, ExecutableState
from pyflow.blocks.pyeditor import PythonEditor
from pyflow.core.add_button import AddEdgeButton, AddNewBlockButton

Expand Down Expand Up @@ -62,14 +62,17 @@ def __init__(self, source: str = "", **kwargs):
self.output_closed = True
self._splitter_size = [1, 1]
self._cached_stdout = ""
self.has_been_run = False
self.blocks_to_run = []

self._pen_outlines = [
QPen(QColor("#7F000000")), # Idle
QPen(QColor("#FF0000")), # Running
QPen(QColor("#00ff00")), # Transmitting
]
self._pen_outlines = {
ExecutableState.IDLE: QPen(QColor("#00000000")), # No outline
ExecutableState.RUNNING: QPen(QColor("#ff6107ff")), # Purple
ExecutableState.PENDING: QPen(QColor("#80fc6107")), # Dark orange
ExecutableState.DONE: QPen(QColor("#158000")), # Dark green
ExecutableState.CRASHED: QPen(QColor("#ff0000")), # Red: Crashed
}
for pen in self._pen_outlines.values():
pen.setWidth(self.pen_width)

self.output_panel_background_color = "#1E1E1E"

Expand Down Expand Up @@ -133,14 +136,14 @@ def init_add_newblock_button(self):

def handle_run_right(self):
"""Called when the button for "Run All" was pressed."""
if self.run_state != 0:
if self.run_state in (ExecutableState.PENDING, ExecutableState.RUNNING):
self._interrupt_execution()
else:
self.run_right()

def handle_run_left(self):
"""Called when the button for "Run Left" was pressed."""
if self.run_state != 0:
if self.run_state in (ExecutableState.PENDING, ExecutableState.RUNNING):
self._interrupt_execution()
else:
self.run_left()
Expand Down Expand Up @@ -170,10 +173,17 @@ def run_code(self):
super().run_code() # actually run the code

def execution_finished(self):
"""Reset the text of the run buttons after it was executed."""
super().execution_finished()
self.run_button.setText(">")
self.run_all_button.setText(">>")

def execution_canceled(self):
"""Reset the text of the run buttons after it was canceled."""
super().execution_canceled()
self.run_button.setText(">")
self.run_all_button.setText(">>")

def link(self, block: "ExecutableBlock"):
"""Link a block to the current one."""
# Add sockets to the new block and the current one
Expand Down Expand Up @@ -259,9 +269,8 @@ def source(self, value: str):
if value != self._source:
# If text has changed, set self and all output blocks to not run
output_blocks, _ = self.custom_bfs(self, reverse=True)
for block in output_blocks:
block.has_been_run = False
self.has_been_run = False
for block in output_blocks + [self]:
block.run_state = ExecutableState.IDLE
self.source_editor.setText(value)
self._source = value

Expand Down
73 changes: 24 additions & 49 deletions pyflow/blocks/executableblock.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
from pyflow.blocks.block import Block
from pyflow.core.socket import Socket
from pyflow.core.edge import Edge
from pyflow.core.executable import Executable, ExecutableState


class ExecutableBlock(Block):
class ExecutableBlock(Block, Executable):

"""
Executable Block
Expand All @@ -35,10 +36,8 @@ def __init__(self, **kwargs):
Create a new executable block.
Do not call this method except when inheriting from this class.
"""
super().__init__(**kwargs)

self.has_been_run = False
self._run_state = 0
Block.__init__(self, **kwargs)
Executable.__init__(self)

# Each element is a list of blocks/edges to be animated
# Running will paint each element one after the other
Expand Down Expand Up @@ -85,19 +84,25 @@ def run_code(self):

if kernel.busy is False:
kernel.run_queue()
self.has_been_run = True
self.run_state = ExecutableState.PENDING

def execution_finished(self):
"""Reset the text of the run buttons."""
self.run_state = 0
"""Reset the state of the block after it was executed."""
if self.run_state != ExecutableState.CRASHED:
self.run_state = ExecutableState.DONE
self.blocks_to_run = []

def execution_canceled(self):
"""Reset the state of the block after its execution was canceled."""
if self.run_state != ExecutableState.CRASHED:
self.run_state = ExecutableState.IDLE
self.blocks_to_run = []

def _interrupt_execution(self):
"""Interrupt an execution, reset the blocks in the queue."""
for block, _ in self.scene().kernel.execution_queue:
# Reset the blocks that have not been run
block.reset_has_been_run()
block.execution_finished()
block.execution_canceled()
# Clear kernel execution queue
self.scene().kernel.execution_queue = []
# Interrupt the kernel
Expand All @@ -110,9 +115,6 @@ def transmitting_animation_in(self):
Animate the visual flow
Set color to transmitting and set a timer before switching to normal
"""
for elem in self.transmitting_queue[0]:
# Set color to transmitting
elem.run_state = 2
QApplication.processEvents()
QTimer.singleShot(self.transmitting_delay, self.transmitting_animation_out)

Expand All @@ -121,14 +123,6 @@ def transmitting_animation_out(self):
Animate the visual flow
After the timer, set color to normal and move on with the queue
"""
for elem in self.transmitting_queue[0]:
# Reset color only if the block will not be run
if hasattr(elem, "has_been_run"):
if elem.has_been_run is True:
elem.run_state = 0
else:
elem.run_state = 0

QApplication.processEvents()
self.transmitting_queue.pop(0)
if self.transmitting_queue:
Expand Down Expand Up @@ -270,17 +264,19 @@ def right_traversal(self):

def run_blocks(self):
"""Run a list of blocks."""
for block in self.blocks_to_run[::-1]:
if not block.has_been_run:
for block in self.blocks_to_run[::-1] + [self]:
if block.run_state not in {
ExecutableState.PENDING,
ExecutableState.RUNNING,
ExecutableState.DONE,
}:
block.run_code()
if not self.has_been_run:
self.run_code()

def run_left(self):
"""Run all of the block's dependencies and then run the block."""

# Reset has_been_run to make sure that the self is run again
self.has_been_run = False
# Reset state to make sure that the self is run again
self.run_state = ExecutableState.IDLE

# To avoid crashing when spamming the button
if self.transmitting_queue:
Expand Down Expand Up @@ -326,12 +322,9 @@ def run_right(self):
# Start transmitting animation
self.transmitting_animation_in()

def reset_has_been_run(self):
"""Called when the output is an error."""
self.has_been_run = False

def error_occured(self):
"""Interrupt the kernel if an error occured"""
self.run_state = ExecutableState.CRASHED
self._interrupt_execution()

@property
Expand All @@ -345,24 +338,6 @@ def source(self) -> str:
def source(self, value: str):
raise NotImplementedError("source(self) should be overriden")

@property
def run_state(self) -> int:
"""Run state.

Describe the current state of the ExecutableBlock:
- 0: idle.
- 1: running.
- 2: transmitting.

"""
return self._run_state

@run_state.setter
def run_state(self, value: int):
self._run_state = value
# Update to force repaint
self.update()

def handle_stdout(self, value: str):
"""Handle the stdout signal."""

Expand Down
48 changes: 17 additions & 31 deletions pyflow/core/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@

from pyflow.core.serializable import Serializable
from pyflow.core.socket import Socket
from pyflow.core.executable import Executable, ExecutableState


class Edge(QGraphicsPathItem, Serializable):
class Edge(QGraphicsPathItem, Serializable, Executable):

"""Base class for directed edges in Pyflow."""

Expand All @@ -28,12 +29,12 @@ class Edge(QGraphicsPathItem, Serializable):

def __init__(
self,
edge_width: float = 4.0,
edge_width: float = 6.0,
path_type=DEFAULT_DATA["path_type"],
edge_color="#001000",
edge_selected_color="#00ff00",
edge_selected_color="#0030FF",
edge_running_color="#FF0000",
edge_transmitting_color="#00ff00",
edge_pending_color="#00ff00",
source: QPointF = QPointF(0, 0),
destination: QPointF = QPointF(0, 0),
source_socket: Socket = None,
Expand All @@ -55,6 +56,8 @@ def __init__(

Serializable.__init__(self)
QGraphicsPathItem.__init__(self, parent=None)
Executable.__init__(self)

self._pen = QPen(QColor(edge_color))
self._pen.setWidthF(edge_width)

Expand All @@ -68,13 +71,14 @@ def __init__(
self._pen_running = QPen(QColor(edge_running_color))
self._pen_running.setWidthF(edge_width)

self._pen_transmitting = QPen(QColor(edge_transmitting_color))
self._pen_transmitting.setWidthF(edge_width)

self.pens = [self._pen, self._pen_running, self._pen_transmitting]
self._pen_pending = QPen(QColor(edge_pending_color))
self._pen_pending.setWidthF(edge_width)

# 0 for normal, 1 for running, 2 for transmitting
self.run_state = 0
self.state_pens = {
ExecutableState.IDLE: self._pen,
ExecutableState.RUNNING: self._pen_running,
ExecutableState.PENDING: self._pen_pending,
}

self.setFlag(QGraphicsPathItem.GraphicsItemFlag.ItemIsSelectable)
self.setZValue(-1)
Expand Down Expand Up @@ -117,16 +121,16 @@ def paint(
self,
painter: QPainter,
option: QStyleOptionGraphicsItem, # pylint:disable=unused-argument
widget: Optional[QWidget] = None,
): # pylint:disable=unused-argument
widget: Optional[QWidget] = None, # pylint:disable=unused-argument
):
"""Paint the edge."""
self.update_path()
if self.isSelected():
pen = self._pen_selected
elif self.destination_socket is None:
pen = self._pen_dragging
else:
pen = self.pens[self.run_state]
pen = self.state_pens[self.run_state]
painter.setPen(pen)
painter.setBrush(Qt.BrushStyle.NoBrush)
painter.drawPath(self.path())
Expand Down Expand Up @@ -257,21 +261,3 @@ def deserialize(self, data: OrderedDict, hashmap: dict = None, restore_id=True):
self.update_path()
except KeyError:
self.remove()

@property
def run_state(self) -> int:
"""Run state.

Describe the current state of the Edge:
- 0: idle.
- 1: running.
- 2: transmitting.

"""
return self._run_state

@run_state.setter
def run_state(self, value: int):
self._run_state = value
# Update to force repaint
self.update()
27 changes: 27 additions & 0 deletions pyflow/core/executable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from enum import Enum


class ExecutableState(Enum):
IDLE = 0
RUNNING = 1
PENDING = 2
DONE = 3
CRASHED = 4


class Executable:
def __init__(self) -> None:
self._run_state = ExecutableState.IDLE

@property
def run_state(self) -> ExecutableState:
"""The current state of the Executable."""
return self._run_state

@run_state.setter
def run_state(self, value: ExecutableState):
assert isinstance(value, ExecutableState)
self._run_state = value
# Update to force repaint if available
if hasattr(self, "update"):
self.update()
Loading