Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
150c5a1
feat(breakpoints): add BreakpointManager for breakpoint tracking
MSherbinii Jan 15, 2026
83ff841
feat(breakpoints): integrate BreakpointManager into ExecutionEngine
MSherbinii Jan 15, 2026
8fe0808
feat(breakpoints): add breakpoint check in State.start() for all stat…
MSherbinii Jan 15, 2026
edf7d04
feat(breakpoints): add breakpoints panel view
MSherbinii Jan 15, 2026
5c557aa
feat(breakpoints): add breakpoints panel controller
MSherbinii Jan 15, 2026
af79608
feat(breakpoints): integrate breakpoints panel into main window
MSherbinii Jan 15, 2026
2495959
feat(breakpoints): add breakpoint checkbox to state overview panel
MSherbinii Jan 15, 2026
5339279
feat(breakpoints): add visual indicator for breakpointed states
MSherbinii Jan 15, 2026
2279962
refactor(breakpoints): move gui_singletons import to top level
MSherbinii Jan 15, 2026
1edc9a0
fix(breakpoints): pause before state execution
MSherbinii Jan 29, 2026
71a2a19
fix(breakpoints): handle None file_system_path for unsaved state mach…
MSherbinii Feb 11, 2026
8778d57
fix(breakpoints): warn user when setting breakpoint on unsaved state …
MSherbinii Feb 12, 2026
28b9351
feat(breakpoints): add breakpoint toggle to right-click state menu
MSherbinii Feb 12, 2026
ee6c12e
feat(breakpoints): instant canvas repaint on breakpoint change
MSherbinii Feb 12, 2026
233b675
fix(breakpoints): improve breakpoint dot size and positioning
MSherbinii Feb 12, 2026
f7a0451
test: add unit tests for breakpoints feature
MSherbinii Mar 2, 2026
4825bdb
test(breakpoints): add GUI unit test for breakpoints panel
MSherbinii Mar 26, 2026
2654188
fix(breakpoints): move singleton imports inside test function to avoi…
MSherbinii Mar 26, 2026
a055902
test(breakpoints): add individual breakpoint removal test
MSherbinii Mar 26, 2026
77321ae
fix(breakpoints): sync graphical editor and state editor on breakpoin…
MSherbinii Mar 30, 2026
7a3d165
Merge branch 'develop' into feat/breakpoints
flolay Apr 2, 2026
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
78 changes: 78 additions & 0 deletions source/rafcon/core/execution/breakpoint_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import os
from rafcon.utils import log

logger = log.get_logger(__name__)


class BreakpointManager:

def __init__(self):
self._breakpoints = {}
self._listeners = []

def add_listener(self, callback):
if callback not in self._listeners:
self._listeners.append(callback)

def remove_listener(self, callback):
self._listeners = [l for l in self._listeners if l is not callback]

def _notify(self):
for listener in list(self._listeners):
listener()

@staticmethod
def _get_state_id(state):
if state.file_system_path is None:
return None
return os.path.basename(state.file_system_path)

def add_breakpoint(self, state, display_name):
state_id = self._get_state_id(state)
self._breakpoints[state_id] = {
'enabled': True,
'name': display_name,
'display_path': state.file_system_path
}
logger.info(f"✓ Breakpoint: {display_name}")
logger.info(f" Path: {state.file_system_path}")
self._notify()

def remove_breakpoint(self, state):
state_id = self._get_state_id(state)
self.remove_breakpoint_by_id(state_id)

def remove_breakpoint_by_id(self, state_id):
if state_id in self._breakpoints:
del self._breakpoints[state_id]
self._notify()

def toggle_breakpoint(self, state_id):
if state_id in self._breakpoints:
self._breakpoints[state_id]['enabled'] = not self._breakpoints[state_id]['enabled']
self._notify()

def clear_all(self):
self._breakpoints.clear()
self._notify()

def should_pause(self, state):
state_id = self._get_state_id(state)
if state_id is None:
return False
if state_id in self._breakpoints:
return self._breakpoints[state_id]['enabled']
return False

def get_all_breakpoints(self):
return dict(self._breakpoints)

def disable_all(self):
for bp in self._breakpoints.values():
bp['enabled'] = False
self._notify()

