easycoder 251215.2__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 (48) hide show
  1. easycoder/__init__.py +4 -3
  2. easycoder/debugger/ec_dbg_value_display copy.py +1 -1
  3. easycoder/debugger/ec_dbg_value_display.py +12 -11
  4. easycoder/debugger/ec_dbg_watchlist.py +146 -12
  5. easycoder/debugger/ec_debug.py +85 -8
  6. easycoder/ec_classes.py +228 -25
  7. easycoder/ec_compiler.py +29 -8
  8. easycoder/ec_core.py +364 -242
  9. easycoder/ec_gclasses.py +20 -9
  10. easycoder/ec_graphics.py +42 -26
  11. easycoder/ec_handler.py +4 -3
  12. easycoder/ec_mqtt.py +248 -0
  13. easycoder/ec_program.py +63 -28
  14. easycoder/ec_psutil.py +1 -1
  15. easycoder/ec_value.py +57 -36
  16. easycoder/pre/README.md +3 -0
  17. easycoder/pre/__init__.py +17 -0
  18. easycoder/pre/debugger/__init__.py +5 -0
  19. easycoder/pre/debugger/ec_dbg_value_display copy.py +195 -0
  20. easycoder/pre/debugger/ec_dbg_value_display.py +24 -0
  21. easycoder/pre/debugger/ec_dbg_watch_list copy.py +219 -0
  22. easycoder/pre/debugger/ec_dbg_watchlist.py +293 -0
  23. easycoder/pre/debugger/ec_debug.py +1014 -0
  24. easycoder/pre/ec_border.py +67 -0
  25. easycoder/pre/ec_classes.py +470 -0
  26. easycoder/pre/ec_compiler.py +291 -0
  27. easycoder/pre/ec_condition.py +27 -0
  28. easycoder/pre/ec_core.py +2772 -0
  29. easycoder/pre/ec_gclasses.py +230 -0
  30. easycoder/pre/ec_graphics.py +1682 -0
  31. easycoder/pre/ec_handler.py +79 -0
  32. easycoder/pre/ec_keyboard.py +439 -0
  33. easycoder/pre/ec_program.py +557 -0
  34. easycoder/pre/ec_psutil.py +48 -0
  35. easycoder/pre/ec_timestamp.py +11 -0
  36. easycoder/pre/ec_value.py +124 -0
  37. easycoder/pre/icons/close.png +0 -0
  38. easycoder/pre/icons/exit.png +0 -0
  39. easycoder/pre/icons/run.png +0 -0
  40. easycoder/pre/icons/step.png +0 -0
  41. easycoder/pre/icons/stop.png +0 -0
  42. easycoder/pre/icons/tick.png +0 -0
  43. {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/METADATA +1 -1
  44. easycoder-260111.1.dist-info/RECORD +59 -0
  45. easycoder-251215.2.dist-info/RECORD +0 -31
  46. {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/WHEEL +0 -0
  47. {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/entry_points.txt +0 -0
  48. {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/licenses/LICENSE +0 -0
easycoder/ec_program.py CHANGED
@@ -1,6 +1,7 @@
1
- import time, sys
1
+ import time, sys, json
2
2
  from copy import deepcopy
3
3
  from collections import deque
4
+
4
5
  from .ec_classes import (
5
6
  Script,
6
7
  Token,
@@ -8,7 +9,8 @@ from .ec_classes import (
8
9
  RuntimeError,
9
10
  NoValueRuntimeError,
10
11
  ECObject,
11
- ECValue
12
+ ECValue,
13
+ normalize_type
12
14
  )
13
15
  from .ec_compiler import Compiler
14
16
  from .ec_core import Core
@@ -51,6 +53,7 @@ class Program:
51
53
  self.symbols = {}
52
54
  self.onError = 0
53
55
  self.debugStep = False
56
+ self.debugSkip = False
54
57
  self.stack = []
55
58
  self.script = Script(source)
56
59
  self.compiler = Compiler(self)
@@ -58,6 +61,7 @@ class Program:
58
61
  self.value = self.compiler.value
59
62
  self.condition = self.compiler.condition
60
63
  self.graphics = None
64
+ self.mqtt = None
61
65
  self.psutil = None
62
66
  self.useClass(Core)
63
67
  self.ticker = 0
@@ -117,6 +121,15 @@ class Program:
117
121
  self.useClass(Graphics)
118
122
  return True
119
123
 
124
+ # Use the MQTT module
125
+ def useMQTT(self):
126
+ if self.mqtt == None:
127
+ print('Loading MQTT module')
128
+ from .ec_mqtt import MQTT
129
+ self.mqtt = MQTT
130
+ self.useClass(MQTT)
131
+ return True
132
+
120
133
  # Use the psutil module
121
134
  def usePSUtil(self):
122
135
  if self.psutil == None:
@@ -163,6 +176,16 @@ class Program:
163
176
  object.setIndex(i) # type: ignore
164
177
  self.run(goto)
165
178
  return
179
+
180
+ # Ensure the program is running
181
+ def ensureRunning(self):
182
+ if not self.running:
183
+ raise RuntimeError(self, 'Improper use of runtime function')
184
+
185
+ # Ensure the program is not running
186
+ def ensureNotRunning(self):
187
+ if self.running:
188
+ raise RuntimeError(self, 'Improper use of non-runtime function')
166
189
 
167
190
  # Get the domain list
168
191
  def getDomains(self):
@@ -173,6 +196,7 @@ class Program:
173
196
 
174
197
  # Get the symbol record for a given name
175
198
  def getVariable(self, name):
199
+ self.ensureRunning()
176
200
  if isinstance(name, dict): name = name['name']
177
201
  try:
178
202
  target = self.code[self.symbols[name]]
@@ -199,10 +223,16 @@ class Program:
199
223
  def checkObjectType(self, object, classes):
200
224
  if isinstance(object, dict): return
201
225
  if not isinstance(object, classes):
226
+ if isinstance(classes, tuple):
227
+ class_names = ", ".join([c.__name__ for c in classes])
228
+ message = f"Variable {object.name} should be one of {class_names}"
229
+ else:
230
+ class_names = classes.__name__
231
+ message = f"Variable {object.name} should be {class_names}"
202
232
  if self.running:
203
- raise RuntimeError(self, f"Objects of type {type(object)} are not instances of {classes}")
233
+ raise RuntimeError(self, message)
204
234
  else:
205
- raise FatalError(self.compiler, f"Objects of type {type(object)} are not instances of {classes}")
235
+ raise FatalError(self.compiler, message)
206
236
 
207
237
  # Get the inner (non-EC) object from a name, record or object
208
238
  def getInnerObject(self, object):
@@ -217,13 +247,13 @@ class Program:
217
247
 
218
248
  def constant(self, content, numeric):
219
249
  result = {}
220
- result['type'] = 'int' if numeric else 'str'
250
+ result['type'] = int if numeric else str
221
251
  result['content'] = content
222
252
  return result
223
253
 
224
254
  # Test if an item is a string or a number
225
255
  def getItemType(self, value):
226
- return 'int' if isinstance(value, int) else 'str'
256
+ return int if isinstance(value, int) else str
227
257
 
228
258
  # Get the value of an item that may be an ECValue or a raw value. Return as an ECValue
229
259
  def getValueOf(self, item):
@@ -234,31 +264,31 @@ class Program:
234
264
  else: value = item
235
265
  else:
236
266
  varType = type(item).__name__
237
- if varType in ['int', 'str', 'bool', 'float', 'list', 'dict']:
238
- if varType == 'int': value.setValue(type='int', content=item)
239
- elif varType == 'str': value.setValue(type='str', content=item)
240
- elif varType == 'bool': value.setValue(type='boolean', content=item)
241
- elif varType == 'float': value.setValue(type='str', content=str(item))
242
- elif varType == 'list': value.setValue(type='list', content=item)
243
- elif varType == 'dict': value.setValue(type='dict', content=item)
244
- else: value.setValue(None)
267
+ if varType == 'int': value.setValue(type=int, content=item)
268
+ elif varType == 'str': value.setValue(type=str, content=item)
269
+ elif varType == 'bool': value.setValue(type=bool, content=item)
270
+ elif varType == 'float': value.setValue(type=str, content=str(item))
271
+ elif varType == 'list': value.setValue(type=list, content=item)
272
+ elif varType == 'dict': value.setValue(type=dict, content=item)
273
+ else: value.setValue(type=None, content=None)
245
274
  return value
246
275
 
247
276
  # Runtime function to evaluate an ECObject or ECValue. Returns another ECValue
248
277
  # This function may be called recursively by value handlers.
249
278
  def evaluate(self, item):
279
+ self.ensureRunning()
250
280
  if isinstance(item, ECObject):
251
281
  value = item.getValue()
252
282
  if value == None:
253
283
  raise RuntimeError(self, f'Symbol {item.getName()} not initialized')
254
284
  else: value = item
255
285
  try:
256
- valType = value.getType() # type: ignore
286
+ valType = normalize_type(value.getType()) # type: ignore
257
287
  except:
258
288
  RuntimeError(self, 'Value does not hold a valid ECValue')
259
289
  result = ECValue(type=valType)
260
290
 
261
- if valType in ('str', 'int', 'boolean', 'list', 'dict'):
291
+ if valType in ('str', 'int', 'bool', 'list', 'dict', None):
262
292
  # Simple value - just return the content
263
293
  result.setContent(value.getContent()) # type: ignore
264
294
 
@@ -276,13 +306,14 @@ class Program:
276
306
  result = variable.getValue() # type: ignore
277
307
  if isinstance(result, ECValue): return self.evaluate(result)
278
308
  if isinstance(result, ECObject): return result.getValue()
279
- else:
280
- # See if one of the domains can handle this value
281
- value = result
282
- result = None
283
- for domain in self.domains:
284
- result = domain.getUnknownValue(value)
285
- if result != None: break
309
+ if isinstance(result, dict) or isinstance(result, list):
310
+ return result
311
+ # See if one of the domains can handle this value
312
+ value = result
313
+ result = None
314
+ for domain in self.domains:
315
+ result = domain.getUnknownValue(value)
316
+ if result != None: break
286
317
 
287
318
  elif valType == 'cat':
288
319
  # Handle concatenation
@@ -292,8 +323,8 @@ class Program:
292
323
  if val != None:
293
324
  if isinstance(val, ECValue): val = str(val.getContent())
294
325
  if val == None: val = ''
295
- else: content += val
296
- result.setValue(type='str', content=content)
326
+ else: content += str(val)
327
+ result.setValue(type=str, content=content)
297
328
 
298
329
  else:
299
330
  # Call the given domain to handle a value
@@ -307,6 +338,7 @@ class Program:
307
338
 
308
339
  # Get the runtime value of a value object (as a string or integer)
309
340
  def textify(self, value):
341
+ self.ensureRunning()
310
342
  if value is None:
311
343
  return None
312
344
 
@@ -323,6 +355,8 @@ class Program:
323
355
  if v.getType() == 'object':
324
356
  return value.getContent() # type: ignore
325
357
  return v.getContent()
358
+ if isinstance(v, (dict, list)):
359
+ return json.dumps(v)
326
360
  return v
327
361
 
328
362
  # Get the content of a symbol
@@ -334,6 +368,7 @@ class Program:
334
368
 
335
369
  # Get the value of a symbol as an ECValue
336
370
  def getSymbolValue(self, record):
371
+ self.ensureRunning()
337
372
  object = self.getObject(record)
338
373
  self.checkObjectType(object, ECObject)
339
374
  value = object.getValue() # type: ignore
@@ -440,7 +475,7 @@ class Program:
440
475
  self.pc += 1
441
476
  else:
442
477
  keyword = command['keyword']
443
- if self.debugStep and 'debug' in command:
478
+ if self.debugStep and not self.debugSkip and 'debug' in command:
444
479
  lino = command['lino'] + 1
445
480
  line = self.script.lines[command['lino']].strip()
446
481
  print(f'{self.name}: Line {lino}: {domainName}:{keyword}: {line}')
@@ -449,9 +484,9 @@ class Program:
449
484
  if handler:
450
485
  command = self.code[self.pc]
451
486
  command['program'] = self
452
- if self.breakpoint:
453
- pass # Place a breakpoint here for a debugger to catch
454
487
  try:
488
+ if self.breakpoint:
489
+ pass # Place a breakpoint here for a debugger to catch
455
490
  self.pc = handler(command)
456
491
  except Exception as e:
457
492
  raise RuntimeError(self, f'Error during execution of {domainName}:{keyword}: {str(e)}')
easycoder/ec_psutil.py CHANGED
@@ -36,7 +36,7 @@ class PSUtil(Handler):
36
36
  def v_memory(self, v):
37
37
  process: Process = Process(os.getpid())
38
38
  megabytes: float = process.memory_info().rss / (1024 * 1024)
39
- return ECValue(domain=self.getName(), type='float', content=megabytes)
39
+ return ECValue(domain=self.getName(), type=float, content=megabytes)
40
40
 
41
41
  #############################################################################
42
42
  # Compile a condition
easycoder/ec_value.py CHANGED
@@ -1,8 +1,9 @@
1
+ from typing import Optional, List
1
2
  from .ec_classes import ECObject, FatalError, ECValue
2
3
 
3
4
  # Create a constant
4
5
  def getConstant(str):
5
- return ECValue(type='str', content=str)
6
+ return ECValue(type=str, content=str)
6
7
 
7
8
  class Value:
8
9
 
@@ -11,9 +12,10 @@ class Value:
11
12
  self.getToken = compiler.getToken
12
13
  self.nextToken = compiler.nextToken
13
14
  self.peek = compiler.peek
15
+ self.skip = compiler.skip
14
16
  self.tokenIs = compiler.tokenIs
15
17
 
16
- def getItem(self):
18
+ def getItem(self) -> Optional[ECValue]:
17
19
  token = self.getToken()
18
20
  if not token:
19
21
  return None
@@ -21,17 +23,17 @@ class Value:
21
23
  value = ECValue()
22
24
 
23
25
  if token == 'true':
24
- value.setValue('boolean', True)
26
+ value.setValue(bool, True)
25
27
  return value
26
28
 
27
29
  if token == 'false':
28
- value.setValue('boolean', False)
30
+ value.setValue(bool, False)
29
31
  return value
30
32
 
31
33
  # Check for a string constant
32
34
  if token[0] == '`':
33
35
  if token[len(token) - 1] == '`':
34
- value.setValue(type='str', content=token[1 : len(token) - 1])
36
+ value.setValue(type=str, content=token[1 : len(token) - 1])
35
37
  return value
36
38
  FatalError(self.compiler, f'Unterminated string "{token}"')
37
39
  return None
@@ -40,7 +42,7 @@ class Value:
40
42
  if token.isnumeric() or (token[0] == '-' and token[1:].isnumeric):
41
43
  val = eval(token)
42
44
  if isinstance(val, int):
43
- value.setValue('int', val)
45
+ value.setValue(int, val)
44
46
  return value
45
47
  FatalError(self.compiler, f'{token} is not an integer')
46
48
 
@@ -53,10 +55,43 @@ class Value:
53
55
  # self.compiler.warning(f'I don\'t understand \'{token}\'')
54
56
  return None
55
57
 
58
+ # Get a list of items following 'the cat of ...'
59
+ def getCatItems(self) -> Optional[List[ECValue]]:
60
+ items: List[ECValue] = []
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
+
56
78
  # Compile a value
57
- def compileValue(self):
79
+ def compileValue(self) -> Optional[ECValue]:
58
80
  token = self.getToken()
59
- item = self.getItem()
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()
60
95
  if item == None:
61
96
  self.compiler.warning(f'ec_value.compileValue: Cannot get the value of "{token}"')
62
97
  return None
@@ -64,39 +99,25 @@ class Value:
64
99
  object = self.compiler.getSymbolRecord(item.getContent())['object']
65
100
  if not object.hasRuntimeValue(): return None
66
101
 
67
- value = ECValue()
68
102
  if self.peek() == 'cat':
69
- items = None
70
- if value.getType() == 'cat': items = value.getContent()
71
- else:
72
- value.setType('cat')
73
- items = [item]
74
- value.setContent(items)
75
- while self.peek() == 'cat':
76
- v = self.nextToken()
77
- v= self.nextToken()
78
- element = self.getItem()
79
- if element != None:
80
- items.append(element) # pyright: ignore[reportOptionalMemberAccess]
81
- value.setContent(items)
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
114
  value = ECValue()
93
- if type(token) == 'str':
94
- token = eval(token)
95
- if isinstance(token, int):
96
- value.setValue(type='int', content=token)
97
- return value
98
- if isinstance(token, float):
99
- value.setValue(type='float', content=token)
100
- return value
101
- value.setValue(type='str', content=token)
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))
102
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
+