easycoder 251105.1__py2.py3-none-any.whl → 260111.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easycoder/__init__.py +6 -3
- easycoder/debugger/__init__.py +5 -0
- easycoder/debugger/ec_dbg_value_display copy.py +195 -0
- easycoder/debugger/ec_dbg_value_display.py +24 -0
- easycoder/debugger/ec_dbg_watch_list copy.py +219 -0
- easycoder/debugger/ec_dbg_watchlist.py +293 -0
- easycoder/debugger/ec_debug.py +1025 -0
- easycoder/ec_classes.py +487 -11
- easycoder/ec_compiler.py +81 -44
- easycoder/ec_condition.py +1 -1
- easycoder/ec_core.py +1042 -1081
- easycoder/ec_gclasses.py +236 -0
- easycoder/ec_graphics.py +1683 -0
- easycoder/ec_handler.py +18 -14
- easycoder/ec_mqtt.py +248 -0
- easycoder/ec_program.py +297 -168
- easycoder/ec_psutil.py +48 -0
- easycoder/ec_value.py +65 -47
- easycoder/pre/README.md +3 -0
- easycoder/pre/__init__.py +17 -0
- easycoder/pre/debugger/__init__.py +5 -0
- easycoder/pre/debugger/ec_dbg_value_display copy.py +195 -0
- easycoder/pre/debugger/ec_dbg_value_display.py +24 -0
- easycoder/pre/debugger/ec_dbg_watch_list copy.py +219 -0
- easycoder/pre/debugger/ec_dbg_watchlist.py +293 -0
- easycoder/{ec_debug.py → pre/debugger/ec_debug.py} +418 -185
- easycoder/pre/ec_border.py +67 -0
- easycoder/pre/ec_classes.py +470 -0
- easycoder/pre/ec_compiler.py +291 -0
- easycoder/pre/ec_condition.py +27 -0
- easycoder/pre/ec_core.py +2772 -0
- easycoder/pre/ec_gclasses.py +230 -0
- easycoder/{ec_pyside.py → pre/ec_graphics.py} +583 -433
- easycoder/pre/ec_handler.py +79 -0
- easycoder/pre/ec_keyboard.py +439 -0
- easycoder/pre/ec_program.py +557 -0
- easycoder/pre/ec_psutil.py +48 -0
- easycoder/pre/ec_timestamp.py +11 -0
- easycoder/pre/ec_value.py +124 -0
- easycoder/pre/icons/close.png +0 -0
- easycoder/pre/icons/exit.png +0 -0
- easycoder/pre/icons/run.png +0 -0
- easycoder/pre/icons/step.png +0 -0
- easycoder/pre/icons/stop.png +0 -0
- easycoder/pre/icons/tick.png +0 -0
- {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/METADATA +11 -1
- easycoder-260111.1.dist-info/RECORD +59 -0
- easycoder-251105.1.dist-info/RECORD +0 -24
- {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/WHEEL +0 -0
- {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/entry_points.txt +0 -0
- {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -4,20 +4,24 @@ from PySide6.QtWidgets import (
|
|
|
4
4
|
QWidget,
|
|
5
5
|
QFrame,
|
|
6
6
|
QHBoxLayout,
|
|
7
|
+
QGridLayout,
|
|
7
8
|
QVBoxLayout,
|
|
8
9
|
QLabel,
|
|
9
10
|
QSplitter,
|
|
10
11
|
QMessageBox,
|
|
11
12
|
QScrollArea,
|
|
13
|
+
QScrollBar,
|
|
12
14
|
QSizePolicy,
|
|
13
15
|
QToolBar,
|
|
14
16
|
QPushButton,
|
|
15
|
-
QInputDialog
|
|
17
|
+
QInputDialog,
|
|
18
|
+
QTabWidget
|
|
16
19
|
)
|
|
17
20
|
from PySide6.QtGui import QTextCursor, QIcon
|
|
18
21
|
from PySide6.QtCore import Qt, QTimer
|
|
19
|
-
from typing import Any
|
|
20
22
|
from typing import Any, Optional
|
|
23
|
+
from .ec_dbg_value_display import ValueDisplay
|
|
24
|
+
from .ec_dbg_watchlist import WatchListWidget
|
|
21
25
|
|
|
22
26
|
class Object():
|
|
23
27
|
def __setattr__(self, name: str, value: Any) -> None:
|
|
@@ -38,6 +42,24 @@ class Debugger(QMainWindow):
|
|
|
38
42
|
def write(self, text: str):
|
|
39
43
|
if not text:
|
|
40
44
|
return
|
|
45
|
+
|
|
46
|
+
# Echo all output to original stdout with proper line breaks
|
|
47
|
+
try:
|
|
48
|
+
if self.debugger._orig_stdout:
|
|
49
|
+
self.debugger._orig_stdout.write(text)
|
|
50
|
+
self.debugger._orig_stdout.flush()
|
|
51
|
+
except Exception:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
# Check if this looks like an error message - if so, also write to original stderr
|
|
55
|
+
if any(err_marker in text for err_marker in ['Error', 'Traceback', 'Exception']):
|
|
56
|
+
try:
|
|
57
|
+
if self.debugger._orig_stderr:
|
|
58
|
+
self.debugger._orig_stderr.write(text)
|
|
59
|
+
self.debugger._orig_stderr.flush()
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
41
63
|
# Buffer text and request a flush on the GUI timer
|
|
42
64
|
self._buf.append(text)
|
|
43
65
|
if self.debugger._flush_timer and not self.debugger._flush_timer.isActive():
|
|
@@ -54,100 +76,40 @@ class Debugger(QMainWindow):
|
|
|
54
76
|
super().__init__(parent)
|
|
55
77
|
self.debugger = parent
|
|
56
78
|
layout = QVBoxLayout(self)
|
|
57
|
-
|
|
58
|
-
# Create toolbar with icon buttons
|
|
59
|
-
toolbar = QToolBar()
|
|
60
|
-
toolbar.setMovable(False)
|
|
61
|
-
|
|
62
|
-
# Get the icons directory path
|
|
63
|
-
icons_dir = os.path.join(os.path.dirname(__file__), 'icons')
|
|
64
|
-
|
|
65
|
-
# Run button
|
|
66
|
-
run_btn = QPushButton()
|
|
67
|
-
run_icon_path = os.path.join(icons_dir, 'run.png')
|
|
68
|
-
run_btn.setIcon(QIcon(run_icon_path))
|
|
69
|
-
run_btn.setToolTip("Run")
|
|
70
|
-
run_btn.clicked.connect(self.on_run_clicked)
|
|
71
|
-
toolbar.addWidget(run_btn)
|
|
72
|
-
|
|
73
|
-
# Step button
|
|
74
|
-
step_btn = QPushButton()
|
|
75
|
-
step_icon_path = os.path.join(icons_dir, 'step.png')
|
|
76
|
-
step_btn.setIcon(QIcon(step_icon_path))
|
|
77
|
-
step_btn.setToolTip("Step")
|
|
78
|
-
step_btn.clicked.connect(self.on_step_clicked)
|
|
79
|
-
toolbar.addWidget(step_btn)
|
|
80
|
-
|
|
81
|
-
# Stop button
|
|
82
|
-
stop_btn = QPushButton()
|
|
83
|
-
stop_icon_path = os.path.join(icons_dir, 'stop.png')
|
|
84
|
-
stop_btn.setIcon(QIcon(stop_icon_path))
|
|
85
|
-
stop_btn.setToolTip("Stop")
|
|
86
|
-
stop_btn.clicked.connect(self.on_stop_clicked)
|
|
87
|
-
toolbar.addWidget(stop_btn)
|
|
88
|
-
|
|
89
|
-
# Exit button
|
|
90
|
-
exit_btn = QPushButton()
|
|
91
|
-
exit_icon_path = os.path.join(icons_dir, 'exit.png')
|
|
92
|
-
exit_btn.setIcon(QIcon(exit_icon_path))
|
|
93
|
-
exit_btn.setToolTip("Exit")
|
|
94
|
-
exit_btn.clicked.connect(self.on_exit_clicked)
|
|
95
|
-
toolbar.addWidget(exit_btn)
|
|
96
|
-
|
|
97
79
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
watch_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
105
|
-
watch_layout = QHBoxLayout(watch_panel)
|
|
80
|
+
# Header panel
|
|
81
|
+
variable_panel = QFrame()
|
|
82
|
+
variable_panel.setFrameShape(QFrame.Shape.StyledPanel)
|
|
83
|
+
variable_panel.setStyleSheet("background-color: white;")
|
|
84
|
+
variable_panel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
85
|
+
watch_layout = QHBoxLayout(variable_panel)
|
|
106
86
|
watch_layout.setContentsMargins(4, 4, 4, 4)
|
|
107
87
|
watch_layout.setSpacing(4)
|
|
108
88
|
|
|
109
|
-
|
|
110
|
-
# Title label
|
|
111
89
|
title_label = QLabel("VARIABLES")
|
|
112
90
|
title_label.setStyleSheet("font-weight: bold; letter-spacing: 1px;")
|
|
113
91
|
watch_layout.addWidget(title_label)
|
|
114
|
-
|
|
115
|
-
# Stretch to push buttons right
|
|
116
92
|
watch_layout.addStretch()
|
|
117
93
|
|
|
118
|
-
|
|
119
|
-
add_btn = QPushButton()
|
|
94
|
+
add_btn = QPushButton("+")
|
|
120
95
|
add_btn.setToolTip("Add variable to watch")
|
|
121
|
-
# TODO: set add_btn.setIcon(QIcon(path)) when icon is available
|
|
122
|
-
add_btn.setText("+")
|
|
123
96
|
add_btn.setFixedSize(24, 24)
|
|
124
97
|
add_btn.clicked.connect(self.on_add_clicked)
|
|
125
98
|
watch_layout.addWidget(add_btn)
|
|
126
99
|
|
|
127
|
-
layout.addWidget(
|
|
128
|
-
|
|
129
|
-
# Watch list area (renders selected variables beneath the toolbar)
|
|
130
|
-
self.watch_list_widget = QWidget()
|
|
131
|
-
self.watch_list_layout = QVBoxLayout(self.watch_list_widget)
|
|
132
|
-
self.watch_list_layout.setContentsMargins(6, 2, 6, 2)
|
|
133
|
-
self.watch_list_layout.setSpacing(2)
|
|
134
|
-
layout.addWidget(self.watch_list_widget)
|
|
135
|
-
|
|
136
|
-
# Keep a simple set to prevent duplicate labels
|
|
137
|
-
self._watch_set = set()
|
|
100
|
+
layout.addWidget(variable_panel)
|
|
138
101
|
|
|
139
|
-
|
|
102
|
+
# Watch list widget
|
|
103
|
+
self.watch_list = WatchListWidget(self.debugger)
|
|
104
|
+
layout.addWidget(self.watch_list, 1)
|
|
140
105
|
|
|
141
106
|
def on_add_clicked(self):
|
|
142
|
-
# Build the variable list from the program. Prefer Program.symbols mapping.
|
|
143
107
|
try:
|
|
144
108
|
program = self.debugger.program # type: ignore[attr-defined]
|
|
145
|
-
# Fallback to scanning code if symbols is empty
|
|
146
109
|
items = []
|
|
147
110
|
if hasattr(program, 'symbols') and isinstance(program.symbols, dict) and program.symbols:
|
|
148
111
|
items = sorted([name for name in program.symbols.keys() if name and not name.endswith(':')])
|
|
149
112
|
else:
|
|
150
|
-
# Fallback heuristic: look for commands whose 'type' == 'symbol' (as per requirement)
|
|
151
113
|
for cmd in getattr(program, 'code', []):
|
|
152
114
|
try:
|
|
153
115
|
if cmd.get('type') == 'symbol' and 'name' in cmd:
|
|
@@ -158,80 +120,60 @@ class Debugger(QMainWindow):
|
|
|
158
120
|
if not items:
|
|
159
121
|
QMessageBox.information(self, "Add Watch", "No variables found in this program.")
|
|
160
122
|
return
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
123
|
+
|
|
124
|
+
# Use a custom chooser dialog with a visible list (already open)
|
|
125
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout, QListWidget, QDialogButtonBox
|
|
126
|
+
dlg = QDialog(self)
|
|
127
|
+
dlg.setWindowTitle("Add Watch")
|
|
128
|
+
v = QVBoxLayout(dlg)
|
|
129
|
+
lst = QListWidget(dlg)
|
|
130
|
+
lst.addItems(items)
|
|
131
|
+
lst.setSelectionMode(QListWidget.SelectionMode.SingleSelection)
|
|
132
|
+
if items:
|
|
133
|
+
lst.setCurrentRow(0)
|
|
134
|
+
# Allow double-click to accept immediately
|
|
135
|
+
def accept_double(item):
|
|
136
|
+
if item:
|
|
137
|
+
lst.setCurrentItem(item)
|
|
138
|
+
dlg.accept()
|
|
139
|
+
lst.itemDoubleClicked.connect(accept_double)
|
|
140
|
+
v.addWidget(lst)
|
|
141
|
+
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=dlg)
|
|
142
|
+
v.addWidget(buttons)
|
|
143
|
+
buttons.accepted.connect(dlg.accept)
|
|
144
|
+
buttons.rejected.connect(dlg.reject)
|
|
145
|
+
if dlg.exec() == QDialog.DialogCode.Accepted:
|
|
146
|
+
cur = lst.currentItem()
|
|
147
|
+
choice = cur.text() if cur else None
|
|
148
|
+
if choice:
|
|
149
|
+
self.watch_list.addVariable(choice)
|
|
150
|
+
try:
|
|
151
|
+
self.debugger.console.append(f"Watching: {choice}") # type: ignore[attr-defined]
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
177
154
|
except Exception as exc:
|
|
178
155
|
QMessageBox.warning(self, "Add Watch", f"Could not list variables: {exc}")
|
|
179
156
|
|
|
180
|
-
def _add_watch_row(self, name: str):
|
|
181
|
-
row = QWidget()
|
|
182
|
-
h = QHBoxLayout(row)
|
|
183
|
-
h.setContentsMargins(0, 0, 0, 0)
|
|
184
|
-
h.setSpacing(4)
|
|
185
|
-
lbl = QLabel(name)
|
|
186
|
-
lbl.setStyleSheet("font-family: mono; padding: 1px 2px;")
|
|
187
|
-
h.addWidget(lbl)
|
|
188
|
-
h.addStretch()
|
|
189
|
-
btn = QPushButton()
|
|
190
|
-
btn.setText("–") # placeholder until icon provided
|
|
191
|
-
btn.setToolTip(f"Remove '{name}' from watch")
|
|
192
|
-
btn.setFixedSize(20, 20)
|
|
193
|
-
|
|
194
|
-
def on_remove():
|
|
195
|
-
try:
|
|
196
|
-
# update internal structures
|
|
197
|
-
if hasattr(self.debugger, 'watched') and name in self.debugger.watched: # type: ignore[attr-defined]
|
|
198
|
-
self.debugger.watched.remove(name) # type: ignore[attr-defined]
|
|
199
|
-
if name in self._watch_set:
|
|
200
|
-
self._watch_set.remove(name)
|
|
201
|
-
# remove row from layout/UI
|
|
202
|
-
row.setParent(None)
|
|
203
|
-
row.deleteLater()
|
|
204
|
-
except Exception:
|
|
205
|
-
pass
|
|
206
|
-
|
|
207
|
-
btn.clicked.connect(on_remove)
|
|
208
|
-
h.addWidget(btn)
|
|
209
|
-
self.watch_list_layout.addWidget(row)
|
|
210
|
-
|
|
211
|
-
def on_run_clicked(self):
|
|
212
|
-
self.debugger.doRun() # type: ignore[attr-defined]
|
|
213
|
-
|
|
214
|
-
def on_step_clicked(self):
|
|
215
|
-
self.debugger.doStep() # type: ignore[attr-defined]
|
|
216
|
-
|
|
217
|
-
def on_stop_clicked(self):
|
|
218
|
-
self.debugger.doStop() # type: ignore[attr-defined]
|
|
219
|
-
|
|
220
|
-
def on_exit_clicked(self):
|
|
221
|
-
self.debugger.doClose() # type: ignore[attr-defined]
|
|
222
|
-
|
|
223
157
|
###########################################################################
|
|
224
|
-
#
|
|
225
|
-
class
|
|
158
|
+
# A single script panel that displays one script's lines
|
|
159
|
+
class ScriptPanel(QWidget):
|
|
226
160
|
scroll: QScrollArea
|
|
227
161
|
layout: QHBoxLayout # type: ignore[assignment]
|
|
228
|
-
blob: QLabel
|
|
229
162
|
|
|
230
163
|
def __init__(self, parent=None):
|
|
231
164
|
super().__init__(parent)
|
|
165
|
+
|
|
166
|
+
# Set white background
|
|
167
|
+
self.setStyleSheet("background-color: white;")
|
|
168
|
+
|
|
169
|
+
# Main layout
|
|
170
|
+
panel_layout = QVBoxLayout(self)
|
|
171
|
+
panel_layout.setContentsMargins(0, 0, 0, 0)
|
|
172
|
+
panel_layout.setSpacing(0)
|
|
232
173
|
|
|
233
174
|
# Create a scroll area - its content widget holds the lines
|
|
234
|
-
self.scroll = QScrollArea(
|
|
175
|
+
self.scroll = QScrollArea()
|
|
176
|
+
self.scroll.setStyleSheet("background-color: white;")
|
|
235
177
|
self.scroll.setWidgetResizable(True)
|
|
236
178
|
|
|
237
179
|
# Ensure this widget and the scroll area expand to fill available space
|
|
@@ -249,21 +191,19 @@ class Debugger(QMainWindow):
|
|
|
249
191
|
|
|
250
192
|
self.scroll.setWidget(self.content)
|
|
251
193
|
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
main_layout.setStretch(0, 1)
|
|
194
|
+
# Add scroll area to the panel layout
|
|
195
|
+
panel_layout.addWidget(self.scroll)
|
|
196
|
+
|
|
197
|
+
# Store script lines for this panel
|
|
198
|
+
self.scriptLines = []
|
|
258
199
|
|
|
259
200
|
#######################################################################
|
|
260
|
-
# Add a line to
|
|
201
|
+
# Add a line to this script panel
|
|
261
202
|
def addLine(self, spec):
|
|
262
203
|
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
is_command = bool(line_lstripped and not line_lstripped.startswith('!'))
|
|
204
|
+
# is_command will be set later by enableBreakpoints() after compilation
|
|
205
|
+
# Initialize to False for now
|
|
206
|
+
spec.is_command = False
|
|
267
207
|
|
|
268
208
|
class Label(QLabel):
|
|
269
209
|
def __init__(self, text, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft, on_click=None):
|
|
@@ -277,7 +217,7 @@ class Debugger(QMainWindow):
|
|
|
277
217
|
if fixed_width is not None:
|
|
278
218
|
self.setFixedWidth(fixed_width)
|
|
279
219
|
self.setAlignment(align | Qt.AlignmentFlag.AlignVCenter)
|
|
280
|
-
self._on_click = on_click
|
|
220
|
+
self._on_click = on_click
|
|
281
221
|
|
|
282
222
|
def mousePressEvent(self, event):
|
|
283
223
|
if self._on_click:
|
|
@@ -313,7 +253,7 @@ class Debugger(QMainWindow):
|
|
|
313
253
|
class ClickableBlob(QLabel):
|
|
314
254
|
def __init__(self, on_click=None):
|
|
315
255
|
super().__init__()
|
|
316
|
-
self._on_click = on_click
|
|
256
|
+
self._on_click = on_click
|
|
317
257
|
def mousePressEvent(self, event):
|
|
318
258
|
if self._on_click:
|
|
319
259
|
try:
|
|
@@ -323,7 +263,7 @@ class Debugger(QMainWindow):
|
|
|
323
263
|
super().mousePressEvent(event)
|
|
324
264
|
|
|
325
265
|
blob_size = 10
|
|
326
|
-
blob = ClickableBlob(on_click=
|
|
266
|
+
blob = ClickableBlob(on_click=lambda: spec.onClick(spec.lino))
|
|
327
267
|
blob.setFixedSize(blob_size, blob_size)
|
|
328
268
|
|
|
329
269
|
def set_blob_visible(widget, visible):
|
|
@@ -348,7 +288,7 @@ class Debugger(QMainWindow):
|
|
|
348
288
|
|
|
349
289
|
# create the line-number label; clicking it reports back to the caller
|
|
350
290
|
lino_label = Label(str(spec.lino+1), fixed_width=width_4, align=Qt.AlignmentFlag.AlignRight,
|
|
351
|
-
on_click=
|
|
291
|
+
on_click=lambda: spec.onClick(spec.lino))
|
|
352
292
|
lino_label.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
|
|
353
293
|
# create the text label for the line itself
|
|
354
294
|
text_label = Label(spec.line, fixed_width=None, align=Qt.AlignmentFlag.AlignLeft)
|
|
@@ -361,14 +301,119 @@ class Debugger(QMainWindow):
|
|
|
361
301
|
self.inner_layout.addWidget(panel)
|
|
362
302
|
return panel
|
|
363
303
|
|
|
364
|
-
def
|
|
365
|
-
self.
|
|
304
|
+
def addStretch(self):
|
|
305
|
+
self.inner_layout.addStretch()
|
|
306
|
+
|
|
307
|
+
###########################################################################
|
|
308
|
+
# The right-hand column of the main window
|
|
309
|
+
class MainRightColumn(QWidget):
|
|
366
310
|
|
|
367
|
-
def
|
|
368
|
-
|
|
311
|
+
def __init__(self, parent=None):
|
|
312
|
+
super().__init__(parent)
|
|
313
|
+
|
|
314
|
+
# Set white background for the entire column
|
|
315
|
+
self.setStyleSheet("background-color: white;")
|
|
316
|
+
|
|
317
|
+
# Main layout for this column
|
|
318
|
+
column_layout = QVBoxLayout(self)
|
|
319
|
+
column_layout.setContentsMargins(0, 0, 0, 0)
|
|
320
|
+
column_layout.setSpacing(0)
|
|
321
|
+
|
|
322
|
+
# Create toolbar with icon buttons
|
|
323
|
+
toolbar = QToolBar()
|
|
324
|
+
toolbar.setMovable(False)
|
|
325
|
+
|
|
326
|
+
# Get the icons directory path
|
|
327
|
+
icons_dir = os.path.join(os.path.dirname(__file__), '../icons')
|
|
328
|
+
|
|
329
|
+
# Get the parent debugger for callbacks
|
|
330
|
+
debugger = parent
|
|
331
|
+
|
|
332
|
+
# Run button
|
|
333
|
+
run_btn = QPushButton()
|
|
334
|
+
run_icon_path = os.path.join(icons_dir, 'run.png')
|
|
335
|
+
run_btn.setIcon(QIcon(run_icon_path))
|
|
336
|
+
run_btn.setToolTip("Run")
|
|
337
|
+
run_btn.clicked.connect(lambda: debugger.doRun() if debugger else None) # type: ignore[attr-defined]
|
|
338
|
+
toolbar.addWidget(run_btn)
|
|
339
|
+
|
|
340
|
+
# Step button
|
|
341
|
+
step_btn = QPushButton()
|
|
342
|
+
step_icon_path = os.path.join(icons_dir, 'step.png')
|
|
343
|
+
step_btn.setIcon(QIcon(step_icon_path))
|
|
344
|
+
step_btn.setToolTip("Step")
|
|
345
|
+
step_btn.clicked.connect(lambda: debugger.doStep() if debugger else None) # type: ignore[attr-defined]
|
|
346
|
+
toolbar.addWidget(step_btn)
|
|
347
|
+
|
|
348
|
+
# Stop button
|
|
349
|
+
stop_btn = QPushButton()
|
|
350
|
+
stop_icon_path = os.path.join(icons_dir, 'stop.png')
|
|
351
|
+
stop_btn.setIcon(QIcon(stop_icon_path))
|
|
352
|
+
stop_btn.setToolTip("Stop")
|
|
353
|
+
stop_btn.clicked.connect(lambda: debugger.doStop() if debugger else None) # type: ignore[attr-defined]
|
|
354
|
+
toolbar.addWidget(stop_btn)
|
|
355
|
+
|
|
356
|
+
# Exit button
|
|
357
|
+
exit_btn = QPushButton()
|
|
358
|
+
exit_icon_path = os.path.join(icons_dir, 'exit.png')
|
|
359
|
+
exit_btn.setIcon(QIcon(exit_icon_path))
|
|
360
|
+
exit_btn.setToolTip("Exit")
|
|
361
|
+
exit_btn.clicked.connect(lambda: debugger.doClose() if debugger else None) # type: ignore[attr-defined]
|
|
362
|
+
toolbar.addWidget(exit_btn)
|
|
363
|
+
|
|
364
|
+
column_layout.addWidget(toolbar)
|
|
365
|
+
|
|
366
|
+
# Create a tab widget to hold multiple script panels
|
|
367
|
+
self.tabWidget = QTabWidget()
|
|
368
|
+
self.tabWidget.setTabsClosable(False) # Don't allow closing tabs for now
|
|
369
|
+
|
|
370
|
+
# Ensure tab widget expands
|
|
371
|
+
self.tabWidget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
372
|
+
|
|
373
|
+
column_layout.addWidget(self.tabWidget)
|
|
374
|
+
|
|
375
|
+
# Dictionary to map program -> script panel
|
|
376
|
+
self.programPanels = {}
|
|
377
|
+
|
|
378
|
+
#######################################################################
|
|
379
|
+
# Add a new script tab
|
|
380
|
+
def addScriptTab(self, program, filename):
|
|
381
|
+
"""Add a new tab for a script"""
|
|
382
|
+
panel = Debugger.ScriptPanel(self)
|
|
383
|
+
# Extract just the filename from the full path
|
|
384
|
+
tab_label = os.path.basename(filename)
|
|
385
|
+
self.tabWidget.addTab(panel, tab_label)
|
|
386
|
+
self.programPanels[id(program)] = panel
|
|
387
|
+
return panel
|
|
388
|
+
|
|
389
|
+
#######################################################################
|
|
390
|
+
# Get the current active script panel
|
|
391
|
+
def getCurrentPanel(self):
|
|
392
|
+
"""Get the currently active script panel"""
|
|
393
|
+
return self.tabWidget.currentWidget()
|
|
394
|
+
|
|
395
|
+
#######################################################################
|
|
396
|
+
# Get the panel for a specific program
|
|
397
|
+
def getPanelForProgram(self, program):
|
|
398
|
+
"""Get the panel associated with a program"""
|
|
399
|
+
return self.programPanels.get(id(program))
|
|
400
|
+
|
|
401
|
+
#######################################################################
|
|
402
|
+
# Legacy method - add a line to the current panel
|
|
403
|
+
def addLine(self, spec):
|
|
404
|
+
"""Delegate to the current panel's addLine method"""
|
|
405
|
+
panel = self.getCurrentPanel()
|
|
406
|
+
if panel and isinstance(panel, Debugger.ScriptPanel):
|
|
407
|
+
return panel.addLine(spec)
|
|
408
|
+
return None
|
|
369
409
|
|
|
410
|
+
#######################################################################
|
|
411
|
+
# Add stretch to current panel
|
|
370
412
|
def addStretch(self):
|
|
371
|
-
|
|
413
|
+
"""Delegate to the current panel's addStretch method"""
|
|
414
|
+
panel = self.getCurrentPanel()
|
|
415
|
+
if panel and isinstance(panel, Debugger.ScriptPanel):
|
|
416
|
+
panel.addStretch()
|
|
372
417
|
|
|
373
418
|
###########################################################################
|
|
374
419
|
# Main debugger class initializer
|
|
@@ -380,6 +425,9 @@ class Debugger(QMainWindow):
|
|
|
380
425
|
# Disable the window close button
|
|
381
426
|
self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
|
|
382
427
|
self.stopped = True
|
|
428
|
+
self.skip_next_breakpoint = False # Flag to skip breakpoint check on resume
|
|
429
|
+
self.saved_queue = [] # Save queue state when stopped to preserve forked threads
|
|
430
|
+
self._highlighted: set[int] = set()
|
|
383
431
|
|
|
384
432
|
# try to load saved geometry from ~/.ecdebug.conf
|
|
385
433
|
cfg_path = os.path.join(os.path.expanduser("~"), ".ecdebug.conf")
|
|
@@ -429,9 +477,10 @@ class Debugger(QMainWindow):
|
|
|
429
477
|
left.setFrameShape(QFrame.Shape.StyledPanel)
|
|
430
478
|
left_layout = QVBoxLayout(left)
|
|
431
479
|
left_layout.setContentsMargins(8, 8, 8, 8)
|
|
480
|
+
left_layout.setSpacing(0)
|
|
432
481
|
self.leftColumn = self.MainLeftColumn(self)
|
|
433
|
-
|
|
434
|
-
left_layout.
|
|
482
|
+
self.leftColumn.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
483
|
+
left_layout.addWidget(self.leftColumn, 1)
|
|
435
484
|
|
|
436
485
|
# Right pane
|
|
437
486
|
right = QFrame()
|
|
@@ -489,7 +538,7 @@ class Debugger(QMainWindow):
|
|
|
489
538
|
|
|
490
539
|
# Use the vertical splitter as the central widget
|
|
491
540
|
self.setCentralWidget(self.vsplitter)
|
|
492
|
-
self.parse(program.script.lines)
|
|
541
|
+
self.parse(program.script.lines, program, program.scriptName)
|
|
493
542
|
self.show()
|
|
494
543
|
|
|
495
544
|
def _flush_console_buffer(self):
|
|
@@ -523,15 +572,41 @@ class Debugger(QMainWindow):
|
|
|
523
572
|
|
|
524
573
|
###########################################################################
|
|
525
574
|
# Parse a script into the right-hand column
|
|
526
|
-
def parse(self, script):
|
|
575
|
+
def parse(self, script, program=None, filename=None):
|
|
576
|
+
"""Parse a script and add it as a tab
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
script: List of script lines
|
|
580
|
+
program: The Program instance this script belongs to
|
|
581
|
+
filename: The filename to use as the tab label
|
|
582
|
+
"""
|
|
527
583
|
self.scriptLines = []
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if
|
|
534
|
-
|
|
584
|
+
|
|
585
|
+
# Get or create the panel for this program
|
|
586
|
+
panel = None
|
|
587
|
+
if program:
|
|
588
|
+
panel = self.rightColumn.getPanelForProgram(program)
|
|
589
|
+
if not panel:
|
|
590
|
+
# Create a new tab for this program
|
|
591
|
+
if not filename:
|
|
592
|
+
filename = getattr(program, 'path', 'Untitled')
|
|
593
|
+
panel = self.rightColumn.addScriptTab(program, filename)
|
|
594
|
+
|
|
595
|
+
# If no program specified or panel creation failed, use current panel
|
|
596
|
+
if not panel:
|
|
597
|
+
panel = self.rightColumn.getCurrentPanel()
|
|
598
|
+
if not panel:
|
|
599
|
+
# Create a default tab
|
|
600
|
+
panel = self.rightColumn.addScriptTab(None, filename or 'Untitled')
|
|
601
|
+
|
|
602
|
+
# Clear existing lines from the panel
|
|
603
|
+
if panel and isinstance(panel, Debugger.ScriptPanel):
|
|
604
|
+
layout = panel.inner_layout
|
|
605
|
+
while layout.count():
|
|
606
|
+
item = layout.takeAt(0)
|
|
607
|
+
widget = item.widget()
|
|
608
|
+
if widget:
|
|
609
|
+
widget.deleteLater()
|
|
535
610
|
|
|
536
611
|
# Parse and add new lines
|
|
537
612
|
lino = 0
|
|
@@ -631,11 +706,49 @@ class Debugger(QMainWindow):
|
|
|
631
706
|
|
|
632
707
|
return output
|
|
633
708
|
|
|
709
|
+
###########################################################################
|
|
710
|
+
# Enable breakpoints to be set on any line that is classed as a command
|
|
711
|
+
def enableBreakpoints(self):
|
|
712
|
+
"""
|
|
713
|
+
Examine each line and set is_command flag based on compiled code.
|
|
714
|
+
A line is a command if:
|
|
715
|
+
- It's not empty or a comment
|
|
716
|
+
- It's not a label (single word ending with colon)
|
|
717
|
+
- It corresponds to a command in program.code with type != 'symbol'
|
|
718
|
+
"""
|
|
719
|
+
# First, mark all lines as non-commands by default
|
|
720
|
+
for lineSpec in self.scriptLines:
|
|
721
|
+
lineSpec.is_command = False
|
|
722
|
+
|
|
723
|
+
# Now iterate through compiled commands and mark those that are executable
|
|
724
|
+
for command in self.program.code:
|
|
725
|
+
if 'lino' not in command:
|
|
726
|
+
continue
|
|
727
|
+
|
|
728
|
+
lino = command['lino']
|
|
729
|
+
if lino < 0 or lino >= len(self.scriptLines):
|
|
730
|
+
continue
|
|
731
|
+
|
|
732
|
+
# Check if this is a symbol declaration (variable/constant definition)
|
|
733
|
+
if command.get('type') == 'symbol':
|
|
734
|
+
continue
|
|
735
|
+
|
|
736
|
+
# Check if this is a structural keyword that shouldn't have breakpoints
|
|
737
|
+
if command.get('keyword') in ['begin', 'end']:
|
|
738
|
+
continue
|
|
739
|
+
|
|
740
|
+
# This is an executable command
|
|
741
|
+
self.scriptLines[lino].is_command = True
|
|
742
|
+
|
|
634
743
|
###########################################################################
|
|
635
744
|
# Here when the user clicks a line number
|
|
636
745
|
def onClickLino(self, lino):
|
|
637
|
-
#
|
|
746
|
+
# Check if this line is a command - if not, take no action
|
|
638
747
|
lineSpec = self.scriptLines[lino]
|
|
748
|
+
if not getattr(lineSpec, 'is_command', True):
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
# Show or hide the red blob next to this line
|
|
639
752
|
lineSpec.bp = not lineSpec.bp
|
|
640
753
|
if lineSpec.bp: lineSpec.label.showBlob()
|
|
641
754
|
else: lineSpec.label.hideBlob()
|
|
@@ -659,8 +772,13 @@ class Debugger(QMainWindow):
|
|
|
659
772
|
if not panel:
|
|
660
773
|
return
|
|
661
774
|
|
|
662
|
-
# Get the
|
|
663
|
-
|
|
775
|
+
# Get the current script panel
|
|
776
|
+
script_panel = self.rightColumn.getCurrentPanel()
|
|
777
|
+
if not script_panel or not isinstance(script_panel, Debugger.ScriptPanel):
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
# Get the scroll area from the script panel
|
|
781
|
+
scroll_area = script_panel.scroll
|
|
664
782
|
|
|
665
783
|
# Get the vertical position of the panel relative to the content widget
|
|
666
784
|
panel_y = panel.y()
|
|
@@ -687,7 +805,7 @@ class Debugger(QMainWindow):
|
|
|
687
805
|
###########################################################################
|
|
688
806
|
# Set the background color of one line of the script
|
|
689
807
|
def setBackground(self, lino, color):
|
|
690
|
-
# Set the background color of the given line
|
|
808
|
+
# Set the background color of the given line and track highlighted lines
|
|
691
809
|
if lino < 0 or lino >= len(self.scriptLines):
|
|
692
810
|
return
|
|
693
811
|
lineSpec = self.scriptLines[lino]
|
|
@@ -696,38 +814,153 @@ class Debugger(QMainWindow):
|
|
|
696
814
|
return
|
|
697
815
|
if color == 'none':
|
|
698
816
|
panel.setStyleSheet("")
|
|
817
|
+
self._highlighted.discard(lino)
|
|
699
818
|
else:
|
|
700
819
|
panel.setStyleSheet(f"background-color: {color};")
|
|
820
|
+
self._highlighted.add(lino)
|
|
821
|
+
|
|
822
|
+
def _clearHighlights(self):
|
|
823
|
+
# Remove highlighting from all previously highlighted lines
|
|
824
|
+
for lino in list(self._highlighted):
|
|
825
|
+
self.setBackground(lino, 'none')
|
|
826
|
+
self._highlighted.clear()
|
|
701
827
|
|
|
702
828
|
###########################################################################
|
|
703
|
-
# Here
|
|
704
|
-
|
|
705
|
-
|
|
829
|
+
# Here before each instruction is run
|
|
830
|
+
# Returns True if the program should halt and wait for user interaction
|
|
831
|
+
def checkIfHalt(self, is_first_command=False):
|
|
706
832
|
self.pc = self.program.pc
|
|
707
833
|
command = self.program.code[self.pc]
|
|
708
|
-
lino = command['lino']
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
834
|
+
lino = command['lino'] if 'lino' in command else 0
|
|
835
|
+
bp = command.get('bp', False)
|
|
836
|
+
|
|
837
|
+
# Check if we should skip this breakpoint check (resuming from same location)
|
|
838
|
+
if self.skip_next_breakpoint:
|
|
839
|
+
self.skip_next_breakpoint = False
|
|
840
|
+
return False
|
|
841
|
+
|
|
842
|
+
# Labels should never halt execution - they're just markers
|
|
843
|
+
# A label is a symbol whose name ends with ':'
|
|
844
|
+
if command.get('type') == 'symbol' and command.get('name', '').endswith(':'):
|
|
845
|
+
return False
|
|
846
|
+
|
|
847
|
+
# Determine if we should halt
|
|
848
|
+
should_halt = False
|
|
849
|
+
|
|
850
|
+
# If this is the first real command (pc==1), always halt to initialize display
|
|
851
|
+
if is_first_command:
|
|
852
|
+
should_halt = True
|
|
853
|
+
self.stopped = True
|
|
854
|
+
print(f"Program ready at line {lino + 1}")
|
|
855
|
+
# If we're in stopped (step) mode, halt after each command
|
|
856
|
+
elif self.stopped:
|
|
857
|
+
should_halt = True
|
|
858
|
+
# If there's a breakpoint on this line, halt
|
|
859
|
+
elif bp:
|
|
860
|
+
print(f"Hit breakpoint at line {lino + 1}")
|
|
712
861
|
self.stopped = True
|
|
713
|
-
|
|
714
|
-
|
|
862
|
+
should_halt = True
|
|
863
|
+
|
|
864
|
+
# If halting, update the UI and save queue state
|
|
865
|
+
if should_halt:
|
|
715
866
|
self.scrollTo(lino)
|
|
716
|
-
self.
|
|
717
|
-
|
|
867
|
+
self._clearHighlights()
|
|
868
|
+
self.setBackground(lino, 'Yellow')
|
|
869
|
+
# Refresh variable values when halted
|
|
870
|
+
self.refreshVariables()
|
|
871
|
+
# Save the current queue state to preserve forked threads
|
|
872
|
+
self._saveQueueState()
|
|
873
|
+
|
|
874
|
+
return should_halt
|
|
875
|
+
|
|
876
|
+
def refreshVariables(self):
|
|
877
|
+
"""Update all watched variable values"""
|
|
878
|
+
try:
|
|
879
|
+
if hasattr(self, 'leftColumn') and hasattr(self.leftColumn, 'watch_list'):
|
|
880
|
+
self.leftColumn.watch_list.refreshVariables(self.program)
|
|
881
|
+
except Exception as ex:
|
|
882
|
+
print(f"Error refreshing variables: {ex}")
|
|
883
|
+
|
|
884
|
+
def _saveQueueState(self):
|
|
885
|
+
"""Save the current global queue state (preserves forked threads)"""
|
|
886
|
+
try:
|
|
887
|
+
# Import the module to access the global queue
|
|
888
|
+
from easycoder import ec_program
|
|
889
|
+
# Save a copy of the queue
|
|
890
|
+
self.saved_queue = list(ec_program.queue)
|
|
891
|
+
except Exception as ex:
|
|
892
|
+
print(f"Error saving queue state: {ex}")
|
|
893
|
+
|
|
894
|
+
def _restoreQueueState(self):
|
|
895
|
+
"""Restore the saved queue state (resume all forked threads)"""
|
|
896
|
+
try:
|
|
897
|
+
# Import here to avoid circular dependency
|
|
898
|
+
from easycoder import ec_program
|
|
899
|
+
# Restore the queue from saved state
|
|
900
|
+
if self.saved_queue:
|
|
901
|
+
ec_program.queue.clear()
|
|
902
|
+
ec_program.queue.extend(self.saved_queue)
|
|
903
|
+
except Exception as ex:
|
|
904
|
+
print(f"Error restoring queue state: {ex}")
|
|
718
905
|
|
|
719
906
|
def doRun(self):
|
|
907
|
+
"""Resume free-running execution from current PC"""
|
|
908
|
+
command = self.program.code[self.pc]
|
|
909
|
+
lino = command.get('lino', 0)
|
|
910
|
+
print(f"Continuing execution at line {lino + 1}")
|
|
911
|
+
|
|
912
|
+
# Clear the highlight on the current line
|
|
913
|
+
self.setBackground(lino, 'none')
|
|
914
|
+
|
|
915
|
+
# Switch to free-running mode
|
|
720
916
|
self.stopped = False
|
|
721
|
-
|
|
917
|
+
|
|
918
|
+
# Skip the breakpoint check for the current instruction (the one we're resuming from)
|
|
919
|
+
self.skip_next_breakpoint = True
|
|
920
|
+
|
|
921
|
+
# Restore the saved queue state to resume all forked threads
|
|
922
|
+
self._restoreQueueState()
|
|
923
|
+
|
|
924
|
+
# Enqueue the current thread, then flush immediately
|
|
722
925
|
self.program.run(self.pc)
|
|
926
|
+
from easycoder.ec_program import flush
|
|
927
|
+
flush()
|
|
723
928
|
|
|
724
929
|
def doStep(self):
|
|
930
|
+
"""Execute one instruction and halt again"""
|
|
725
931
|
command = self.program.code[self.pc]
|
|
726
|
-
|
|
727
|
-
|
|
932
|
+
lino = command.get('lino', 0)
|
|
933
|
+
|
|
934
|
+
# Clear the highlight on the current line
|
|
935
|
+
self.setBackground(lino, 'none')
|
|
936
|
+
|
|
937
|
+
# Stay in stopped mode (will halt after next instruction)
|
|
938
|
+
self.stopped = True
|
|
939
|
+
|
|
940
|
+
# Skip the breakpoint check for the current instruction (the one we're stepping from)
|
|
941
|
+
self.skip_next_breakpoint = True
|
|
942
|
+
|
|
943
|
+
# Restore the saved queue state to resume all forked threads
|
|
944
|
+
self._restoreQueueState()
|
|
945
|
+
|
|
946
|
+
# Enqueue the current thread, then flush a single cycle
|
|
728
947
|
self.program.run(self.pc)
|
|
948
|
+
from easycoder.ec_program import flush
|
|
949
|
+
flush()
|
|
729
950
|
|
|
730
951
|
def doStop(self):
|
|
952
|
+
try:
|
|
953
|
+
lino = self.program.code[self.pc].get('lino', 0) + 1
|
|
954
|
+
print(f"Stopped by user at line {lino}")
|
|
955
|
+
except Exception:
|
|
956
|
+
print("Stopped by user")
|
|
957
|
+
# Clear all previous highlights and mark the current line
|
|
958
|
+
try:
|
|
959
|
+
self._clearHighlights()
|
|
960
|
+
current_lino = self.program.code[self.pc].get('lino', 0)
|
|
961
|
+
self.setBackground(current_lino, 'LightYellow')
|
|
962
|
+
except Exception:
|
|
963
|
+
pass
|
|
731
964
|
self.stopped = True
|
|
732
965
|
|
|
733
966
|
def doClose(self):
|