def enable_all(self):
for bp in self._breakpoints.values():
bp['enabled'] = True
self._notify()
1 change: 1 addition & 0 deletions source/rafcon/core/execution/execution_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def __init__(self, state_machine_manager):
self.new_execution_command_handled = True
self.stop_state_machine_after_finishing_step = False
self.running_only_selected_state = False
self.breakpoint_manager = BreakpointManager()
self._replay_context = None

@Observable.observed
Expand Down
10 changes: 10 additions & 0 deletions source/rafcon/core/states/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from yaml import YAMLObject

from rafcon.core.id_generator import *
from rafcon.core.singleton import state_machine_execution_engine
from rafcon.core.state_elements.state_element import StateElement
from rafcon.core.state_elements.data_port import DataPort, InputDataPort, OutputDataPort
from rafcon.core.state_elements.logical_port import Income, Outcome
Expand Down Expand Up @@ -261,6 +262,15 @@ def start(self, execution_history, backward_execution=False, generate_run_id=Tru

:return:
"""
if state_machine_execution_engine.breakpoint_manager.should_pause(self):
logger.info(f"Breakpoint hit: {self.name}")
if self.parent and self.parent.last_child:
self.parent.last_child.state_execution_status = StateExecutionStatus.WAIT_FOR_NEXT_STATE
state_machine_execution_engine.pause()
state_machine_execution_engine._wait_while_in_pause_or_in_step_mode()
if self.parent and self.parent.last_child:
self.parent.last_child.state_execution_status = StateExecutionStatus.INACTIVE

self.execution_history = execution_history
if generate_run_id:
self._run_id = run_id_generator()
Expand Down
139 changes: 139 additions & 0 deletions source/rafcon/gui/controllers/breakpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
from gi.repository import Gtk
from gi.repository import GObject

from rafcon.core.singleton import state_machine_execution_engine
from rafcon.gui.controllers.utils.extended_controller import ExtendedController
from rafcon.gui.models.state_machine_manager import StateMachineManagerModel
from rafcon.gui.views.breakpoints import BreakpointsView
from rafcon.utils import log

logger = log.get_logger(__name__)


class BreakpointsController(ExtendedController):
"""Controller for the breakpoints panel.

Manages the list of breakpoints, allowing users to enable/disable
or remove them.
"""

# TreeStore column indices
COL_ENABLED = 0 # checkbox
COL_NAME = 1 # state name
COL_PATH = 2 # display path
COL_STATE_ID = 3 # state ID (hidden, used as key)

def __init__(self, model=None, view=None):
assert isinstance(model, StateMachineManagerModel)
assert isinstance(view, BreakpointsView)

super(BreakpointsController, self).__init__(model, view)

# Create list store: [enabled (bool), name (str), path (str), state_id (str)]
self.breakpoints_store = Gtk.ListStore(GObject.TYPE_BOOLEAN, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING)

# Get tree view from view
self.breakpoints_tree = view['breakpoints_tree']
self.breakpoints_tree.set_model(self.breakpoints_store)

# Setup columns
self._setup_tree_columns()

# Initial update
self.update()

def _setup_tree_columns(self):
"""Setup the tree view columns"""
# Column 1: Enabled checkbox
renderer_toggle = Gtk.CellRendererToggle()
renderer_toggle.connect("toggled", self.on_breakpoint_toggled)
column_enabled = Gtk.TreeViewColumn("Enabled", renderer_toggle, active=self.COL_ENABLED)
self.breakpoints_tree.append_column(column_enabled)

# Column 2: State name
renderer_text = Gtk.CellRendererText()
column_name = Gtk.TreeViewColumn("State", renderer_text, text=self.COL_NAME)
column_name.set_expand(True)
self.breakpoints_tree.append_column(column_name)

# Column 3: Path (displayed)
renderer_path = Gtk.CellRendererText()
column_path = Gtk.TreeViewColumn("Path", renderer_path, text=self.COL_PATH)
column_path.set_expand(True)
self.breakpoints_tree.append_column(column_path)

# Note: COL_STATE_ID (column 4) is hidden, used only as key for lookups

def register_view(self, view):
"""Connect button signals"""
super(BreakpointsController, self).register_view(view)
view['remove_button'].connect('clicked', self.on_remove_selected)
view['remove_all_button'].connect('clicked', self.on_remove_all)
view['refresh_button'].connect('clicked', self.on_refresh)
view['toggle_all_button'].connect('toggled', self.on_toggle_all)

def update(self):
"""Refresh the breakpoints list from the breakpoint manager"""
self.breakpoints_store.clear()

# Get all breakpoints from the execution engine
breakpoints = state_machine_execution_engine.breakpoint_manager.get_all_breakpoints()

# Add each breakpoint to the list
for state_id, info in breakpoints.items():
self.breakpoints_store.append([
info['enabled'],
info['name'],
info.get('display_path', ''),
state_id
])

def on_breakpoint_toggled(self, widget, path):
"""Toggle breakpoint enabled/disabled"""
# Get the row
tree_iter = self.breakpoints_store.get_iter(path)
state_id = self.breakpoints_store.get_value(tree_iter, self.COL_STATE_ID)

# Toggle in breakpoint manager
state_machine_execution_engine.breakpoint_manager.toggle_breakpoint(state_id)

# Update display
self.update()

def on_remove_selected(self, widget):
"""Remove selected breakpoint"""
selection = self.breakpoints_tree.get_selection()
model, tree_iter = selection.get_selected()

if tree_iter is None:
logger.info("No breakpoint selected to remove")
return

# Get state ID
state_id = model.get_value(tree_iter, self.COL_STATE_ID)

# Remove from breakpoint manager (triggers _notify to update graphical editor)
state_machine_execution_engine.breakpoint_manager.remove_breakpoint_by_id(state_id)

# Update display
self.update()

def on_remove_all(self, widget):
"""Remove all breakpoints"""
state_machine_execution_engine.breakpoint_manager.clear_all()
self.update()
logger.info("All breakpoints removed")

def on_refresh(self, widget):
"""Refresh the breakpoints list"""
self.update()

def on_toggle_all(self, toggle_button):
"""Toggle all breakpoints on/off"""
if toggle_button.get_active():
state_machine_execution_engine.breakpoint_manager.disable_all()
toggle_button.set_label("Enable All")
else:
state_machine_execution_engine.breakpoint_manager.enable_all()
toggle_button.set_label("Disable All")
self.update()
8 changes: 8 additions & 0 deletions source/rafcon/gui/controllers/graphical_editor_gaphas.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import math

from rafcon.core.decorators import lock_state_machine
from rafcon.core.singleton import state_machine_execution_engine
from rafcon.core.states.state import StateType
from rafcon.gui.clipboard import global_clipboard
from rafcon.gui.controllers.utils.extended_controller import ExtendedController
Expand Down Expand Up @@ -92,6 +93,7 @@ def __init__(self, model, view):
"".format(time.time() - start_time, self.model.state_machine_id))

def destroy(self):
state_machine_execution_engine.breakpoint_manager.remove_listener(self._on_breakpoint_changed)
if self.view:
self.view.editor.prepare_destruction()
super(GraphicalEditorController, self).destroy()
Expand All @@ -105,6 +107,8 @@ def register_view(self, view):
self.view.editor.connect("drag-data-received", self.on_drag_data_received)
self.drag_motion_handler_id = self.view.editor.connect("drag-motion", self.on_drag_motion)

state_machine_execution_engine.breakpoint_manager.add_listener(self._on_breakpoint_changed)

try:
self.setup_canvas()
except:
Expand Down Expand Up @@ -212,6 +216,10 @@ def on_drag_motion(self, widget, context, x, y, time):
if not rafcon.gui.singleton.global_gui_config.get_config_value('DRAG_N_DROP_WITH_FOCUS'):
self.view.editor.handler_unblock(self.focus_changed_handler_id)

def _on_breakpoint_changed(self):
self.canvas.update_root_items()
self.canvas.update_now()

def update_view(self, *args, **kwargs):
self.canvas.update_root_items()

Expand Down
7 changes: 7 additions & 0 deletions source/rafcon/gui/controllers/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from rafcon.core.execution.execution_status import StateMachineExecutionStatus
from rafcon.gui.config import global_gui_config as gui_config
from rafcon.gui.controllers.execution_history import ExecutionHistoryTreeController
from rafcon.gui.controllers.breakpoints import BreakpointsController
from rafcon.gui.controllers.global_variable_manager import GlobalVariableManagerController
from rafcon.gui.controllers.library_tree import LibraryTreeController
from rafcon.gui.controllers.menu_bar import MenuBarController
Expand Down Expand Up @@ -162,6 +163,12 @@ def __init__(self, state_machine_manager_model, view):
execution_history_ctrl = ExecutionHistoryTreeController(state_machine_manager_model, view.execution_history)
self.add_controller('execution_history_ctrl', execution_history_ctrl)

######################################################
# breakpoints
######################################################
breakpoints_ctrl = BreakpointsController(state_machine_manager_model, view.breakpoints)
self.add_controller('breakpoints_ctrl', breakpoints_ctrl)

######################################################
# execution ticker
######################################################
Expand Down
24 changes: 24 additions & 0 deletions source/rafcon/gui/controllers/right_click_menu/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from functools import partial

import rafcon.core.singleton as core_singletons
from rafcon.core.singleton import state_machine_execution_engine
from rafcon.core.states.barrier_concurrency_state import BarrierConcurrencyState
from rafcon.core.states.preemptive_concurrency_state import PreemptiveConcurrencyState
import rafcon.gui.singleton as gui_singletons
Expand Down Expand Up @@ -187,6 +188,7 @@ def generate_right_click_menu_state(self):
save_as_library_sub_menu.append(create_menu_item(library_root_key, constants.SIGN_LIB,
callback_function,
accel_code=None, accel_group=accel_group))
self.insert_breakpoint_in_menu(menu)
menu.append(Gtk.SeparatorMenuItem())
callback_function = partial(self.on_change_background_color, state_model=selected_state_m)
menu.append(create_menu_item("Change Background Color",
Expand Down Expand Up @@ -216,6 +218,17 @@ def insert_is_start_state_in_menu(self, menu, shortcuts_dict, accel_group):
menu.append(create_check_menu_item("Is start state", selected_state_m.is_start, self.on_toggle_is_start_state,
accel_code=shortcuts_dict['is_start_state'][0], accel_group=accel_group))

def insert_breakpoint_in_menu(self, menu):
selection = gui_singletons.state_machine_manager_model.get_selected_state_machine_model().selection
if len(selection.states) != 1:
return
selected_state_m = selection.get_selected_state()
bm = state_machine_execution_engine.breakpoint_manager
state_id = bm._get_state_id(selected_state_m.state)
is_set = state_id is not None and state_id in bm.get_all_breakpoints()
label = "Disable Breakpoint" if is_set else "Set Breakpoint"
menu.append(create_menu_item(label, constants.BUTTON_STOP, self.on_toggle_breakpoint))

def insert_execution_sub_menu_in_menu(self, menu, shortcuts_dict, accel_group):
execution_sub_menu_item, execution_sub_menu = append_sub_menu_to_parent_menu("Execution", menu,
constants.BUTTON_START)
Expand Down Expand Up @@ -278,6 +291,8 @@ def generate_right_click_menu_library(self):
sub_menu.append(create_menu_item("Take name from library", constants.BUTTON_EXCHANGE,
partial(self.on_substitute_library_with_template_activate, keep_name=False)))

self.insert_breakpoint_in_menu(menu)

menu.append(Gtk.SeparatorMenuItem())
selection = gui_singletons.state_machine_manager_model.get_selected_state_machine_model().selection
selected_state_m = selection.get_selected_state()
Expand All @@ -304,6 +319,15 @@ def on_select_library_tree_element(widget, date=None, state_m=None):
def on_toggle_is_start_state(self, widget, data=None):
self.shortcut_manager.trigger_action("is_start_state", None, None)

def on_toggle_breakpoint(self, widget, data=None):
selection = gui_singletons.state_machine_manager_model.get_selected_state_machine_model().selection
selected_state_m = selection.get_selected_state()
states_editor_ctrl = gui_singletons.main_window_controller.get_controller('states_editor_ctrl')
state_id = states_editor_ctrl.get_state_identifier(selected_state_m)
props_ctrl = states_editor_ctrl.tabs[state_id]['controller'].get_controller('properties_ctrl')
checkbox = props_ctrl.view['breakpoint_checkbox']
checkbox.set_active(not checkbox.get_active())

def on_add_execution_state_activate(self, widget=None, data=None):
self.shortcut_manager.trigger_action('add_execution_state', None, None, cursor_position=self.menu_position)

Expand Down
Loading