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.
Files changed (51) hide show
  1. easycoder/__init__.py +6 -3
  2. easycoder/debugger/__init__.py +5 -0
  3. easycoder/debugger/ec_dbg_value_display copy.py +195 -0
  4. easycoder/debugger/ec_dbg_value_display.py +24 -0
  5. easycoder/debugger/ec_dbg_watch_list copy.py +219 -0
  6. easycoder/debugger/ec_dbg_watchlist.py +293 -0
  7. easycoder/debugger/ec_debug.py +1025 -0
  8. easycoder/ec_classes.py +487 -11
  9. easycoder/ec_compiler.py +81 -44
  10. easycoder/ec_condition.py +1 -1
  11. easycoder/ec_core.py +1042 -1081
  12. easycoder/ec_gclasses.py +236 -0
  13. easycoder/ec_graphics.py +1683 -0
  14. easycoder/ec_handler.py +18 -14
  15. easycoder/ec_mqtt.py +248 -0
  16. easycoder/ec_program.py +297 -168
  17. easycoder/ec_psutil.py +48 -0
  18. easycoder/ec_value.py +65 -47
  19. easycoder/pre/README.md +3 -0
  20. easycoder/pre/__init__.py +17 -0
  21. easycoder/pre/debugger/__init__.py +5 -0
  22. easycoder/pre/debugger/ec_dbg_value_display copy.py +195 -0
  23. easycoder/pre/debugger/ec_dbg_value_display.py +24 -0
  24. easycoder/pre/debugger/ec_dbg_watch_list copy.py +219 -0
  25. easycoder/pre/debugger/ec_dbg_watchlist.py +293 -0
  26. easycoder/{ec_debug.py → pre/debugger/ec_debug.py} +418 -185
  27. easycoder/pre/ec_border.py +67 -0
  28. easycoder/pre/ec_classes.py +470 -0
  29. easycoder/pre/ec_compiler.py +291 -0
  30. easycoder/pre/ec_condition.py +27 -0
  31. easycoder/pre/ec_core.py +2772 -0
  32. easycoder/pre/ec_gclasses.py +230 -0
  33. easycoder/{ec_pyside.py → pre/ec_graphics.py} +583 -433
  34. easycoder/pre/ec_handler.py +79 -0
  35. easycoder/pre/ec_keyboard.py +439 -0
  36. easycoder/pre/ec_program.py +557 -0
  37. easycoder/pre/ec_psutil.py +48 -0
  38. easycoder/pre/ec_timestamp.py +11 -0
  39. easycoder/pre/ec_value.py +124 -0
  40. easycoder/pre/icons/close.png +0 -0
  41. easycoder/pre/icons/exit.png +0 -0
  42. easycoder/pre/icons/run.png +0 -0
  43. easycoder/pre/icons/step.png +0 -0
  44. easycoder/pre/icons/stop.png +0 -0
  45. easycoder/pre/icons/tick.png +0 -0
  46. {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/METADATA +11 -1
  47. easycoder-260111.1.dist-info/RECORD +59 -0
  48. easycoder-251105.1.dist-info/RECORD +0 -24
  49. {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/WHEEL +0 -0
  50. {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/entry_points.txt +0 -0
  51. {easycoder-251105.1.dist-info → easycoder-260111.1.dist-info}/licenses/LICENSE +0 -0
easycoder/ec_value.py CHANGED
@@ -1,11 +1,9 @@
1
- from .ec_classes import FatalError
1
+ from typing import Optional, List
2
+ from .ec_classes import ECObject, FatalError, ECValue
2
3
 
3
4
  # Create a constant
4
5
  def getConstant(str):
5
- value = {}
6
- value['type'] = 'text'
7
- value['content'] = str
8
- return value
6
+ return ECValue(type=str, content=str)
9
7
 
10
8
  class Value:
11
9
 
@@ -14,30 +12,28 @@ class Value:
14
12
  self.getToken = compiler.getToken
15
13
  self.nextToken = compiler.nextToken
16
14
  self.peek = compiler.peek
15
+ self.skip = compiler.skip
17
16
  self.tokenIs = compiler.tokenIs
18
17
 
19
- def getItem(self):
18
+ def getItem(self) -> Optional[ECValue]:
20
19
  token = self.getToken()
21
20
  if not token:
22
21
  return None
23
22
 
24
- value = {}
23
+ value = ECValue()
25
24
 
26
25
  if token == 'true':
27
- value['type'] = 'boolean'
28
- value['content'] = True
26
+ value.setValue(bool, True)
29
27
  return value
30
28
 
31
29
  if token == 'false':
32
- value['type'] = 'boolean'
33
- value['content'] = False
30
+ value.setValue(bool, False)
34
31
  return value
35
32
 
36
33
  # Check for a string constant
37
34
  if token[0] == '`':
38
35
  if token[len(token) - 1] == '`':
39
- value['type'] = 'text'
40
- value['content'] = token[1 : len(token) - 1]
36
+ value.setValue(type=str, content=token[1 : len(token) - 1])
41
37
  return value
42
38
  FatalError(self.compiler, f'Unterminated string "{token}"')
43
39
  return None
@@ -46,8 +42,7 @@ class Value:
46
42
  if token.isnumeric() or (token[0] == '-' and token[1:].isnumeric):
47
43
  val = eval(token)
48
44
  if isinstance(val, int):
49
- value['type'] = 'int'
50
- value['content'] = val
45
+ value.setValue(int, val)
51
46
  return value
52
47
  FatalError(self.compiler, f'{token} is not an integer')
53
48
 
@@ -55,51 +50,74 @@ class Value:
55
50
  mark = self.compiler.getIndex()
56
51
  for domain in self.compiler.program.getDomains():
57
52
  item = domain.compileValue()
58
- if item != None:
59
- return item
53
+ if item != None: return item
60
54
  self.compiler.rewindTo(mark)
61
55
  # self.compiler.warning(f'I don\'t understand \'{token}\'')
62
56
  return None
63
57
 
64
- def compileValue(self):
65
- token = self.getToken()
58
+ # Get a list of items following 'the cat of ...'
59
+ def getCatItems(self) -> Optional[List[ECValue]]:
60
+ items: List[ECValue] = []
66
61
  item = self.getItem()
62
+ if item == None: return None
63
+ items.append(item)
64
+ while self.peek() in ['cat', 'and']:
65
+ self.nextToken()
66
+ self.nextToken()
67
+ element = self.getItem()
68
+ if element != None:
69
+ items.append(element) # pyright: ignore[reportOptionalMemberAccess]
70
+ return items
71
+
72
+ # Check if any domain has something to add to the value
73
+ def checkDomainAdditions(self, value):
74
+ for domain in self.compiler.program.getDomains():
75
+ value = domain.modifyValue(value)
76
+ return value
77
+
78
+ # Compile a value
79
+ def compileValue(self) -> Optional[ECValue]:
80
+ token = self.getToken()
81
+ # Special-case the plugin-safe full form: "the cat of ..."
82
+ if token == 'the' and self.peek() == 'cat':
83
+ self.nextToken() # move to 'cat'
84
+ self.skip('of')
85
+ self.nextToken()
86
+ items = self.getCatItems()
87
+ value = ECValue(type='cat', content=items)
88
+ return self.checkDomainAdditions(value)
89
+
90
+ # Otherwise, consume any leading articles before normal parsing
91
+ self.compiler.skipArticles()
92
+ token = self.getToken()
93
+
94
+ item: ECValue|None = self.getItem()
67
95
  if item == None:
68
96
  self.compiler.warning(f'ec_value.compileValue: Cannot get the value of "{token}"')
69
97
  return None
98
+ if item.getType() == 'symbol':
99
+ object = self.compiler.getSymbolRecord(item.getContent())['object']
100
+ if not object.hasRuntimeValue(): return None
70
101
 
71
- value = {}
72
102
  if self.peek() == 'cat':
73
- value['type'] = 'cat'
74
- value['numeric'] = False
75
- value['value'] = [item]
76
- while self.peek() == 'cat':
77
- self.nextToken()
78
- self.nextToken()
79
- item = self.getItem()
80
- if item != None:
81
- value['value'].append(item)
103
+ self.nextToken() # consume 'cat'
104
+ self.nextToken()
105
+ items = self.getCatItems()
106
+ if items != None: items.insert(0, item)
107
+ value = ECValue(type='cat', content=items)
82
108
  else:
83
109
  value = item
84
110
 
85
- # See if any domain has something to add to the value
86
- for domain in self.compiler.program.getDomains():
87
- value = domain.modifyValue(value)
88
-
89
- return value
111
+ return self.checkDomainAdditions(value)
90
112
 
91
113
  def compileConstant(self, token):
92
- value = {}
93
- if type(token) == 'str':
94
- token = eval(token)
95
- if isinstance(token, int):
96
- value['type'] = 'int'
97
- value['content'] = token
98
- return value
99
- if isinstance(token, float):
100
- value['type'] = 'float'
101
- value['content'] = token
102
- return value
103
- value['type'] = 'text'
104
- value['content'] = token
114
+ value = ECValue()
115
+ if isinstance(token, str):
116
+ value.setValue(type=int, content=token)
117
+ elif isinstance(token, int):
118
+ value.setValue(type=int, content=token)
119
+ elif isinstance(token, float):
120
+ value.setValue(type=float, content=token)
121
+ else:
122
+ value.setValue(type=str, content=str(token))
105
123
  return value
@@ -0,0 +1,3 @@
1
+ # 'Pre' version of EasyCoder
2
+
3
+ The code in this directory is a version of EasyCoder that will run on Python versions lower than 10. It is missing more recent improvements such as the use of annotations.
@@ -0,0 +1,17 @@
1
+ '''EasyCoder for Python'''
2
+
3
+ from .ec_classes import *
4
+ from .ec_gclasses import *
5
+ from .ec_compiler import *
6
+ from .ec_condition import *
7
+ from .ec_core import *
8
+ from .ec_graphics import *
9
+ from .ec_handler import *
10
+ from .ec_keyboard import *
11
+ from .ec_border import *
12
+ from .ec_program import *
13
+ from .ec_psutil import *
14
+ from .ec_timestamp import *
15
+ from .ec_value import *
16
+
17
+ __version__ = "251227.1"
@@ -0,0 +1,5 @@
1
+ """EasyCoder debugger module"""
2
+
3
+ from .ec_debug import Debugger
4
+
5
+ __all__ = ['Debugger']
@@ -0,0 +1,195 @@
1
+ """ValueDisplay widget for displaying variable values in the EasyCoder debugger"""
2
+
3
+ from PySide6.QtWidgets import (
4
+ QWidget,
5
+ QFrame,
6
+ QVBoxLayout,
7
+ QLabel,
8
+ QScrollArea,
9
+ )
10
+ from PySide6.QtCore import Qt
11
+
12
+
13
+ class ValueDisplay(QWidget):
14
+ """Widget to display a variable value with type-appropriate formatting"""
15
+
16
+ def __init__(self, parent=None):
17
+ super().__init__(parent)
18
+ vlayout = QVBoxLayout(self)
19
+ vlayout.setContentsMargins(0, 0, 0, 0)
20
+ vlayout.setSpacing(2)
21
+
22
+ # Main value label (always visible)
23
+ self.value_label = QLabel()
24
+ self.value_label.setStyleSheet("font-family: mono; padding: 1px 2px;")
25
+ self.value_label.setWordWrap(False)
26
+ vlayout.addWidget(self.value_label)
27
+
28
+ # Expanded details inside a scroll area (initially hidden)
29
+ self.details_scroll = QScrollArea()
30
+ self.details_scroll.setFrameShape(QFrame.Shape.NoFrame)
31
+ self.details_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
32
+ self.details_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
33
+ self.details_scroll.setWidgetResizable(True)
34
+ self.details_content = QWidget()
35
+ self.details_layout = QVBoxLayout(self.details_content)
36
+ self.details_layout.setContentsMargins(10, 0, 0, 0)
37
+ self.details_layout.setSpacing(1)
38
+ self.details_scroll.setWidget(self.details_content)
39
+ self.details_scroll.hide()
40
+ vlayout.addWidget(self.details_scroll)
41
+
42
+ self.is_expanded = False
43
+ self.current_value = None
44
+ self.max_detail_rows = 10
45
+
46
+ def setValue(self, symbol_record, program):
47
+ """Update display with current value from symbol record"""
48
+ if not symbol_record:
49
+ self.value_label.setText("<not found>")
50
+ return
51
+
52
+ # Check if variable has value capability
53
+ symbol_object = program.getObject(symbol_record)
54
+ if not symbol_object.hasRuntimeValue():
55
+ self.value_label.setText(f"<{symbol_record.get('type', 'no-value')}>")
56
+ return
57
+
58
+ # Get the value array
59
+ value_array = symbol_object.getValues()
60
+
61
+ if not value_array or len(value_array) == 0:
62
+ self.value_label.setText("<empty>")
63
+ return
64
+
65
+ # For arrays, show summary
66
+ if len(value_array) > 1:
67
+ index = symbol_record.get('index', 0)
68
+ self.value_label.setText(f"[{len(value_array)} elements] @{index}")
69
+
70
+ # If expanded, show individual elements
71
+ if self.is_expanded:
72
+ self._show_array_elements(value_array, index)
73
+ else:
74
+ self._hide_details()
75
+ else:
76
+ # Single value - show it directly
77
+ val = program.textify(symbol_object)
78
+ self._show_single_value(val)
79
+
80
+ def _show_single_value(self, content):
81
+ """Display a single value element"""
82
+ if content is None or content == {}:
83
+ self.value_label.setText("<none>")
84
+ return
85
+
86
+ if isinstance(content, bool):
87
+ self.value_label.setText(str(content))
88
+ elif isinstance(content, int):
89
+ self.value_label.setText(str(content))
90
+ elif isinstance(content, str):
91
+ # Check if it's JSON
92
+ if isinstance(content, str) and content.strip().startswith(('{', '[')):
93
+ # Likely JSON - show truncated with expand option
94
+ self._set_elided_text(str(content), multiplier=2.0)
95
+ if self.is_expanded and len(content) > 50:
96
+ self._show_text_details(content)
97
+ else:
98
+ # Regular string
99
+ text_s = str(content)
100
+ self._set_elided_text(text_s, multiplier=2.0)
101
+ if self.is_expanded:
102
+ self._show_text_details(text_s)
103
+ else:
104
+ self.value_label.setText(str(content))
105
+
106
+ def _show_array_elements(self, value_array, current_index):
107
+ """Show expanded array elements"""
108
+ self.details_scroll.show()
109
+ # Clear existing
110
+ while self.details_layout.count():
111
+ item = self.details_layout.takeAt(0)
112
+ if item.widget():
113
+ item.widget().deleteLater()
114
+
115
+ # Show all elements with internal vertical scrolling capped to N lines
116
+ for i in range(len(value_array)):
117
+ val = value_array[i]
118
+ marker = '→ ' if i == current_index else ' '
119
+
120
+ if val is None or val == {}:
121
+ text = f"{marker}[{i}]: <none>"
122
+ else:
123
+ val_type = val.get('type', '?')
124
+ content = val.get('content', '')
125
+ if val_type == str:
126
+ # keep each element concise
127
+ s = str(content)
128
+ if len(s) > 120:
129
+ content = s[:120] + '...'
130
+ text = f'{marker}[{i}]: {content}'
131
+
132
+ lbl = QLabel(text)
133
+ lbl.setStyleSheet("font-family: mono; font-size: 9pt;")
134
+ self.details_layout.addWidget(lbl)
135
+ # Cap scroll area height to max_detail_rows
136
+ fm = self.fontMetrics()
137
+ max_h = int(self.max_detail_rows * fm.height() * 1.2)
138
+ self.details_scroll.setMaximumHeight(max_h)
139
+
140
+ def _show_text_details(self, text):
141
+ """Show full text in details area"""
142
+ self.details_scroll.show()
143
+ # Clear existing
144
+ while self.details_layout.count():
145
+ item = self.details_layout.takeAt(0)
146
+ if item.widget():
147
+ item.widget().deleteLater()
148
+
149
+ lbl = QLabel(text)
150
+ lbl.setStyleSheet("font-family: mono; font-size: 9pt;")
151
+ lbl.setWordWrap(True)
152
+ self.details_layout.addWidget(lbl)
153
+ # Cap to max_detail_rows
154
+ fm = self.fontMetrics()
155
+ max_h = int(self.max_detail_rows * fm.height() * 1.2)
156
+ self.details_scroll.setMaximumHeight(max_h)
157
+
158
+ def _hide_details(self):
159
+ """Hide expanded details"""
160
+ self.details_scroll.hide()
161
+
162
+ def toggleExpand(self):
163
+ """Toggle between expanded and compact view"""
164
+ self.is_expanded = not self.is_expanded
165
+ # Caller should call setValue again to refresh display
166
+
167
+ def _approx_available_width(self) -> int:
168
+ try:
169
+ # Try to find nearest scroll area's viewport width
170
+ w = self
171
+ depth = 0
172
+ while w and depth < 6:
173
+ if isinstance(w, QScrollArea):
174
+ return max(200, w.viewport().width())
175
+ w = w.parentWidget()
176
+ depth += 1
177
+ # Fallback to our own width or a safe default
178
+ return max(240, self.width())
179
+ except Exception:
180
+ return 320
181
+
182
+ def _set_elided_text(self, text: str, multiplier: float = 2.0):
183
+ try:
184
+ fm = self.value_label.fontMetrics()
185
+ avail = int(self._approx_available_width() * multiplier)
186
+ # Apply quotes for display
187
+ quoted = f'"{text}"'
188
+ elided = fm.elidedText(quoted, Qt.TextElideMode.ElideRight, max(80, avail))
189
+ self.value_label.setText(elided)
190
+ except Exception:
191
+ # Fallback simple trim
192
+ s = text
193
+ if len(s) > 160:
194
+ s = s[:160] + '...'
195
+ self.value_label.setText(f'"{s}"')
@@ -0,0 +1,24 @@
1
+ """ValueDisplay widget for displaying variable values in the EasyCoder debugger"""
2
+
3
+ from PySide6.QtWidgets import QLabel
4
+
5
+
6
+ class ValueDisplay(QLabel):
7
+ """Widget to display a variable value with type-appropriate formatting"""
8
+
9
+ def __init__(self, parent=None):
10
+ super().__init__(parent)
11
+
12
+ @staticmethod
13
+ def render_text(program, symbol_name):
14
+ record = program.getVariable(symbol_name)
15
+ value = program.textify(record)
16
+ return None if value is None else str(value)
17
+
18
+ def setValue(self, program, symbol_name):
19
+ try:
20
+ rendered = self.render_text(program, symbol_name)
21
+ except Exception as exc:
22
+ rendered = f"<error: {exc}>"
23
+ self.setText(rendered if rendered is not None else "")
24
+
@@ -0,0 +1,219 @@
1
+ """WatchListWidget for managing the variable watch list in the EasyCoder debugger"""
2
+
3
+ from PySide6.QtWidgets import (
4
+ QWidget,
5
+ QFrame,
6
+ QHBoxLayout,
7
+ QVBoxLayout,
8
+ QGridLayout,
9
+ QLabel,
10
+ """WatchListWidget for managing the variable watch list in the EasyCoder debugger
11
+
12
+ Simplified to show variables in single-line form: `Name = value`.
13
+ """
14
+
15
+ from PySide6.QtWidgets import (
16
+ QWidget,
17
+ QHBoxLayout,
18
+ QVBoxLayout,
19
+ QLabel,
20
+ QPushButton,
21
+ QSizePolicy,
22
+ )
23
+ from .ec_dbg_value_display import ValueDisplay
24
+
25
+
26
+ class WatchListWidget(QWidget):
27
+ """Simple watch list that renders each variable on a single line."""
28
+
29
+ def __init__(self, debugger):
30
+ super().__init__(debugger)
31
+ self.debugger = debugger
32
+ self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
33
+
34
+ self._rows: dict[str, dict] = {}
35
+ self._variable_set: set[str] = set()
36
+ self._placeholder: QLabel | None = None
37
+
38
+ self.layout = QVBoxLayout(self)
39
+ self.layout.setContentsMargins(0, 0, 0, 0)
40
+ self.layout.setSpacing(2)
41
+
42
+ self._show_placeholder()
43
+
44
+ # ------------------------------------------------------------------
45
+ def _show_placeholder(self):
46
+ if self._placeholder is not None:
47
+ return
48
+ self._placeholder = QLabel("No variables watched. Click + to add.")
49
+ self._placeholder.setStyleSheet("color: #666; font-style: italic; padding: 4px 2px;")
50
+ self.layout.addWidget(self._placeholder)
51
+
52
+ def _hide_placeholder(self):
53
+ if self._placeholder is None:
54
+ return
55
+ self.layout.removeWidget(self._placeholder)
56
+ self._placeholder.deleteLater()
57
+ self._placeholder = None
58
+
59
+ # ------------------------------------------------------------------
60
+ def addVariable(self, name: str):
61
+ if not name or name in self._variable_set:
62
+ return
63
+ if not hasattr(self.debugger, 'watched'):
64
+ self.debugger.watched = [] # type: ignore[attr-defined]
65
+ if name not in self.debugger.watched: # type: ignore[attr-defined]
66
+ self.debugger.watched.append(name) # type: ignore[attr-defined]
67
+
68
+ # Ensure placeholder hidden
69
+ self._hide_placeholder()
70
+
71
+ # Build a simple row: [ QLabel("Name = value") | remove_btn ]
72
+ row_widget = QWidget(self)
73
+ row_layout = QHBoxLayout(row_widget)
74
+ row_layout.setContentsMargins(4, 0, 4, 0)
75
+ row_layout.setSpacing(6)
76
+
77
+ label = QLabel("")
78
+ label.setWordWrap(False)
79
+ row_layout.addWidget(label, 1)
80
+
81
+ remove_btn = QPushButton("–")
82
+ remove_btn.setFixedSize(22, 22)
83
+
84
+ def on_remove():
85
+ try:
86
+ if hasattr(self.debugger, 'watched') and name in self.debugger.watched: # type: ignore[attr-defined]
87
+ self.debugger.watched.remove(name) # type: ignore[attr-defined]
88
+ if name in self._variable_set:
89
+ self._variable_set.remove(name)
90
+ self.layout.removeWidget(row_widget)
91
+ row_widget.deleteLater()
92
+ self._rows.pop(name, None)
93
+ if not self._rows:
94
+ """WatchListWidget for managing the variable watch list in the EasyCoder debugger
95
+
96
+ Simplified to show variables in single-line form: `Name = value`.
97
+ """
98
+
99
+ from PySide6.QtWidgets import (
100
+ QWidget,
101
+ QHBoxLayout,
102
+ QVBoxLayout,
103
+ QLabel,
104
+ QPushButton,
105
+ QSizePolicy,
106
+ )
107
+ from .ec_dbg_value_display import ValueDisplay
108
+
109
+
110
+ class WatchListWidget(QWidget):
111
+ """Simple watch list that renders each variable on a single line."""
112
+
113
+ def __init__(self, debugger):
114
+ super().__init__(debugger)
115
+ self.debugger = debugger
116
+ self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
117
+
118
+ self._rows: dict[str, dict] = {}
119
+ self._variable_set: set[str] = set()
120
+ self._placeholder: QLabel | None = None
121
+
122
+ self.layout = QVBoxLayout(self)
123
+ self.layout.setContentsMargins(0, 0, 0, 0)
124
+ self.layout.setSpacing(2)
125
+
126
+ self._show_placeholder()
127
+
128
+ # ------------------------------------------------------------------
129
+ def _show_placeholder(self):
130
+ if self._placeholder is not None:
131
+ return
132
+ self._placeholder = QLabel("No variables watched. Click + to add.")
133
+ self._placeholder.setStyleSheet("color: #666; font-style: italic; padding: 4px 2px;")
134
+ self.layout.addWidget(self._placeholder)
135
+
136
+ def _hide_placeholder(self):
137
+ if self._placeholder is None:
138
+ return
139
+ self.layout.removeWidget(self._placeholder)
140
+ self._placeholder.deleteLater()
141
+ self._placeholder = None
142
+
143
+ # ------------------------------------------------------------------
144
+ def addVariable(self, name: str):
145
+ if not name or name in self._variable_set:
146
+ return
147
+ if not hasattr(self.debugger, 'watched'):
148
+ self.debugger.watched = [] # type: ignore[attr-defined]
149
+ if name not in self.debugger.watched: # type: ignore[attr-defined]
150
+ self.debugger.watched.append(name) # type: ignore[attr-defined]
151
+
152
+ # Ensure placeholder hidden
153
+ self._hide_placeholder()
154
+
155
+ # Build a simple row: [ QLabel("Name = value") | remove_btn ]
156
+ row_widget = QWidget(self)
157
+ row_layout = QHBoxLayout(row_widget)
158
+ row_layout.setContentsMargins(4, 0, 4, 0)
159
+ row_layout.setSpacing(6)
160
+
161
+ label = QLabel("")
162
+ label.setWordWrap(False)
163
+ row_layout.addWidget(label, 1)
164
+
165
+ remove_btn = QPushButton("–")
166
+ remove_btn.setFixedSize(22, 22)
167
+
168
+ def on_remove():
169
+ try:
170
+ if hasattr(self.debugger, 'watched') and name in self.debugger.watched: # type: ignore[attr-defined]
171
+ self.debugger.watched.remove(name) # type: ignore[attr-defined]
172
+ if name in self._variable_set:
173
+ self._variable_set.remove(name)
174
+ self.layout.removeWidget(row_widget)
175
+ row_widget.deleteLater()
176
+ self._rows.pop(name, None)
177
+ if not self._rows:
178
+ self._show_placeholder()
179
+ except Exception:
180
+ pass
181
+
182
+ remove_btn.clicked.connect(on_remove)
183
+ row_layout.addWidget(remove_btn, 0)
184
+
185
+ # Track row
186
+ self.layout.addWidget(row_widget)
187
+ self._rows[name] = {
188
+ 'widget': row_widget,
189
+ 'label': label,
190
+ }
191
+ self._variable_set.add(name)
192
+
193
+ # Initial refresh for the new row
194
+ try:
195
+ self._refresh_one(name, self.debugger.program)
196
+ except Exception:
197
+ pass
198
+
199
+ # ------------------------------------------------------------------
200
+ def _refresh_one(self, name: str, program):
201
+ row = self._rows.get(name)
202
+ if not row:
203
+ return
204
+ try:
205
+ # ValueDisplay reduced to textify given symbol name
206
+ val_display = ValueDisplay()
207
+ val_display.setValue(program, name)
208
+ value_text = val_display.text()
209
+ except Exception as e:
210
+ value_text = f"<error: {e}>"
211
+ row['label'].setText(f"{name} = {value_text}")
212
+
213
+ # ------------------------------------------------------------------
214
+ def refreshVariables(self, program):
215
+ if not self._rows:
216
+ self._show_placeholder()
217
+ return
218
+ for name in list(self._rows.keys()):
219
+ self._refresh_one(name, program)