PyClickerRPA 0.2.0__tar.gz → 0.3.1__tar.gz
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.
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PKG-INFO +7 -4
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/PKG-INFO +7 -4
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/SOURCES.txt +2 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/README.md +6 -3
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/capture.py +26 -10
- pyclickerrpa-0.3.1/autoclicker/expr.py +94 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/flow_connection.py +8 -1
- pyclickerrpa-0.3.1/autoclicker/gui/flow_editor.py +524 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/flow_node.py +55 -8
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/main_window.py +268 -37
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/qt_runner.py +16 -1
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/task_widget.py +80 -15
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/models.py +7 -1
- pyclickerrpa-0.3.1/autoclicker/ocr.py +107 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/runner.py +107 -5
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/windows.py +24 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/pyproject.toml +1 -1
- pyclickerrpa-0.3.1/tests/test_flow_branch.py +146 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_runner.py +37 -0
- pyclickerrpa-0.2.0/autoclicker/gui/flow_editor.py +0 -340
- pyclickerrpa-0.2.0/autoclicker/ocr.py +0 -63
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/LICENSE +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/entry_points.txt +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/requires.txt +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/PyClickerRPA.egg-info/top_level.txt +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/__init__.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/adb.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/app.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/clicker.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/dpi.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/__init__.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/hotkeys.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/monitor.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/preview_label.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/recorder.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/scheduler_widget.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/snippet_library.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/gui/testkit_widget.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/matcher.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/run.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/__init__.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/__main__.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/assertions.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/case.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/loader.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/pytest_plugin.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/report.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/runner.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/testkit/visual.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/__init__.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/__main__.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/run_manager.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/scheduler.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/serialize.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/server.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/static/app.js +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/static/index.html +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/autoclicker/web/static/style.css +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/setup.cfg +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_adb.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_matcher.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_models.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_testkit.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_testkit_gui.py +0 -0
- {pyclickerrpa-0.2.0 → pyclickerrpa-0.3.1}/tests/test_web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyClickerRPA
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, recorder, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
5
5
|
Author: Equinox
|
|
6
6
|
License-Expression: MIT
|
|
@@ -84,12 +84,15 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
84
84
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
85
85
|
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
86
86
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
87
|
-
count/var, set-var, read-text→variable,
|
|
88
|
-
call sub-task, groups
|
|
87
|
+
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
88
|
+
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
89
|
+
**stop** step to end a run — with retry / repeat / until policies.
|
|
89
90
|
|
|
90
91
|
## Authoring (desktop GUI)
|
|
91
92
|
|
|
92
|
-
Drag-and-drop **node flow graph**
|
|
93
|
+
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
94
|
+
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
95
|
+
interoperable **step list** with a full
|
|
93
96
|
per-step dialog; an action **recorder** (global hooks), snippet library, task
|
|
94
97
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
95
98
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyClickerRPA
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, recorder, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
5
5
|
Author: Equinox
|
|
6
6
|
License-Expression: MIT
|
|
@@ -84,12 +84,15 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
84
84
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
85
85
|
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
86
86
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
87
|
-
count/var, set-var, read-text→variable,
|
|
88
|
-
call sub-task, groups
|
|
87
|
+
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
88
|
+
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
89
|
+
**stop** step to end a run — with retry / repeat / until policies.
|
|
89
90
|
|
|
90
91
|
## Authoring (desktop GUI)
|
|
91
92
|
|
|
92
|
-
Drag-and-drop **node flow graph**
|
|
93
|
+
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
94
|
+
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
95
|
+
interoperable **step list** with a full
|
|
93
96
|
per-step dialog; an action **recorder** (global hooks), snippet library, task
|
|
94
97
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
95
98
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
@@ -13,6 +13,7 @@ autoclicker/app.py
|
|
|
13
13
|
autoclicker/capture.py
|
|
14
14
|
autoclicker/clicker.py
|
|
15
15
|
autoclicker/dpi.py
|
|
16
|
+
autoclicker/expr.py
|
|
16
17
|
autoclicker/matcher.py
|
|
17
18
|
autoclicker/models.py
|
|
18
19
|
autoclicker/ocr.py
|
|
@@ -52,6 +53,7 @@ autoclicker/web/static/app.js
|
|
|
52
53
|
autoclicker/web/static/index.html
|
|
53
54
|
autoclicker/web/static/style.css
|
|
54
55
|
tests/test_adb.py
|
|
56
|
+
tests/test_flow_branch.py
|
|
55
57
|
tests/test_matcher.py
|
|
56
58
|
tests/test_models.py
|
|
57
59
|
tests/test_runner.py
|
|
@@ -46,12 +46,15 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
46
46
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
47
47
|
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
48
48
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
49
|
-
count/var, set-var, read-text→variable,
|
|
50
|
-
call sub-task, groups
|
|
49
|
+
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
50
|
+
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
51
|
+
**stop** step to end a run — with retry / repeat / until policies.
|
|
51
52
|
|
|
52
53
|
## Authoring (desktop GUI)
|
|
53
54
|
|
|
54
|
-
Drag-and-drop **node flow graph**
|
|
55
|
+
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
56
|
+
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
57
|
+
interoperable **step list** with a full
|
|
55
58
|
per-step dialog; an action **recorder** (global hooks), snippet library, task
|
|
56
59
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
57
60
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
@@ -30,35 +30,51 @@ def _bitmap_to_bgr(bmp, width: int, height: int) -> np.ndarray:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def capture_window(hwnd) -> np.ndarray:
|
|
33
|
-
"""Capture the client area of *hwnd* as a BGR ndarray.
|
|
33
|
+
"""Capture the **client area** of *hwnd* as a BGR ndarray.
|
|
34
|
+
|
|
35
|
+
PrintWindow renders the whole window (title bar + borders + client) from the
|
|
36
|
+
window's top-left, so we render the full window and then crop to the client
|
|
37
|
+
rect. The result's (0, 0) is the client top-left — matching the Clicker's
|
|
38
|
+
``ClientToScreen`` mapping, so match coords click the right spot (no offset
|
|
39
|
+
by the non-client area / title bar).
|
|
40
|
+
"""
|
|
34
41
|
if not _HAS_WIN32:
|
|
35
42
|
raise RuntimeError("win32 modules unavailable")
|
|
43
|
+
import ctypes
|
|
36
44
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
_, _, cw, ch = win32gui.GetClientRect(hwnd) # client size
|
|
46
|
+
if cw <= 0 or ch <= 0:
|
|
47
|
+
return np.empty((0, 0, 3), dtype=np.uint8)
|
|
48
|
+
|
|
49
|
+
wl, wt, wr, wb = win32gui.GetWindowRect(hwnd) # whole-window screen rect
|
|
50
|
+
ww, wh = wr - wl, wb - wt
|
|
51
|
+
csx, csy = win32gui.ClientToScreen(hwnd, (0, 0)) # client top-left in screen
|
|
52
|
+
ox, oy = csx - wl, csy - wt # client offset within window
|
|
53
|
+
if ww <= 0 or wh <= 0:
|
|
40
54
|
return np.empty((0, 0, 3), dtype=np.uint8)
|
|
41
55
|
|
|
42
56
|
hwnd_dc = win32gui.GetWindowDC(hwnd)
|
|
43
57
|
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
|
|
44
58
|
save_dc = mfc_dc.CreateCompatibleDC()
|
|
45
59
|
bmp = win32ui.CreateBitmap()
|
|
46
|
-
bmp.CreateCompatibleBitmap(mfc_dc,
|
|
60
|
+
bmp.CreateCompatibleBitmap(mfc_dc, ww, wh) # full-window sized bitmap
|
|
47
61
|
save_dc.SelectObject(bmp)
|
|
48
62
|
|
|
49
63
|
try:
|
|
50
|
-
import ctypes
|
|
51
64
|
ok = ctypes.windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), PW_RENDERFULLCONTENT)
|
|
52
65
|
if not ok:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
img = _bitmap_to_bgr(bmp, width, height)
|
|
66
|
+
save_dc.BitBlt((0, 0), (ww, wh), mfc_dc, (0, 0), win32con.SRCCOPY)
|
|
67
|
+
full = _bitmap_to_bgr(bmp, ww, wh)
|
|
56
68
|
finally:
|
|
57
69
|
win32gui.DeleteObject(bmp.GetHandle())
|
|
58
70
|
save_dc.DeleteDC()
|
|
59
71
|
mfc_dc.DeleteDC()
|
|
60
72
|
win32gui.ReleaseDC(hwnd, hwnd_dc)
|
|
61
|
-
|
|
73
|
+
|
|
74
|
+
ox = max(0, min(ox, max(0, ww - 1)))
|
|
75
|
+
oy = max(0, min(oy, max(0, wh - 1)))
|
|
76
|
+
client = full[oy:oy + ch, ox:ox + cw]
|
|
77
|
+
return np.ascontiguousarray(client)
|
|
62
78
|
|
|
63
79
|
|
|
64
80
|
def capture_screen() -> np.ndarray:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Safe arithmetic/string expression evaluator for the SetVar step.
|
|
2
|
+
|
|
3
|
+
`SetVar` values starting with ``=`` are evaluated here (e.g. ``=count + 1``,
|
|
4
|
+
``=max(a, b) * 2``, ``=upper(name)``). Variable names resolve against the
|
|
5
|
+
runner's variable dict — numeric-looking values become numbers, everything
|
|
6
|
+
else stays a string; unknown names default to 0.
|
|
7
|
+
|
|
8
|
+
Security: this never calls the builtin ``eval``. It walks a whitelisted AST
|
|
9
|
+
and rejects attribute access, subscripting, comprehensions, and any call to a
|
|
10
|
+
function outside a fixed safe set — so ``__import__(...)`` / ``().__class__``
|
|
11
|
+
style escapes raise ``ValueError`` instead of executing.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import ast
|
|
16
|
+
import operator
|
|
17
|
+
|
|
18
|
+
_BIN_OPS = {
|
|
19
|
+
ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
|
|
20
|
+
ast.Div: operator.truediv, ast.FloorDiv: operator.floordiv,
|
|
21
|
+
ast.Mod: operator.mod, ast.Pow: operator.pow,
|
|
22
|
+
}
|
|
23
|
+
_UNARY_OPS = {ast.UAdd: operator.pos, ast.USub: operator.neg, ast.Not: operator.not_}
|
|
24
|
+
_CMP_OPS = {
|
|
25
|
+
ast.Eq: operator.eq, ast.NotEq: operator.ne, ast.Lt: operator.lt,
|
|
26
|
+
ast.LtE: operator.le, ast.Gt: operator.gt, ast.GtE: operator.ge,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_FUNCS = {
|
|
30
|
+
"int": int, "float": float, "str": str, "round": round, "abs": abs,
|
|
31
|
+
"min": min, "max": max, "len": len,
|
|
32
|
+
"upper": lambda s: str(s).upper(), "lower": lambda s: str(s).lower(),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _coerce(value: str):
|
|
37
|
+
"""A variable value string → number when it looks numeric, else the string."""
|
|
38
|
+
try:
|
|
39
|
+
f = float(value)
|
|
40
|
+
return int(f) if f.is_integer() else f
|
|
41
|
+
except (TypeError, ValueError):
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _eval(node, variables):
|
|
46
|
+
if isinstance(node, ast.Expression):
|
|
47
|
+
return _eval(node.body, variables)
|
|
48
|
+
if isinstance(node, ast.Constant): # numbers, strings, True/False/None
|
|
49
|
+
return node.value
|
|
50
|
+
if isinstance(node, ast.Name):
|
|
51
|
+
return _coerce(variables.get(node.id, "0"))
|
|
52
|
+
if isinstance(node, ast.BinOp) and type(node.op) in _BIN_OPS:
|
|
53
|
+
return _BIN_OPS[type(node.op)](_eval(node.left, variables), _eval(node.right, variables))
|
|
54
|
+
if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY_OPS:
|
|
55
|
+
return _UNARY_OPS[type(node.op)](_eval(node.operand, variables))
|
|
56
|
+
if isinstance(node, ast.BoolOp):
|
|
57
|
+
vals = [_eval(v, variables) for v in node.values]
|
|
58
|
+
result = vals[0]
|
|
59
|
+
for v in vals[1:]:
|
|
60
|
+
result = (result and v) if isinstance(node.op, ast.And) else (result or v)
|
|
61
|
+
return result
|
|
62
|
+
if isinstance(node, ast.Compare):
|
|
63
|
+
left = _eval(node.left, variables)
|
|
64
|
+
for op, comparator in zip(node.ops, node.comparators):
|
|
65
|
+
if type(op) not in _CMP_OPS:
|
|
66
|
+
raise ValueError(f"不支持的比较运算符: {type(op).__name__}")
|
|
67
|
+
right = _eval(comparator, variables)
|
|
68
|
+
if not _CMP_OPS[type(op)](left, right):
|
|
69
|
+
return False
|
|
70
|
+
left = right
|
|
71
|
+
return True
|
|
72
|
+
if isinstance(node, ast.Call):
|
|
73
|
+
if not isinstance(node.func, ast.Name) or node.func.id not in _FUNCS:
|
|
74
|
+
raise ValueError("只允许调用: " + ", ".join(sorted(_FUNCS)))
|
|
75
|
+
if node.keywords:
|
|
76
|
+
raise ValueError("函数不支持关键字参数")
|
|
77
|
+
args = [_eval(a, variables) for a in node.args]
|
|
78
|
+
return _FUNCS[node.func.id](*args)
|
|
79
|
+
raise ValueError(f"表达式包含不允许的语法: {type(node).__name__}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def safe_eval(expr: str, variables: dict) -> str:
|
|
83
|
+
"""Evaluate `expr` against `variables`, returning a string result.
|
|
84
|
+
|
|
85
|
+
Integer-valued floats render without a trailing ``.0`` so counters stay clean.
|
|
86
|
+
Raises ``ValueError`` (or SyntaxError) on anything outside the whitelist.
|
|
87
|
+
"""
|
|
88
|
+
tree = ast.parse(expr, mode="eval")
|
|
89
|
+
result = _eval(tree, variables)
|
|
90
|
+
if isinstance(result, float) and result.is_integer():
|
|
91
|
+
return str(int(result))
|
|
92
|
+
if isinstance(result, bool):
|
|
93
|
+
return "1" if result else "0"
|
|
94
|
+
return str(result)
|
|
@@ -65,7 +65,14 @@ class FlowConnection(QGraphicsPathItem):
|
|
|
65
65
|
dy = end.y() - start.y()
|
|
66
66
|
ctrl = min(dy * 0.5, 80.0)
|
|
67
67
|
path.cubicTo(start.x(), start.y() + ctrl, end.x(), end.y() - ctrl, end.x(), end.y())
|
|
68
|
-
self.
|
|
68
|
+
branch = getattr(self.from_port, "branch", None)
|
|
69
|
+
if branch == "true":
|
|
70
|
+
col = QColor(76, 175, 80) # 真:绿
|
|
71
|
+
elif branch == "false":
|
|
72
|
+
col = QColor(229, 57, 53) # 假:红
|
|
73
|
+
else:
|
|
74
|
+
col = QColor(200, 200, 200)
|
|
75
|
+
self.setPen(QPen(col, 2))
|
|
69
76
|
|
|
70
77
|
self.setPath(path)
|
|
71
78
|
mid = path.pointAtPercent(0.5)
|