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.
- easycoder/__init__.py +4 -3
- easycoder/debugger/ec_dbg_value_display copy.py +1 -1
- easycoder/debugger/ec_dbg_value_display.py +12 -11
- easycoder/debugger/ec_dbg_watchlist.py +146 -12
- easycoder/debugger/ec_debug.py +85 -8
- easycoder/ec_classes.py +228 -25
- easycoder/ec_compiler.py +29 -8
- easycoder/ec_core.py +364 -242
- easycoder/ec_gclasses.py +20 -9
- easycoder/ec_graphics.py +42 -26
- easycoder/ec_handler.py +4 -3
- easycoder/ec_mqtt.py +248 -0
- easycoder/ec_program.py +63 -28
- easycoder/ec_psutil.py +1 -1
- easycoder/ec_value.py +57 -36
- 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/pre/debugger/ec_debug.py +1014 -0
- 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/pre/ec_graphics.py +1682 -0
- 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-251215.2.dist-info → easycoder-260111.1.dist-info}/METADATA +1 -1
- easycoder-260111.1.dist-info/RECORD +59 -0
- easycoder-251215.2.dist-info/RECORD +0 -31
- {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/WHEEL +0 -0
- {easycoder-251215.2.dist-info → easycoder-260111.1.dist-info}/entry_points.txt +0 -0
- {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,
|
|
233
|
+
raise RuntimeError(self, message)
|
|
204
234
|
else:
|
|
205
|
-
raise FatalError(self.compiler,
|
|
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'] =
|
|
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
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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', '
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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(
|
|
26
|
+
value.setValue(bool, True)
|
|
25
27
|
return value
|
|
26
28
|
|
|
27
29
|
if token == 'false':
|
|
28
|
-
value.setValue(
|
|
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=
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
value.setValue(type=
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
easycoder/pre/README.md
ADDED
|
@@ -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,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
|
+
|