chore(gdb): fix several edge case (#10477)

This commit is contained in:
Gabor Kiss-Vamosi
2026-09-01 12:11:11 +00:00
committed by GitHub
parent 7b0d56966d
commit 0ece6e7ff3
75 changed files with 2837 additions and 470 deletions
File diff suppressed because it is too large Load Diff
+50 -5
View File
@@ -1,7 +1,7 @@
# lvglgdb
GDB Python extension for inspecting and debugging LVGL internals.
Works with live debugging sessions, core dumps, and other
GDB Python extension for inspecting and debugging LVGL internals.
Works with live debugging sessions, core dumps, and other
GDB-compatible targets.
## Installation
@@ -20,8 +20,11 @@ py import lvglgdb
### Dump Commands
Where a command takes a widget, style, class or layer, any C expression GDB can
evaluate will do: a variable, a member chain, or a plain address.
```bash
dump obj # Dump widget tree
dump obj # Dump widget tree (same listing as `info widget`)
dump display -f png # Dump display framebuffer as PNG
dump cache image # Dump image cache entries
dump cache image_header # Dump image header cache entries
@@ -32,18 +35,28 @@ dump indev # Dump input devices
dump group # Dump focus groups
dump image_decoder # Dump registered image decoders
dump fs_drv # Dump filesystem drivers
dump draw_task <layer_expr> # Dump draw tasks for a layer
dump draw_task lv_global->disp_default->layer_head # Draw tasks of a layer
dump dashboard # Generate interactive HTML dashboard
dump dashboard -o out.html # Save dashboard to file
dump widget props lv_keyboard # Which fields a widget class has
```
### Info Commands
```bash
info lvgl_version # LVGL version of the target, and this plugin's version
info widget # List every widget, indented by tree depth
info widget my_obj # All fields of one widget, incl. its own struct
info widget 0x50e000000820 # ...an address works as well as a variable
info widget my_obj text # Only the named field(s)
info widget ball_1 # Find it by name, anywhere on any screen
info widget label3 -p screen_1/button_2 # ...or by name below a parent path
info widget screen_1/button_2/label3 # ...or by a full name path
info style my_style # Inspect a single lv_style_t
info style --obj my_obj # Inspect all styles of an lv_obj_t
# sizes as 100%/content, enums and fonts by name
info draw_unit # Show draw unit information
info obj_class obj->class_p # Show object class hierarchy
info obj_class &lv_button_class # Show object class hierarchy
info subject &my_subject # Show subject and its observers
```
@@ -53,6 +66,38 @@ info subject &my_subject # Show subject and its observers
layer view, widget tree, style inspector, cache stats, animation list, and
draw buffer previews (RGB565 / RGB888 / ARGB8888 / XRGB8888).
## Regenerating the constant tables
The style, flag, state, event and widget tables are generated from the LVGL
headers, not written by hand. After changing an enum or adding a widget field,
regenerate them from this directory:
```bash
python3 scripts/generate_all.py
```
The headers are taken from the nearest LVGL checkout above this one. Set
`LVGL_ROOT` to read a different one — either a checkout or an installed tree
that only has the public headers under `include/lvgl/`:
```bash
LVGL_ROOT=~/lvgl python3 scripts/generate_all.py
```
A generator that cannot find LVGL stops with an error rather than writing an
empty table over a good one. Regenerating rewrites every table, so check the
full diff: a parsing change that looks harmless for one table can quietly empty
another.
## Tests
```bash
python3 -m unittest discover tests
```
The decoders and the generator helpers are pure Python, so these run without a
target and without GDB.
## Structure
```mermaid
+15 -1
View File
@@ -1,6 +1,15 @@
import gdb
from .core import DumpObj, DumpIndev, DumpGroup, InfoObjClass, InfoSubject
from .core import (
DumpObj,
DumpIndev,
DumpGroup,
InfoObjClass,
InfoSubject,
InfoWidget,
DumpWidget,
DumpWidgetProps,
)
from .display import DumpDisplayBuf
from .draw import InfoDrawUnit, DumpDrawTask
from .misc import (
@@ -12,6 +21,7 @@ from .misc import (
DumpTimer,
DumpImageDecoder,
DumpFsDrv,
InfoVersion,
)
from .dashboard import DumpDashboard
from .debugger import Debugger
@@ -42,12 +52,16 @@ DumpFsDrv()
DumpIndev()
DumpGroup()
DumpDrawTask()
DumpWidget() # prefix for 'dump widget props'
DumpWidgetProps()
# Infos
InfoStyle()
InfoDrawUnit()
InfoObjClass()
InfoSubject()
InfoVersion()
InfoWidget()
# Drivers
Lvglobal()
@@ -3,6 +3,7 @@ from .lv_indev import DumpIndev
from .lv_group import DumpGroup
from .lv_obj_class import InfoObjClass
from .lv_observer import InfoSubject
from .lv_widget import InfoWidget, DumpWidget, DumpWidgetProps
__all__ = [
"DumpObj",
@@ -10,4 +11,7 @@ __all__ = [
"DumpGroup",
"InfoObjClass",
"InfoSubject",
"InfoWidget",
"DumpWidget",
"DumpWidgetProps",
]
+11 -35
View File
@@ -1,9 +1,8 @@
import argparse
import gdb
from lvglgdb.lvgl import curr_inst
from lvglgdb.lvgl import LVObject, dump_obj_info
from lvglgdb.value import CorruptedError
from .lv_widget import print_all_widgets, print_tree, resolve_widget
class DumpObj(gdb.Command):
@@ -14,24 +13,6 @@ class DumpObj(gdb.Command):
"dump obj", gdb.COMMAND_USER, gdb.COMPLETE_EXPRESSION
)
def dump_obj(self, obj: LVObject, depth=0, limit=None):
if not obj:
return
# dump self
print(" " * depth, end="")
dump_obj_info(obj)
if limit is not None and depth >= limit:
return
# dump children
try:
for child in obj.children:
self.dump_obj(child, depth + 1, limit=limit)
except CorruptedError:
print(" " * (depth + 1) + "(corrupted children)")
def invoke(self, args, from_tty):
parser = argparse.ArgumentParser(description="Dump lvgl obj tree.")
parser.add_argument(
@@ -46,25 +27,20 @@ class DumpObj(gdb.Command):
type=str,
nargs="?",
default=None,
help="Optional root obj to dump.",
help="Optional root widget: a name, a name path, or an expression.",
)
try:
args = parser.parse_args(gdb.string_to_argv(args))
except SystemExit:
return
# The listing is `info widget`'s, so both commands print the same tree
# and there is only one renderer to keep current with the widget data.
if args.root:
root = gdb.parse_and_eval(args.root)
root = LVObject(root)
self.dump_obj(root, limit=args.level)
root, error = resolve_widget(args.root)
if root is None:
print(f"Error: {error}")
return
print_tree(root, limit=args.level)
else:
# dump all displays
depth = 0
for disp in curr_inst().displays():
print(f"Display {hex(disp)}")
try:
for screen in disp.screens:
print(f"{' ' * (depth + 1)}Screen@{hex(screen)}")
self.dump_obj(screen, depth=depth + 1, limit=args.level)
except CorruptedError:
print(f"{' ' * (depth + 1)}(corrupted screens)")
print_all_widgets(limit=args.level)
File diff suppressed because it is too large Load Diff
@@ -4,6 +4,7 @@ from .lv_anim import DumpAnim
from .lv_timer import DumpTimer
from .lv_image_decoder import DumpImageDecoder
from .lv_fs import DumpFsDrv
from .lv_version import InfoVersion
__all__ = [
"InfoStyle",
@@ -14,4 +15,5 @@ __all__ = [
"DumpTimer",
"DumpImageDecoder",
"DumpFsDrv",
"InfoVersion",
]
+114
View File
@@ -0,0 +1,114 @@
import os
import re
import gdb
import lvglgdb
def _call(expression):
try:
return gdb.parse_and_eval(expression)
except gdb.error:
return None
_VERSION_MACRO = re.compile(
r"#define\s+LVGL_VERSION_(MAJOR|MINOR|PATCH|INFO)\s+(\S+)")
def version_from_sources():
"""The version read from the lv_version.h the target was built against.
Neither way of asking the binary is dependable. lv_version_major() is
`static inline`, so it is dropped unless something calls it, and the macros
need -g3 *and* a stop inside a translation unit that included lv_version.h -
lv_timer.c, where a breakpoint on lv_timer_handler lands, does not.
DWARF does record the path of every source file, so the LVGL tree can be
found through any of its symbols and the version header read from disk.
Returns (major, minor, patch, info) or None.
"""
for symbol in ("lv_obj_create", "lv_timer_handler", "lv_init"):
try:
found = gdb.lookup_global_symbol(symbol)
if found is None or found.symtab is None:
continue
# DWARF records whatever separator the compiler used, which is not
# always the host's: a MinGW build writes forward slashes.
path = found.symtab.fullname().replace("\\", "/")
marker = "/src/"
if marker not in path:
continue
root = path[:path.rindex(marker)]
# Since 9.6 the public headers live in include/lvgl/ and the
# top-level lv_version.h is a deprecated wrapper that only includes
# the real one, so the defines are not in it. Read that one first
# and keep the top-level file for the releases that still have it.
for header in (os.path.join(root, "include", "lvgl", "lv_version.h"),
os.path.join(root, "lv_version.h")):
if not os.path.exists(header):
continue
with open(header) as f:
macros = dict(_VERSION_MACRO.findall(f.read()))
if not {"MAJOR", "MINOR", "PATCH"} <= set(macros):
continue
return (int(macros["MAJOR"]), int(macros["MINOR"]),
int(macros["PATCH"]), macros.get("INFO", '""').strip('"'))
except (gdb.error, OSError, ValueError):
continue
return None
def lvgl_version():
"""The LVGL version of the target, as (major, minor, patch, info).
lv_version_major() and friends in lvgl.h are the intended way to ask, with
the version macros as a second try. Both depend on how the target was
built, so lv_version.h itself is the third and most dependable one.
Fields that cannot be read come back as None.
"""
parts = []
for name in ("major", "minor", "patch"):
value = _call(f"lv_version_{name}()")
if value is None:
value = _call(f"LVGL_VERSION_{name.upper()}")
parts.append(int(value) if value is not None else None)
info = _call("lv_version_info()") or _call("LVGL_VERSION_INFO")
try:
info = info.string() if info is not None else None
except gdb.error:
info = None
# `info` counts as missing too: a build that hides it loses the "-dev" that
# tells a development version from a release.
if any(p is None for p in parts) or info is None:
from_sources = version_from_sources()
if from_sources is not None:
return from_sources
return (*parts, info)
def version_string():
""""9.6.0-dev", or as much of it as the target makes readable."""
major, minor, patch, info = lvgl_version()
if major is None:
return ("unknown: lv_version_major() was inlined away, the version "
"macros need a -g3 build, and LVGL's sources are not on disk")
numbers = ".".join("?" if p is None else str(p) for p in (major, minor, patch))
return numbers + (f"-{info}" if info else "")
class InfoVersion(gdb.Command):
"""print the LVGL version of the target, and this plugin's version"""
def __init__(self):
super(InfoVersion, self).__init__(
"info lvgl_version", gdb.COMMAND_USER, gdb.COMPLETE_NONE
)
def invoke(self, args, from_tty):
print(f"LVGL: {version_string()}")
print(f"lvglgdb: {lvglgdb.__version__}")
+5 -1
View File
@@ -31,7 +31,11 @@ class LVGL:
def displays(self) -> "Iterator[LVDisplay]":
ll = self.lv_global.disp_ll
if not ll:
# disp_ll is an inline lv_ll_t, not a pointer. Some GDB builds cannot
# convert an aggregate to a boolean, and Value.__bool__ answers False
# when that conversion fails - which would report no displays at all.
# The list is empty exactly when it has no head.
if not ll.head:
return
from ..display.lv_display import LVDisplay
+17 -2
View File
@@ -1,3 +1,5 @@
import gdb
from lvglgdb.value import Value, ValueInput
from ..misc.lv_ll import LVList
@@ -59,10 +61,23 @@ class LVGroup(Value):
def obj_count(self) -> int:
return LVList(self.obj_ll, "lv_obj_t").len
def _members(self):
"""The objects in the group.
lv_group_init() sizes obj_ll for `lv_obj_t *`, so each node holds a
pointer to the object rather than the object itself. Reading the node as
an lv_obj_t would report the list's own memory as a widget.
"""
obj_ptr_t = gdb.lookup_type("lv_obj_t").pointer()
for node in LVList(self.obj_ll):
# The node's payload is the pointer, so read through it the same way
# LVList reads its own next/prev links.
yield Value(int(node)).cast(obj_ptr_t, ptr=True).dereference()
def __iter__(self):
from .lv_obj import LVObject
for obj_ptr in LVList(self.obj_ll, "lv_obj_t"):
for obj_ptr in self._members():
yield LVObject(obj_ptr)
def snapshot(self):
@@ -80,7 +95,7 @@ class LVGroup(Value):
focused_addr = None
member_addrs = []
for obj_ptr in LVList(self.obj_ll, "lv_obj_t"):
for obj_ptr in self._members():
addr = int(obj_ptr)
if addr:
member_addrs.append(hex(addr))
+43 -3
View File
@@ -91,9 +91,49 @@ class LVObject(Value):
@property
def flags_raw(self) -> int:
"""Return raw flags bitmask, 0 if corrupted."""
raw = self.safe_field("flags", 0)
return int(raw)
"""Return raw flags bitmask, 0 if corrupted.
Up to LVGL 9.5 this was a single `flags` bitmask. Since 9.6 each flag
is its own bitfield named after it, so the mask is rebuilt from them.
Without this every object reports no flags at all, because safe_field()
cannot tell a missing field from a clear one.
"""
from .lv_obj_flag_consts import OBJ_FLAG_NAMES
raw = self.safe_field("flags")
if raw is not None:
return int(raw)
value = 0
for bit, name in OBJ_FLAG_NAMES.items():
# Composite names (SCROLL_CHAIN) have no field of their own.
field = self.safe_field(name.lower())
if field is not None and int(field):
value |= bit
return value | self._user_flags_mask()
def _user_flags_mask(self) -> int:
"""The eight flags 9.6 keeps in spec_attr->user_flags, as a flag mask.
These have no bitfield of their own, so the rebuild above misses all of
them - and LAYOUT_1 is LV_OBJ_FLAG_FLEX_IN_NEW_TRACK, so losing it costs
a flex layout its wrapping.
"""
from .lv_obj_flag_consts import OBJ_FLAG_NAMES
raw = self._get_spec_int("user_flags")
if not raw:
return 0
# The order lv_obj_add_flag() writes them in, from bit 0 up.
names = ("LAYOUT_1", "LAYOUT_2", "WIDGET_1", "WIDGET_2",
"USER_1", "USER_2", "USER_3", "USER_4")
bit_of = {name: bit for bit, name in OBJ_FLAG_NAMES.items()}
value = 0
for index, name in enumerate(names):
if raw & (1 << index) and name in bit_of:
value |= bit_of[name]
return value
@property
def flags_list(self) -> list[str]:
+7 -4
View File
@@ -37,7 +37,7 @@ def safe_collect(
def collect_xxx(): ...
In decorator mode, exceptions cause a gdb warning and return [].
In iteration mode, per-item exceptions are skipped silently.
In iteration mode, a failing item is warned about and left out.
"""
if isinstance(items_or_label, str):
label = items_or_label
@@ -60,8 +60,11 @@ def safe_collect(
for item in items_or_label:
try:
result.append(transform(item))
except Exception:
except Exception as e:
# Dropping an object drops everything below it too, so say so
# rather than letting a whole subtree vanish from a dump.
gdb.write(f"Warning: skipped an item: {type(e).__name__}: {e}\n")
continue
except Exception:
pass
except Exception as e:
gdb.write(f"Warning: stopped collecting early: {type(e).__name__}: {e}\n")
return result
+47 -4
View File
@@ -1,12 +1,40 @@
from __future__ import annotations
from pathlib import Path
from typing import Optional
import gdb
import numpy as np
from PIL import Image
from lvglgdb.value import Value, ValueInput
# numpy and Pillow are needed to turn pixels into an image, and nothing else in
# the plugin needs them. Importing them here would make every command - and
# `import lvglgdb` itself - fail on a GDB whose Python does not have them, so
# they are imported where they are used. `from __future__ import annotations`
# above keeps the type hints from needing them at definition time.
np = None
Image = None
def _require_imaging():
"""Import numpy and Pillow, with a message that says where they must go."""
global np, Image
if np is not None and Image is not None:
return
try:
import numpy
from PIL import Image as pil_image
except ImportError as e:
import sys
raise gdb.GdbError(
f"{e.name} is needed to read images, and has to be installed for "
f"the Python that GDB embeds "
f"({sys.version_info.major}.{sys.version_info.minor}), not for the "
f"one on your PATH. On Debian and Ubuntu: sudo apt install "
f"python3-{'numpy' if e.name == 'numpy' else 'pil'}"
) from e
np, Image = numpy, pil_image
class LVDrawBuf(Value):
"""LVGL draw buffer"""
@@ -84,13 +112,20 @@ class LVDrawBuf(Value):
Returns:
PIL.Image or None on failure.
"""
_require_imaging()
header = self.super_value("header")
stride = int(header["stride"])
height = int(header["h"])
cf_info = self.color_format_info()
data_ptr = self.super_value("data")
data_size = int(self.super_value("data_size"))
width = (stride * 8) // cf_info["bpp"] if cf_info["bpp"] else 0
# The width is in the header. Deriving it from the stride instead turns
# any row padding into extra pixels, which shears the whole image.
width = int(header["w"])
bpp = cf_info["bpp"]
row_bytes = (width * bpp + 7) // 8 if bpp else 0
if not stride:
stride = row_bytes
expected_data_size = stride * height
if not data_ptr or width <= 0 or height <= 0:
@@ -115,6 +150,13 @@ class LVDrawBuf(Value):
.read_memory(int(data_ptr), expected_data_size)
.tobytes()
)
if stride > row_bytes:
# _convert_to_image expects rows packed back to back, so drop the
# padding at the end of each row.
pixel_data = b"".join(
pixel_data[y * stride:y * stride + row_bytes]
for y in range(height)
)
return self._convert_to_image(pixel_data, width, height, cf_info["value"])
def data_dump(self, filename: str, format: str = None) -> bool:
@@ -185,6 +227,7 @@ class LVDrawBuf(Value):
Returns:
PIL.Image or None if conversion fails
"""
_require_imaging()
try:
if color_format == self._color_formats["RGB565"]:
# Convert RGB565 to RGB888
+4 -1
View File
@@ -5,6 +5,7 @@ from .lv_style import (
style_prop_name,
decode_selector,
format_style_value,
read_symbol,
)
from .lv_rb import LVRedBlackTree
from .lv_cache import LVCache
@@ -26,7 +27,7 @@ from .lv_anim import LVAnim
from .lv_timer import LVTimer
from .lv_image_decoder import LVImageDecoder
from .lv_fs import LVFsDrv
from .lv_utils import format_coord
from .lv_utils import format_coord, format_enum
__all__ = [
"LVList",
@@ -55,4 +56,6 @@ __all__ = [
"LVImageDecoder",
"LVFsDrv",
"format_coord",
"format_enum",
"read_symbol",
]
+11 -4
View File
@@ -68,7 +68,12 @@ class LVCache(Value):
def __iter__(self):
"""Create appropriate iterator based on cache class"""
return create_cache_iterator(self)
iterator = create_cache_iterator(self)
if iterator is None:
# Returning None here would make `for entry in cache` fail with
# TypeError about NoneType, which says nothing about the cache.
raise gdb.GdbError(f"unsupported cache type: {self.name}")
return iterator
def items(self):
"""Get all cache entries as a list"""
@@ -81,9 +86,11 @@ class LVCache(Value):
"""Run sanity check and print results as a table"""
from lvglgdb.lvgl.formatter import print_table
iterator = iter(self)
if iterator is None:
errors = [f"unsupported cache type: {self.name}"]
try:
iterator = iter(self)
except gdb.GdbError as unsupported:
iterator = ()
errors = [str(unsupported)]
else:
errors = iterator.sanity_check(entry_checker)
@@ -30,3 +30,8 @@ def create_cache_iterator(cache) -> LVCacheIteratorBase | None:
except Exception:
raise TypeError(f"Unsupported cache type {cache}")
# An unknown or null class: None is the documented answer, and callers check
# for it. LVCache.__iter__ turns it into an error that names the cache, so
# `for entry in cache` cannot fail with a bare TypeError about NoneType.
return None
@@ -30,7 +30,13 @@ class LVCacheLRURBIterator(LVCacheIteratorBase):
"""
for ll_node in LVList(lru_cache.ll):
ll_addr = int(ll_node)
# The node holds a pointer to the rb node, so the cast is to
# lv_rb_node_t ** and the pointer has to be read through before any
# field of it can be.
rb_node = Value(ll_node).cast(rb_node_pp_t)
if not rb_node:
continue
rb_node = rb_node.dereference()
if not rb_node:
continue
data = rb_node.data
+129 -18
View File
@@ -9,8 +9,25 @@ from .lv_style_consts import (
STATE_FLAGS,
COLOR_PROPS,
POINTER_PROPS,
SRC_PROPS,
COORD_PROPS,
BOOL_PROPS,
ENUM_PROP_VALUES,
COORD_TYPE_SHIFT,
)
_COORD_TYPE_MASK = 3 << COORD_TYPE_SHIFT
_COORD_TYPE_SPEC = 1 << COORD_TYPE_SHIFT
_COORD_MAX = (1 << COORD_TYPE_SHIFT) - 1
_PCT_STORED_MAX = _COORD_MAX - 1
_PCT_POS_MAX = _PCT_STORED_MAX // 2
# lv_image_src_get_type(): the first byte of an image source says what it is.
_IMAGE_HEADER_MAGIC = 0x19
_IMAGE_HEADER_LEGACY = 0x00
_symbol_cache: dict[int, "str | None"] = {}
def style_prop_name(prop_id: int) -> str:
"""Resolve style property ID to human-readable name."""
@@ -35,29 +52,104 @@ def decode_selector(selector: int) -> str:
return f"{part_str}|{state_str}"
def format_style_value(prop_id: int, value: Value) -> str:
"""Format a style value based on property type (with ANSI color block)."""
def decode_coord(raw: int) -> str:
"""Render an int32 coordinate the way the source wrote it.
LV_PCT() and LV_SIZE_CONTENT survive in the stored value as a type tag in
the top bits, so a percentage never has to be inferred from geometry.
"""
bits = raw & 0xFFFFFFFF
# Only LV_COORD_TYPE_SPEC carries an encoding. LV_COORD_TYPE_PX_NEG is not a
# second one: there is no LV_COORD_SET_PX_NEG, and in two's complement every
# negative int32 already has those top bits, so a "PX_NEG" value is just a
# negative number and prints as itself.
if bits & _COORD_TYPE_MASK != _COORD_TYPE_SPEC:
return str(raw)
plain = bits & ~_COORD_TYPE_MASK
if plain == _COORD_MAX:
return "content"
# Not unreachable: _COORD_TYPE_MASK covers bits 29-30 only, so a negative
# raw keeps bit 31 in `plain`. LVGL's LV_COORD_IS_PCT() guards the same way.
if plain > _PCT_STORED_MAX:
return str(raw)
pct = plain if plain <= _PCT_POS_MAX else _PCT_POS_MAX - plain
return f"{pct}%"
def decode_enum(prop_id: int, raw: int) -> str:
"""Name an enum value, decomposing bitmasks such as BORDER_SIDE.
Only single-bit members are combined, and only when they account for every
set bit. Otherwise a sequential enum like FLEX_FLOW would render an
out-of-range value as a meaningless list of its low members.
"""
names = ENUM_PROP_VALUES[prop_id]
if raw in names:
return names[raw]
parts, covered = [], 0
for bit in sorted(b for b in names if b and not b & (b - 1)):
if raw & bit:
parts.append(names[bit])
covered |= bit
return "|".join(parts) if parts and covered == raw else str(raw)
def read_symbol(addr: int) -> "str | None":
"""Resolve an address to its C symbol name, or None."""
if addr not in _symbol_cache:
try:
out = gdb.execute(f"info symbol {addr:#x}", to_string=True).strip()
except gdb.error:
out = ""
name = out.split(" in section ")[0] if " in section " in out else None
_symbol_cache[addr] = name
return _symbol_cache[addr]
def _forget_symbols(_event=None):
"""Addresses move when the program is re-run, so the cache must not outlive it."""
_symbol_cache.clear()
try:
gdb.events.exited.connect(_forget_symbols)
gdb.events.new_objfile.connect(_forget_symbols)
except AttributeError:
pass # older GDB, or imported outside a debug session
def _read_image_src(addr: int) -> "str | None":
"""Read an image source pointer the way lv_image_src_get_type() does."""
try:
if prop_id in COLOR_PROPS:
color = value.color
r = int(color.red) & 0xFF
g = int(color.green) & 0xFF
b = int(color.blue) & 0xFF
block = f"\033[48;2;{r};{g};{b}m \033[0m"
return f"#{r:02x}{g:02x}{b:02x} {block}"
elif prop_id in POINTER_PROPS:
ptr = int(value.ptr)
return f"{ptr:#x}" if ptr else "NULL"
else:
return str(int(value.num))
except CorruptedError:
return str(value)
# bytes(): GDB returns a memoryview whose element type differs
# between versions - b'\xf3' on some, 243 on others.
first = bytes(gdb.selected_inferior().read_memory(addr, 1))[0]
if first == _IMAGE_HEADER_MAGIC or first == _IMAGE_HEADER_LEGACY:
return None # an lv_image_dsc_t, so the symbol name is the useful part
return gdb.Value(addr).cast(gdb.lookup_type("char").pointer()).string()
except (gdb.error, gdb.MemoryError, UnicodeDecodeError):
return None
def _format_pointer(prop_id: int, addr: int) -> str:
"""Name a pointer property: its symbol, the string it points at, or its address."""
if not addr:
return "NULL"
symbol = read_symbol(addr)
if symbol:
return symbol
if prop_id in SRC_PROPS:
text = _read_image_src(addr)
if text:
return text
return f"{addr:#x}"
def _style_value_data(prop_id: int, value: Value) -> dict:
"""Extract style value as pure data dict (no ANSI codes).
Returns dict with 'value_str' and optional 'color_rgb'.
Returns dict with 'value_str', optional 'color_rgb' and, for pointers,
'ptr' so the raw address is never lost behind a resolved name.
"""
try:
if prop_id in COLOR_PROPS:
@@ -71,13 +163,32 @@ def _style_value_data(prop_id: int, value: Value) -> dict:
}
elif prop_id in POINTER_PROPS:
ptr = int(value.ptr)
return {"value_str": f"{ptr:#x}" if ptr else "NULL"}
data = {"value_str": _format_pointer(prop_id, ptr)}
if ptr:
data["ptr"] = f"{ptr:#x}"
return data
elif prop_id in ENUM_PROP_VALUES:
return {"value_str": decode_enum(prop_id, int(value.num))}
elif prop_id in BOOL_PROPS:
return {"value_str": "true" if int(value.num) else "false"}
elif prop_id in COORD_PROPS:
return {"value_str": decode_coord(int(value.num))}
else:
return {"value_str": str(int(value.num))}
except CorruptedError:
return {"value_str": str(value)}
def format_style_value(prop_id: int, value: Value) -> str:
"""Format a style value based on property type (with ANSI color block)."""
data = _style_value_data(prop_id, value)
rgb = data.get("color_rgb")
if rgb:
block = f"\033[48;2;{rgb['r']};{rgb['g']};{rgb['b']}m \033[0m"
return f"{data['value_str']} {block}"
return data["value_str"]
@dataclass
class StyleEntry:
"""A single resolved style property."""
File diff suppressed because it is too large Load Diff
+65 -3
View File
@@ -96,7 +96,69 @@ def format_coord(val):
if plain <= _PCT_POS_MAX:
return f"{plain}%"
return f"{_PCT_POS_MAX - plain}%"
if coord_type == _COORD_TYPE_PX_NEG:
plain = val & ~_COORD_TYPE_MASK
return str(-plain) if plain else "0"
# LV_COORD_TYPE_PX_NEG needs no branch of its own. There is no
# LV_COORD_SET_PX_NEG: the tag exists so LV_COORD_IS_PX() accepts negatives,
# and in two's complement every negative int32 already has those top bits,
# so such a value is an ordinary negative pixel count. Taking
# LV_COORD_PLAIN() of it and negating turned -5 into -536870907.
return str(val)
_enum_cache = {}
def _enum_members(type_name):
"""Members of a C enum as {value: name}, or None if the target has no such type."""
if type_name not in _enum_cache:
try:
members = {
int(f.enumval): f.name for f in gdb.lookup_type(type_name).fields()
}
except (gdb.error, AttributeError, TypeError):
members = None
_enum_cache[type_name] = members
return _enum_cache[type_name]
def format_enum(type_name, value, prefix="", bitmask=False):
"""Name a value of a C enum, reading the names from the target's debug info.
The fields holding these values are plain unsigned bitfields, so GDB prints
them as bare numbers and the enum has to be looked up by name. Reading it
from the target rather than from a generated table means the names always
match the binary in front of you - and when the enum was optimised out,
the raw number is still printed.
"""
if value is None:
return None
value = int(value)
members = _enum_members(type_name)
if not members:
return str(value)
def _short(name):
return name[len(prefix):] if prefix and name.startswith(prefix) else name
if value in members:
return _short(members[value])
if not bitmask:
return str(value)
parts, covered = [], 0
for bit in sorted(b for b in members if b and not b & (b - 1)):
if value & bit:
parts.append(_short(members[bit]))
covered |= bit
return "|".join(parts) if parts and covered == value else str(value)
def _forget_enums(_event=None):
"""Enum layouts belong to the objfile they were read from."""
_enum_cache.clear()
try:
gdb.events.exited.connect(_forget_enums)
gdb.events.new_objfile.connect(_forget_enums)
except AttributeError:
pass # older GDB, or imported outside a debug session
+22 -5
View File
@@ -57,12 +57,29 @@ def safe_point(obj, field_name):
def safe_wrapper(obj, field_name, module_path, class_name):
"""Read a struct field using its known Value wrapper, return snapshot dict."""
"""Read a struct field using its known Value wrapper, return snapshot dict.
Not every wrapper has a snapshot(); LVArray does not. Value forwards an
unknown attribute to gdb.Value.__getitem__, so asking for one raises
gdb.error rather than AttributeError, and the caller would otherwise lose
the whole widget - and its subtree - over a single field.
"""
import gdb
val = obj.safe_field(field_name)
if val is None or not getattr(val, 'is_ok', True):
return None
import importlib
mod = importlib.import_module(module_path)
cls = getattr(mod, class_name)
wrapper = cls(val)
return wrapper.snapshot().as_dict()
try:
mod = importlib.import_module(module_path)
cls = getattr(mod, class_name)
wrapper = cls(val)
if getattr(type(wrapper), "snapshot", None) is None:
# An embedded struct - lv_scale_t.needles - is not convertible to an
# int, so the address has to come from the field itself.
if val.type.strip_typedefs().code != gdb.TYPE_CODE_PTR:
val = val.address
return {"addr": hex(int(val)), "type": class_name}
return wrapper.snapshot().as_dict()
except (gdb.error, gdb.MemoryError, ImportError, AttributeError, TypeError):
return None
@@ -13,19 +13,20 @@ class LV3dtexture(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_3dtexture_t", ptr=True) or self
self._wv_lv_3dtexture_t = self.cast("lv_3dtexture_t", ptr=True) or self
self._wv = self._wv_lv_3dtexture_t
@property
def id(self):
return int(self._wv.safe_field("id", 0))
return int(self._wv_lv_3dtexture_t.safe_field("id", 0))
@property
def h_flip(self):
return int(self._wv.safe_field("h_flip", 0))
return int(self._wv_lv_3dtexture_t.safe_field("h_flip", 0))
@property
def v_flip(self):
return int(self._wv.safe_field("v_flip", 0))
return int(self._wv_lv_3dtexture_t.safe_field("v_flip", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,19 +14,20 @@ class LVAnimimg(LVImage):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_animimg_t", ptr=True) or self
self._wv_lv_animimg_t = self.cast("lv_animimg_t", ptr=True) or self
self._wv = self._wv_lv_animimg_t
@property
def anim(self):
return safe_wrapper(self._wv, "anim", "lvglgdb.lvgl.misc.lv_anim", "LVAnim")
return safe_wrapper(self._wv_lv_animimg_t, "anim", "lvglgdb.lvgl.misc.lv_anim", "LVAnim")
@property
def dsc(self):
return ptr_or_none(self._wv.safe_field("dsc"))
return ptr_or_none(self._wv_lv_animimg_t.safe_field("dsc"))
@property
def pic_count(self):
return int(self._wv.safe_field("pic_count", 0))
return int(self._wv_lv_animimg_t.safe_field("pic_count", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+18 -17
View File
@@ -13,80 +13,81 @@ class LVArc(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_arc_t", ptr=True) or self
self._wv_lv_arc_t = self.cast("lv_arc_t", ptr=True) or self
self._wv = self._wv_lv_arc_t
@property
def rotation(self):
return int(self._wv.safe_field("rotation", 0))
return int(self._wv_lv_arc_t.safe_field("rotation", 0))
@property
def indic_angle_start(self):
return int(self._wv.safe_field("indic_angle_start", 0))
return int(self._wv_lv_arc_t.safe_field("indic_angle_start", 0))
@property
def indic_angle_end(self):
return int(self._wv.safe_field("indic_angle_end", 0))
return int(self._wv_lv_arc_t.safe_field("indic_angle_end", 0))
@property
def bg_angle_start(self):
return int(self._wv.safe_field("bg_angle_start", 0))
return int(self._wv_lv_arc_t.safe_field("bg_angle_start", 0))
@property
def bg_angle_end(self):
return int(self._wv.safe_field("bg_angle_end", 0))
return int(self._wv_lv_arc_t.safe_field("bg_angle_end", 0))
@property
def value(self):
"""Current value of the arc"""
return int(self._wv.safe_field("value", 0))
return int(self._wv_lv_arc_t.safe_field("value", 0))
@property
def min_value(self):
"""Minimum value of the arc"""
return int(self._wv.safe_field("min_value", 0))
return int(self._wv_lv_arc_t.safe_field("min_value", 0))
@property
def max_value(self):
"""Maximum value of the arc"""
return int(self._wv.safe_field("max_value", 0))
return int(self._wv_lv_arc_t.safe_field("max_value", 0))
@property
def dragging(self):
return int(self._wv.safe_field("dragging", 0))
return int(self._wv_lv_arc_t.safe_field("dragging", 0))
@property
def type(self):
return int(self._wv.safe_field("type", 0))
return int(self._wv_lv_arc_t.safe_field("type", 0))
@property
def min_close(self):
"""1: the last pressed angle was closer to minimum end"""
return int(self._wv.safe_field("min_close", 0))
return int(self._wv_lv_arc_t.safe_field("min_close", 0))
@property
def in_out(self):
"""1: The click was within the background arc angles. 0: Click outside"""
return int(self._wv.safe_field("in_out", 0))
return int(self._wv_lv_arc_t.safe_field("in_out", 0))
@property
def chg_rate(self):
"""Drag angle rate of change of the arc (degrees/sec)"""
return int(self._wv.safe_field("chg_rate", 0))
return int(self._wv_lv_arc_t.safe_field("chg_rate", 0))
@property
def last_tick(self):
"""Last dragging event timestamp of the arc"""
return int(self._wv.safe_field("last_tick", 0))
return int(self._wv_lv_arc_t.safe_field("last_tick", 0))
@property
def last_angle(self):
"""Last dragging angle of the arc"""
return int(self._wv.safe_field("last_angle", 0))
return int(self._wv_lv_arc_t.safe_field("last_angle", 0))
@property
def knob_offset(self):
"""knob offset from the main arc"""
return int(self._wv.safe_field("knob_offset", 0))
return int(self._wv_lv_arc_t.safe_field("knob_offset", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+16 -15
View File
@@ -14,70 +14,71 @@ class LVArclabel(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_arclabel_t", ptr=True) or self
self._wv_lv_arclabel_t = self.cast("lv_arclabel_t", ptr=True) or self
self._wv = self._wv_lv_arclabel_t
@property
def text(self):
return safe_string(self._wv, "text")
return safe_string(self._wv_lv_arclabel_t, "text")
@property
def dot_begin(self):
"""Offset where bytes have been replaced with dots"""
return int(self._wv.safe_field("dot_begin", 0))
return int(self._wv_lv_arclabel_t.safe_field("dot_begin", 0))
@property
def angle_start(self):
return int(self._wv.safe_field("angle_start", 0))
return int(self._wv_lv_arclabel_t.safe_field("angle_start", 0))
@property
def angle_size(self):
return int(self._wv.safe_field("angle_size", 0))
return int(self._wv_lv_arclabel_t.safe_field("angle_size", 0))
@property
def offset(self):
return int(self._wv.safe_field("offset", 0))
return int(self._wv_lv_arclabel_t.safe_field("offset", 0))
@property
def radius(self):
return int(self._wv.safe_field("radius", 0))
return int(self._wv_lv_arclabel_t.safe_field("radius", 0))
@property
def center_offset(self):
return safe_point(self._wv, "center_offset")
return safe_point(self._wv_lv_arclabel_t, "center_offset")
@property
def dir(self):
return int(self._wv.safe_field("dir", 0))
return int(self._wv_lv_arclabel_t.safe_field("dir", 0))
@property
def text_align_v(self):
"""Vertical text alignment"""
return int(self._wv.safe_field("text_align_v", 0))
return int(self._wv_lv_arclabel_t.safe_field("text_align_v", 0))
@property
def text_align_h(self):
"""Horizontal text alignment"""
return int(self._wv.safe_field("text_align_h", 0))
return int(self._wv_lv_arclabel_t.safe_field("text_align_h", 0))
@property
def static_txt(self):
"""Flag to indicate the text is static"""
return int(self._wv.safe_field("static_txt", 0))
return int(self._wv_lv_arclabel_t.safe_field("static_txt", 0))
@property
def recolor(self):
"""Enable in-line letter re-coloring"""
return int(self._wv.safe_field("recolor", 0))
return int(self._wv_lv_arclabel_t.safe_field("recolor", 0))
@property
def overflow(self):
"""Overflow mode: 0=visible, 1=ellipsis, 2=clip"""
return int(self._wv.safe_field("overflow", 0))
return int(self._wv_lv_arclabel_t.safe_field("overflow", 0))
@property
def end_overlap(self):
"""End overlap flag, false if prevent end overlap"""
return int(self._wv.safe_field("end_overlap", 0))
return int(self._wv_lv_arclabel_t.safe_field("end_overlap", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+10 -9
View File
@@ -14,47 +14,48 @@ class LVBar(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_bar_t", ptr=True) or self
self._wv_lv_bar_t = self.cast("lv_bar_t", ptr=True) or self
self._wv = self._wv_lv_bar_t
@property
def cur_value(self):
"""Current value of the bar"""
return int(self._wv.safe_field("cur_value", 0))
return int(self._wv_lv_bar_t.safe_field("cur_value", 0))
@property
def min_value(self):
"""Minimum value of the bar"""
return int(self._wv.safe_field("min_value", 0))
return int(self._wv_lv_bar_t.safe_field("min_value", 0))
@property
def max_value(self):
"""Maximum value of the bar"""
return int(self._wv.safe_field("max_value", 0))
return int(self._wv_lv_bar_t.safe_field("max_value", 0))
@property
def start_value(self):
"""Start value of the bar"""
return int(self._wv.safe_field("start_value", 0))
return int(self._wv_lv_bar_t.safe_field("start_value", 0))
@property
def indic_area(self):
"""Save the indicator area. Might be used by derived types"""
return safe_area(self._wv, "indic_area")
return safe_area(self._wv_lv_bar_t, "indic_area")
@property
def val_reversed(self):
"""Whether value been reversed"""
return int(self._wv.safe_field("val_reversed", 0))
return int(self._wv_lv_bar_t.safe_field("val_reversed", 0))
@property
def mode(self):
"""Type of bar"""
return int(self._wv.safe_field("mode", 0))
return int(self._wv_lv_bar_t.safe_field("mode", 0))
@property
def orientation(self):
"""Orientation of bar"""
return int(self._wv.safe_field("orientation", 0))
return int(self._wv_lv_bar_t.safe_field("orientation", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,31 +14,32 @@ class LVBarcode(LVCanvas):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_barcode_t", ptr=True) or self
self._wv_lv_barcode_t = self.cast("lv_barcode_t", ptr=True) or self
self._wv = self._wv_lv_barcode_t
@property
def dark_color(self):
return safe_color(self._wv, "dark_color")
return safe_color(self._wv_lv_barcode_t, "dark_color")
@property
def light_color(self):
return safe_color(self._wv, "light_color")
return safe_color(self._wv_lv_barcode_t, "light_color")
@property
def scale(self):
return int(self._wv.safe_field("scale", 0))
return int(self._wv_lv_barcode_t.safe_field("scale", 0))
@property
def direction(self):
return int(self._wv.safe_field("direction", 0))
return int(self._wv_lv_barcode_t.safe_field("direction", 0))
@property
def tiled(self):
return int(self._wv.safe_field("tiled", 0))
return int(self._wv_lv_barcode_t.safe_field("tiled", 0))
@property
def encoding(self):
return int(self._wv.safe_field("encoding", 0))
return int(self._wv_lv_barcode_t.safe_field("encoding", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -13,7 +13,8 @@ class LVButton(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_button_t", ptr=True) or self
self._wv_lv_button_t = self.cast("lv_button_t", ptr=True) or self
self._wv = self._wv_lv_button_t
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,47 +14,48 @@ class LVButtonmatrix(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_buttonmatrix_t", ptr=True) or self
self._wv_lv_buttonmatrix_t = self.cast("lv_buttonmatrix_t", ptr=True) or self
self._wv = self._wv_lv_buttonmatrix_t
@property
def map_p(self):
"""Pointer to the current map"""
return ptr_or_none(self._wv.safe_field("map_p"))
return ptr_or_none(self._wv_lv_buttonmatrix_t.safe_field("map_p"))
@property
def button_areas(self):
"""Array of areas of buttons"""
return ptr_or_none(self._wv.safe_field("button_areas"))
return ptr_or_none(self._wv_lv_buttonmatrix_t.safe_field("button_areas"))
@property
def ctrl_bits(self):
"""Array of control bytes"""
return ptr_or_none(self._wv.safe_field("ctrl_bits"))
return ptr_or_none(self._wv_lv_buttonmatrix_t.safe_field("ctrl_bits"))
@property
def btn_cnt(self):
"""Number of button in 'map_p'(Handled by the library)"""
return int(self._wv.safe_field("btn_cnt", 0))
return int(self._wv_lv_buttonmatrix_t.safe_field("btn_cnt", 0))
@property
def row_cnt(self):
"""Number of rows in 'map_p'(Handled by the library)"""
return int(self._wv.safe_field("row_cnt", 0))
return int(self._wv_lv_buttonmatrix_t.safe_field("row_cnt", 0))
@property
def btn_id_sel(self):
"""Index of the active button (being pressed/released etc) or LV_BUTTONMATRIX_BUTTON_NONE"""
return int(self._wv.safe_field("btn_id_sel", 0))
return int(self._wv_lv_buttonmatrix_t.safe_field("btn_id_sel", 0))
@property
def one_check(self):
"""1: Single button toggled at once"""
return int(self._wv.safe_field("one_check", 0))
return int(self._wv_lv_buttonmatrix_t.safe_field("one_check", 0))
@property
def auto_free_map(self):
"""1: Automatically free the map when the widget is deleted"""
return int(self._wv.safe_field("auto_free_map", 0))
return int(self._wv_lv_buttonmatrix_t.safe_field("auto_free_map", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,25 +14,26 @@ class LVCalendar(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_calendar_t", ptr=True) or self
self._wv_lv_calendar_t = self.cast("lv_calendar_t", ptr=True) or self
self._wv = self._wv_lv_calendar_t
@property
def btnm(self):
return ptr_or_none(self._wv.safe_field("btnm"))
return ptr_or_none(self._wv_lv_calendar_t.safe_field("btnm"))
@property
def highlighted_dates(self):
"""Apply different style on these days (pointer to user-defined array)"""
return ptr_or_none(self._wv.safe_field("highlighted_dates"))
return ptr_or_none(self._wv_lv_calendar_t.safe_field("highlighted_dates"))
@property
def highlighted_dates_num(self):
"""Number of elements in `highlighted_days`"""
return int(self._wv.safe_field("highlighted_dates_num", 0))
return int(self._wv_lv_calendar_t.safe_field("highlighted_dates_num", 0))
@property
def use_chinese_calendar(self):
return int(self._wv.safe_field("use_chinese_calendar", 0))
return int(self._wv_lv_calendar_t.safe_field("use_chinese_calendar", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,15 +14,16 @@ class LVCanvas(LVImage):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_canvas_t", ptr=True) or self
self._wv_lv_canvas_t = self.cast("lv_canvas_t", ptr=True) or self
self._wv = self._wv_lv_canvas_t
@property
def draw_buf(self):
return ptr_or_none(self._wv.safe_field("draw_buf"))
return ptr_or_none(self._wv_lv_canvas_t.safe_field("draw_buf"))
@property
def static_buf(self):
return safe_wrapper(self._wv, "static_buf", "lvglgdb.lvgl.draw.lv_draw_buf", "LVDrawBuf")
return safe_wrapper(self._wv_lv_canvas_t, "static_buf", "lvglgdb.lvgl.draw.lv_draw_buf", "LVDrawBuf")
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+10 -9
View File
@@ -14,45 +14,46 @@ class LVChart(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_chart_t", ptr=True) or self
self._wv_lv_chart_t = self.cast("lv_chart_t", ptr=True) or self
self._wv = self._wv_lv_chart_t
@property
def series_ll(self):
"""Linked list for series (stores lv_chart_series_t)"""
return safe_wrapper(self._wv, "series_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
return safe_wrapper(self._wv_lv_chart_t, "series_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def cursor_ll(self):
"""Linked list for cursors (stores lv_chart_cursor_t)"""
return safe_wrapper(self._wv, "cursor_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
return safe_wrapper(self._wv_lv_chart_t, "cursor_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def pressed_point_id(self):
return int(self._wv.safe_field("pressed_point_id", 0))
return int(self._wv_lv_chart_t.safe_field("pressed_point_id", 0))
@property
def hdiv_cnt(self):
"""Number of horizontal division lines"""
return int(self._wv.safe_field("hdiv_cnt", 0))
return int(self._wv_lv_chart_t.safe_field("hdiv_cnt", 0))
@property
def vdiv_cnt(self):
"""Number of vertical division lines"""
return int(self._wv.safe_field("vdiv_cnt", 0))
return int(self._wv_lv_chart_t.safe_field("vdiv_cnt", 0))
@property
def point_cnt(self):
"""Number of points in all series"""
return int(self._wv.safe_field("point_cnt", 0))
return int(self._wv_lv_chart_t.safe_field("point_cnt", 0))
@property
def type(self):
"""Chart type"""
return int(self._wv.safe_field("type", 0))
return int(self._wv_lv_chart_t.safe_field("type", 0))
@property
def update_mode(self):
return int(self._wv.safe_field("update_mode", 0))
return int(self._wv_lv_chart_t.safe_field("update_mode", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,15 +14,16 @@ class LVCheckbox(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_checkbox_t", ptr=True) or self
self._wv_lv_checkbox_t = self.cast("lv_checkbox_t", ptr=True) or self
self._wv = self._wv_lv_checkbox_t
@property
def txt(self):
return safe_string(self._wv, "txt")
return safe_string(self._wv_lv_checkbox_t, "txt")
@property
def static_txt(self):
return int(self._wv.safe_field("static_txt", 0))
return int(self._wv_lv_checkbox_t.safe_field("static_txt", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+14 -13
View File
@@ -14,67 +14,68 @@ class LVDropdown(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_dropdown_t", ptr=True) or self
self._wv_lv_dropdown_t = self.cast("lv_dropdown_t", ptr=True) or self
self._wv = self._wv_lv_dropdown_t
@property
def list(self):
"""The dropped down list"""
return ptr_or_none(self._wv.safe_field("list"))
return ptr_or_none(self._wv_lv_dropdown_t.safe_field("list"))
@property
def text(self):
"""Text to display on the dropdown's button"""
return safe_string(self._wv, "text")
return safe_string(self._wv_lv_dropdown_t, "text")
@property
def symbol(self):
"""Arrow or other icon when the drop-down list is closed"""
return ptr_or_none(self._wv.safe_field("symbol"))
return ptr_or_none(self._wv_lv_dropdown_t.safe_field("symbol"))
@property
def options(self):
"""Options in a '\n' separated list"""
return safe_string(self._wv, "options")
return safe_string(self._wv_lv_dropdown_t, "options")
@property
def option_cnt(self):
"""Number of options"""
return int(self._wv.safe_field("option_cnt", 0))
return int(self._wv_lv_dropdown_t.safe_field("option_cnt", 0))
@property
def sel_opt_id(self):
"""Index of the currently selected option"""
return int(self._wv.safe_field("sel_opt_id", 0))
return int(self._wv_lv_dropdown_t.safe_field("sel_opt_id", 0))
@property
def sel_opt_id_orig(self):
"""Store the original index on focus"""
return int(self._wv.safe_field("sel_opt_id_orig", 0))
return int(self._wv_lv_dropdown_t.safe_field("sel_opt_id_orig", 0))
@property
def pr_opt_id(self):
"""Index of the currently pressed option"""
return int(self._wv.safe_field("pr_opt_id", 0))
return int(self._wv_lv_dropdown_t.safe_field("pr_opt_id", 0))
@property
def dir(self):
"""Direction in which the list should open"""
return int(self._wv.safe_field("dir", 0))
return int(self._wv_lv_dropdown_t.safe_field("dir", 0))
@property
def static_options(self):
"""1: Only a pointer is saved in `options`"""
return int(self._wv.safe_field("static_options", 0))
return int(self._wv_lv_dropdown_t.safe_field("static_options", 0))
@property
def selected_highlight(self):
"""1: Make the selected option highlighted in the list"""
return int(self._wv.safe_field("selected_highlight", 0))
return int(self._wv_lv_dropdown_t.safe_field("selected_highlight", 0))
@property
def static_text(self):
"""1: Only a pointer is saved in `text`"""
return int(self._wv.safe_field("static_text", 0))
return int(self._wv_lv_dropdown_t.safe_field("static_text", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,11 +14,12 @@ class LVDropdownList(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_dropdown_list_t", ptr=True) or self
self._wv_lv_dropdown_list_t = self.cast("lv_dropdown_list_t", ptr=True) or self
self._wv = self._wv_lv_dropdown_list_t
@property
def dropdown(self):
return ptr_or_none(self._wv.safe_field("dropdown"))
return ptr_or_none(self._wv_lv_dropdown_list_t.safe_field("dropdown"))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,19 +14,20 @@ class LVFfmpegPlayer(LVImage):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_ffmpeg_player_t", ptr=True) or self
self._wv_lv_ffmpeg_player_t = self.cast("lv_ffmpeg_player_t", ptr=True) or self
self._wv = self._wv_lv_ffmpeg_player_t
@property
def timer(self):
return ptr_or_none(self._wv.safe_field("timer"))
return ptr_or_none(self._wv_lv_ffmpeg_player_t.safe_field("timer"))
@property
def auto_restart(self):
return int(self._wv.safe_field("auto_restart", 0))
return int(self._wv_lv_ffmpeg_player_t.safe_field("auto_restart", 0))
@property
def decoder_name(self):
return safe_string(self._wv, "decoder_name")
return safe_string(self._wv_lv_ffmpeg_player_t, "decoder_name")
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,43 +14,44 @@ class LVGstreamer(LVImage):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_gstreamer_t", ptr=True) or self
self._wv_lv_gstreamer_t = self.cast("lv_gstreamer_t", ptr=True) or self
self._wv = self._wv_lv_gstreamer_t
@property
def last_buffer(self):
return ptr_or_none(self._wv.safe_field("last_buffer"))
return ptr_or_none(self._wv_lv_gstreamer_t.safe_field("last_buffer"))
@property
def last_sample(self):
return ptr_or_none(self._wv.safe_field("last_sample"))
return ptr_or_none(self._wv_lv_gstreamer_t.safe_field("last_sample"))
@property
def pipeline(self):
return ptr_or_none(self._wv.safe_field("pipeline"))
return ptr_or_none(self._wv_lv_gstreamer_t.safe_field("pipeline"))
@property
def audio_convert(self):
return ptr_or_none(self._wv.safe_field("audio_convert"))
return ptr_or_none(self._wv_lv_gstreamer_t.safe_field("audio_convert"))
@property
def video_convert(self):
return ptr_or_none(self._wv.safe_field("video_convert"))
return ptr_or_none(self._wv_lv_gstreamer_t.safe_field("video_convert"))
@property
def audio_volume(self):
return ptr_or_none(self._wv.safe_field("audio_volume"))
return ptr_or_none(self._wv_lv_gstreamer_t.safe_field("audio_volume"))
@property
def gstreamer_timer(self):
return ptr_or_none(self._wv.safe_field("gstreamer_timer"))
return ptr_or_none(self._wv_lv_gstreamer_t.safe_field("gstreamer_timer"))
@property
def frame_queue(self):
return ptr_or_none(self._wv.safe_field("frame_queue"))
return ptr_or_none(self._wv_lv_gstreamer_t.safe_field("frame_queue"))
@property
def is_video_info_valid(self):
return int(self._wv.safe_field("is_video_info_valid", 0))
return int(self._wv_lv_gstreamer_t.safe_field("is_video_info_valid", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+16 -15
View File
@@ -14,76 +14,77 @@ class LVImage(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_image_t", ptr=True) or self
self._wv_lv_image_t = self.cast("lv_image_t", ptr=True) or self
self._wv = self._wv_lv_image_t
@property
def src(self):
"""Image source: Pointer to an array or a file or a symbol"""
return ptr_or_none(self._wv.safe_field("src"))
return ptr_or_none(self._wv_lv_image_t.safe_field("src"))
@property
def bitmap_mask_src(self):
"""Pointer to an A8 bitmap mask"""
return ptr_or_none(self._wv.safe_field("bitmap_mask_src"))
return ptr_or_none(self._wv_lv_image_t.safe_field("bitmap_mask_src"))
@property
def offset(self):
return safe_point(self._wv, "offset")
return safe_point(self._wv_lv_image_t, "offset")
@property
def w(self):
"""Width of the image (Handled by the library)"""
return int(self._wv.safe_field("w", 0))
return int(self._wv_lv_image_t.safe_field("w", 0))
@property
def h(self):
"""Height of the image (Handled by the library)"""
return int(self._wv.safe_field("h", 0))
return int(self._wv_lv_image_t.safe_field("h", 0))
@property
def rotation(self):
"""Rotation angle of the image"""
return int(self._wv.safe_field("rotation", 0))
return int(self._wv_lv_image_t.safe_field("rotation", 0))
@property
def scale_x(self):
"""256 means no zoom, 512 double size, 128 half size"""
return int(self._wv.safe_field("scale_x", 0))
return int(self._wv_lv_image_t.safe_field("scale_x", 0))
@property
def scale_y(self):
"""256 means no zoom, 512 double size, 128 half size"""
return int(self._wv.safe_field("scale_y", 0))
return int(self._wv_lv_image_t.safe_field("scale_y", 0))
@property
def pivot(self):
"""Rotation center of the image"""
return safe_point(self._wv, "pivot")
return safe_point(self._wv_lv_image_t, "pivot")
@property
def src_type(self):
"""See: lv_image_src_t"""
return int(self._wv.safe_field("src_type", 0))
return int(self._wv_lv_image_t.safe_field("src_type", 0))
@property
def cf(self):
"""Color format from `lv_color_format_t`"""
return int(self._wv.safe_field("cf", 0))
return int(self._wv_lv_image_t.safe_field("cf", 0))
@property
def antialias(self):
"""Apply anti-aliasing in transformations (rotate, zoom)"""
return int(self._wv.safe_field("antialias", 0))
return int(self._wv_lv_image_t.safe_field("antialias", 0))
@property
def align(self):
"""Image size mode when image size and object size is different. See lv_image_align_t"""
return int(self._wv.safe_field("align", 0))
return int(self._wv_lv_image_t.safe_field("align", 0))
@property
def blend_mode(self):
"""Element of `lv_blend_mode_t`"""
return int(self._wv.safe_field("blend_mode", 0))
return int(self._wv_lv_image_t.safe_field("blend_mode", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -13,7 +13,8 @@ class LVImagebutton(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_imagebutton_t", ptr=True) or self
self._wv_lv_imagebutton_t = self.cast("lv_imagebutton_t", ptr=True) or self
self._wv = self._wv_lv_imagebutton_t
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,63 +14,64 @@ class LVImePinyin(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_ime_pinyin_t", ptr=True) or self
self._wv_lv_ime_pinyin_t = self.cast("lv_ime_pinyin_t", ptr=True) or self
self._wv = self._wv_lv_ime_pinyin_t
@property
def kb(self):
return ptr_or_none(self._wv.safe_field("kb"))
return ptr_or_none(self._wv_lv_ime_pinyin_t.safe_field("kb"))
@property
def cand_panel(self):
return ptr_or_none(self._wv.safe_field("cand_panel"))
return ptr_or_none(self._wv_lv_ime_pinyin_t.safe_field("cand_panel"))
@property
def dict(self):
return ptr_or_none(self._wv.safe_field("dict"))
return ptr_or_none(self._wv_lv_ime_pinyin_t.safe_field("dict"))
@property
def k9_legal_py_ll(self):
return safe_wrapper(self._wv, "k9_legal_py_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
return safe_wrapper(self._wv_lv_ime_pinyin_t, "k9_legal_py_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def cand_str(self):
"""Candidate string"""
return safe_string(self._wv, "cand_str")
return safe_string(self._wv_lv_ime_pinyin_t, "cand_str")
@property
def k9_py_ll_pos(self):
"""Current pinyin map pages(k9)"""
return int(self._wv.safe_field("k9_py_ll_pos", 0))
return int(self._wv_lv_ime_pinyin_t.safe_field("k9_py_ll_pos", 0))
@property
def k9_legal_py_count(self):
"""Count of legal Pinyin numbers(k9)"""
return int(self._wv.safe_field("k9_legal_py_count", 0))
return int(self._wv_lv_ime_pinyin_t.safe_field("k9_legal_py_count", 0))
@property
def k9_input_str_len(self):
"""9-key input(k9) mode input string max len"""
return int(self._wv.safe_field("k9_input_str_len", 0))
return int(self._wv_lv_ime_pinyin_t.safe_field("k9_input_str_len", 0))
@property
def ta_count(self):
"""The number of characters entered in the text box this time"""
return int(self._wv.safe_field("ta_count", 0))
return int(self._wv_lv_ime_pinyin_t.safe_field("ta_count", 0))
@property
def cand_num(self):
"""Number of candidates"""
return int(self._wv.safe_field("cand_num", 0))
return int(self._wv_lv_ime_pinyin_t.safe_field("cand_num", 0))
@property
def py_page(self):
"""Current pinyin map pages(k26)"""
return int(self._wv.safe_field("py_page", 0))
return int(self._wv_lv_ime_pinyin_t.safe_field("py_page", 0))
@property
def mode(self):
"""Set mode, 1: 26-key input(k26), 0: 9-key input(k9). Default: 1."""
return int(self._wv.safe_field("mode", 0))
return int(self._wv_lv_ime_pinyin_t.safe_field("mode", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,22 +14,23 @@ class LVKeyboard(LVButtonmatrix):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_keyboard_t", ptr=True) or self
self._wv_lv_keyboard_t = self.cast("lv_keyboard_t", ptr=True) or self
self._wv = self._wv_lv_keyboard_t
@property
def ta(self):
"""Pointer to the assigned text area"""
return ptr_or_none(self._wv.safe_field("ta"))
return ptr_or_none(self._wv_lv_keyboard_t.safe_field("ta"))
@property
def mode(self):
"""Key map type"""
return int(self._wv.safe_field("mode", 0))
return int(self._wv_lv_keyboard_t.safe_field("mode", 0))
@property
def popovers(self):
"""Show button titles in popovers on press"""
return int(self._wv.safe_field("popovers", 0))
return int(self._wv_lv_keyboard_t.safe_field("popovers", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+17 -16
View File
@@ -14,76 +14,77 @@ class LVLabel(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_label_t", ptr=True) or self
self._wv_lv_label_t = self.cast("lv_label_t", ptr=True) or self
self._wv = self._wv_lv_label_t
@property
def text(self):
return safe_string(self._wv, "text")
return safe_string(self._wv_lv_label_t, "text")
@property
def translation_tag(self):
return safe_string(self._wv, "translation_tag")
return safe_string(self._wv_lv_label_t, "translation_tag")
@property
def dot_begin(self):
"""Offset where bytes have been replaced with dots"""
return int(self._wv.safe_field("dot_begin", 0))
return int(self._wv_lv_label_t.safe_field("dot_begin", 0))
@property
def max_lines(self):
return int(self._wv.safe_field("max_lines", 0))
return int(self._wv_lv_label_t.safe_field("max_lines", 0))
@property
def sel_start(self):
return int(self._wv.safe_field("sel_start", 0))
return int(self._wv_lv_label_t.safe_field("sel_start", 0))
@property
def sel_end(self):
return int(self._wv.safe_field("sel_end", 0))
return int(self._wv_lv_label_t.safe_field("sel_end", 0))
@property
def size_cache(self):
"""Text size cache"""
return safe_point(self._wv, "size_cache")
return safe_point(self._wv_lv_label_t, "size_cache")
@property
def offset(self):
"""Text draw position offset"""
return safe_point(self._wv, "offset")
return safe_point(self._wv_lv_label_t, "offset")
@property
def long_mode(self):
"""Determine what to do with the long texts"""
return int(self._wv.safe_field("long_mode", 0))
return int(self._wv_lv_label_t.safe_field("long_mode", 0))
@property
def static_txt(self):
"""Flag to indicate the text is static"""
return int(self._wv.safe_field("static_txt", 0))
return int(self._wv_lv_label_t.safe_field("static_txt", 0))
@property
def recolor(self):
"""Enable in-line letter re-coloring"""
return int(self._wv.safe_field("recolor", 0))
return int(self._wv_lv_label_t.safe_field("recolor", 0))
@property
def expand(self):
"""Ignore real width (used by the library with LV_LABEL_LONG_MODE_SCROLL)"""
return int(self._wv.safe_field("expand", 0))
return int(self._wv_lv_label_t.safe_field("expand", 0))
@property
def invalid_size_cache(self):
"""1: Recalculate size and update cache"""
return int(self._wv.safe_field("invalid_size_cache", 0))
return int(self._wv_lv_label_t.safe_field("invalid_size_cache", 0))
@property
def need_refr_text(self):
"""1: Refresh text after layout update completion"""
return int(self._wv.safe_field("need_refr_text", 0))
return int(self._wv_lv_label_t.safe_field("need_refr_text", 0))
@property
def text_size(self):
return safe_point(self._wv, "text_size")
return safe_point(self._wv_lv_label_t, "text_size")
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+4 -3
View File
@@ -14,16 +14,17 @@ class LVLed(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_led_t", ptr=True) or self
self._wv_lv_led_t = self.cast("lv_led_t", ptr=True) or self
self._wv = self._wv_lv_led_t
@property
def color(self):
return safe_color(self._wv, "color")
return safe_color(self._wv_lv_led_t, "color")
@property
def bright(self):
"""Current brightness of the LED (0..255)"""
return int(self._wv.safe_field("bright", 0))
return int(self._wv_lv_led_t.safe_field("bright", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+5 -4
View File
@@ -13,22 +13,23 @@ class LVLine(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_line_t", ptr=True) or self
self._wv_lv_line_t = self.cast("lv_line_t", ptr=True) or self
self._wv = self._wv_lv_line_t
@property
def point_num(self):
"""Number of points in 'point_array'"""
return int(self._wv.safe_field("point_num", 0))
return int(self._wv_lv_line_t.safe_field("point_num", 0))
@property
def y_inv(self):
"""1: y == 0 will be on the bottom"""
return int(self._wv.safe_field("y_inv", 0))
return int(self._wv_lv_line_t.safe_field("y_inv", 0))
@property
def point_array_is_mutable(self):
"""whether the point array is const or mutable"""
return int(self._wv.safe_field("point_array_is_mutable", 0))
return int(self._wv_lv_line_t.safe_field("point_array_is_mutable", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+18 -17
View File
@@ -14,72 +14,73 @@ class LVMenu(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_menu_t", ptr=True) or self
self._wv_lv_menu_t = self.cast("lv_menu_t", ptr=True) or self
self._wv = self._wv_lv_menu_t
@property
def storage(self):
"""a pointer to obj that is the parent of all pages not displayed"""
return ptr_or_none(self._wv.safe_field("storage"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("storage"))
@property
def main(self):
return ptr_or_none(self._wv.safe_field("main"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("main"))
@property
def main_page(self):
return ptr_or_none(self._wv.safe_field("main_page"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("main_page"))
@property
def main_header(self):
return ptr_or_none(self._wv.safe_field("main_header"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("main_header"))
@property
def main_header_title(self):
return ptr_or_none(self._wv.safe_field("main_header_title"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("main_header_title"))
@property
def sidebar(self):
return ptr_or_none(self._wv.safe_field("sidebar"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("sidebar"))
@property
def sidebar_page(self):
return ptr_or_none(self._wv.safe_field("sidebar_page"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("sidebar_page"))
@property
def sidebar_header(self):
return ptr_or_none(self._wv.safe_field("sidebar_header"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("sidebar_header"))
@property
def sidebar_header_title(self):
return ptr_or_none(self._wv.safe_field("sidebar_header_title"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("sidebar_header_title"))
@property
def selected_tab(self):
return ptr_or_none(self._wv.safe_field("selected_tab"))
return ptr_or_none(self._wv_lv_menu_t.safe_field("selected_tab"))
@property
def history_ll(self):
return safe_wrapper(self._wv, "history_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
return safe_wrapper(self._wv_lv_menu_t, "history_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def cur_depth(self):
return int(self._wv.safe_field("cur_depth", 0))
return int(self._wv_lv_menu_t.safe_field("cur_depth", 0))
@property
def prev_depth(self):
return int(self._wv.safe_field("prev_depth", 0))
return int(self._wv_lv_menu_t.safe_field("prev_depth", 0))
@property
def sidebar_generated(self):
return int(self._wv.safe_field("sidebar_generated", 0))
return int(self._wv_lv_menu_t.safe_field("sidebar_generated", 0))
@property
def mode_header(self):
return int(self._wv.safe_field("mode_header", 0))
return int(self._wv_lv_menu_t.safe_field("mode_header", 0))
@property
def mode_root_back_btn(self):
return int(self._wv.safe_field("mode_root_back_btn", 0))
return int(self._wv_lv_menu_t.safe_field("mode_root_back_btn", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,15 +14,16 @@ class LVMenuPage(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_menu_page_t", ptr=True) or self
self._wv_lv_menu_page_t = self.cast("lv_menu_page_t", ptr=True) or self
self._wv = self._wv_lv_menu_page_t
@property
def title(self):
return safe_string(self._wv, "title")
return safe_string(self._wv_lv_menu_page_t, "title")
@property
def static_title(self):
return int(self._wv.safe_field("static_title", 0))
return int(self._wv_lv_menu_page_t.safe_field("static_title", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,28 +14,29 @@ class LVMsgbox(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_msgbox_t", ptr=True) or self
self._wv_lv_msgbox_t = self.cast("lv_msgbox_t", ptr=True) or self
self._wv = self._wv_lv_msgbox_t
@property
def header(self):
return ptr_or_none(self._wv.safe_field("header"))
return ptr_or_none(self._wv_lv_msgbox_t.safe_field("header"))
@property
def content(self):
return ptr_or_none(self._wv.safe_field("content"))
return ptr_or_none(self._wv_lv_msgbox_t.safe_field("content"))
@property
def footer(self):
return ptr_or_none(self._wv.safe_field("footer"))
return ptr_or_none(self._wv_lv_msgbox_t.safe_field("footer"))
@property
def title(self):
return ptr_or_none(self._wv.safe_field("title"))
return ptr_or_none(self._wv_lv_msgbox_t.safe_field("title"))
@property
def auto_parent(self):
"""Mark that the parent was automatically created"""
return int(self._wv.safe_field("auto_parent", 0))
return int(self._wv_lv_msgbox_t.safe_field("auto_parent", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+10 -9
View File
@@ -14,45 +14,46 @@ class LVQrcode(LVCanvas):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_qrcode_t", ptr=True) or self
self._wv_lv_qrcode_t = self.cast("lv_qrcode_t", ptr=True) or self
self._wv = self._wv_lv_qrcode_t
@property
def dark_color(self):
return safe_color(self._wv, "dark_color")
return safe_color(self._wv_lv_qrcode_t, "dark_color")
@property
def light_color(self):
return safe_color(self._wv, "light_color")
return safe_color(self._wv_lv_qrcode_t, "light_color")
@property
def data(self):
"""Copy of the payload, kept so the bitmap can be re-encoded on a property change"""
return ptr_or_none(self._wv.safe_field("data"))
return ptr_or_none(self._wv_lv_qrcode_t.safe_field("data"))
@property
def data_len(self):
"""Stored payload length in bytes"""
return int(self._wv.safe_field("data_len", 0))
return int(self._wv_lv_qrcode_t.safe_field("data_len", 0))
@property
def quiet_zone(self):
"""Add the QR spec's blank margin around the code (boolean toggle)"""
return int(self._wv.safe_field("quiet_zone", 0))
return int(self._wv_lv_qrcode_t.safe_field("quiet_zone", 0))
@property
def update_mode(self):
"""lv_qrcode_update_mode_t: when a property change is re-encoded"""
return int(self._wv.safe_field("update_mode", 0))
return int(self._wv_lv_qrcode_t.safe_field("update_mode", 0))
@property
def needs_update(self):
"""The bitmap is out of date; re-encoded on the next redraw (deferred mode)"""
return int(self._wv.safe_field("needs_update", 0))
return int(self._wv_lv_qrcode_t.safe_field("needs_update", 0))
@property
def render_failed(self):
"""The last encode attempt failed (or none has run yet)"""
return int(self._wv.safe_field("render_failed", 0))
return int(self._wv_lv_qrcode_t.safe_field("render_failed", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -13,35 +13,36 @@ class LVRoller(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_roller_t", ptr=True) or self
self._wv_lv_roller_t = self.cast("lv_roller_t", ptr=True) or self
self._wv = self._wv_lv_roller_t
@property
def option_cnt(self):
"""Number of options"""
return int(self._wv.safe_field("option_cnt", 0))
return int(self._wv_lv_roller_t.safe_field("option_cnt", 0))
@property
def sel_opt_id(self):
"""Index of the current option"""
return int(self._wv.safe_field("sel_opt_id", 0))
return int(self._wv_lv_roller_t.safe_field("sel_opt_id", 0))
@property
def sel_opt_id_ori(self):
"""Store the original index on focus"""
return int(self._wv.safe_field("sel_opt_id_ori", 0))
return int(self._wv_lv_roller_t.safe_field("sel_opt_id_ori", 0))
@property
def inf_page_cnt(self):
"""Number of extra pages added to make the roller look infinite"""
return int(self._wv.safe_field("inf_page_cnt", 0))
return int(self._wv_lv_roller_t.safe_field("inf_page_cnt", 0))
@property
def mode(self):
return int(self._wv.safe_field("mode", 0))
return int(self._wv_lv_roller_t.safe_field("mode", 0))
@property
def moved(self):
return int(self._wv.safe_field("moved", 0))
return int(self._wv_lv_roller_t.safe_field("moved", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+18 -17
View File
@@ -14,85 +14,86 @@ class LVScale(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_scale_t", ptr=True) or self
self._wv_lv_scale_t = self.cast("lv_scale_t", ptr=True) or self
self._wv = self._wv_lv_scale_t
@property
def section_ll(self):
"""Linked list for the sections (stores lv_scale_section_t)"""
return safe_wrapper(self._wv, "section_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
return safe_wrapper(self._wv_lv_scale_t, "section_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def txt_src(self):
return ptr_or_none(self._wv.safe_field("txt_src"))
return ptr_or_none(self._wv_lv_scale_t.safe_field("txt_src"))
@property
def mode(self):
"""Orientation and layout of scale."""
return int(self._wv.safe_field("mode", 0))
return int(self._wv_lv_scale_t.safe_field("mode", 0))
@property
def range_min(self):
"""Scale's minimum value"""
return int(self._wv.safe_field("range_min", 0))
return int(self._wv_lv_scale_t.safe_field("range_min", 0))
@property
def range_max(self):
"""Scale's maximum value"""
return int(self._wv.safe_field("range_max", 0))
return int(self._wv_lv_scale_t.safe_field("range_max", 0))
@property
def total_tick_count(self):
"""Total number of ticks (major and minor)"""
return int(self._wv.safe_field("total_tick_count", 0))
return int(self._wv_lv_scale_t.safe_field("total_tick_count", 0))
@property
def major_tick_every(self):
"""Frequency of major ticks to minor ticks"""
return int(self._wv.safe_field("major_tick_every", 0))
return int(self._wv_lv_scale_t.safe_field("major_tick_every", 0))
@property
def label_enabled(self):
"""Draw labels for major ticks?"""
return int(self._wv.safe_field("label_enabled", 0))
return int(self._wv_lv_scale_t.safe_field("label_enabled", 0))
@property
def post_draw(self):
return int(self._wv.safe_field("post_draw", 0))
return int(self._wv_lv_scale_t.safe_field("post_draw", 0))
@property
def draw_ticks_on_top(self):
"""Draw ticks on top of main line?"""
return int(self._wv.safe_field("draw_ticks_on_top", 0))
return int(self._wv_lv_scale_t.safe_field("draw_ticks_on_top", 0))
@property
def angle_range(self):
"""Degrees between low end and high end of scale"""
return int(self._wv.safe_field("angle_range", 0))
return int(self._wv_lv_scale_t.safe_field("angle_range", 0))
@property
def rotation(self):
"""Clockwise angular offset from 3-o'clock position of low end of scale"""
return int(self._wv.safe_field("rotation", 0))
return int(self._wv_lv_scale_t.safe_field("rotation", 0))
@property
def custom_label_cnt(self):
"""Number of custom labels provided in `txt_src`"""
return int(self._wv.safe_field("custom_label_cnt", 0))
return int(self._wv_lv_scale_t.safe_field("custom_label_cnt", 0))
@property
def last_tick_width(self):
"""Width of last tick in pixels"""
return int(self._wv.safe_field("last_tick_width", 0))
return int(self._wv_lv_scale_t.safe_field("last_tick_width", 0))
@property
def first_tick_width(self):
"""Width of first tick in pixels"""
return int(self._wv.safe_field("first_tick_width", 0))
return int(self._wv_lv_scale_t.safe_field("first_tick_width", 0))
@property
def needles(self):
"""Needle list of this scale"""
return safe_wrapper(self._wv, "needles", "lvglgdb.lvgl.misc.lv_array", "LVArray")
return safe_wrapper(self._wv_lv_scale_t, "needles", "lvglgdb.lvgl.misc.lv_array", "LVArray")
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,34 +14,35 @@ class LVSlider(LVBar):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_slider_t", ptr=True) or self
self._wv_lv_slider_t = self.cast("lv_slider_t", ptr=True) or self
self._wv = self._wv_lv_slider_t
@property
def left_knob_area(self):
return safe_area(self._wv, "left_knob_area")
return safe_area(self._wv_lv_slider_t, "left_knob_area")
@property
def right_knob_area(self):
return safe_area(self._wv, "right_knob_area")
return safe_area(self._wv_lv_slider_t, "right_knob_area")
@property
def pressed_point(self):
return safe_point(self._wv, "pressed_point")
return safe_point(self._wv_lv_slider_t, "pressed_point")
@property
def value_to_set(self):
"""Which bar value to set"""
return ptr_or_none(self._wv.safe_field("value_to_set"))
return ptr_or_none(self._wv_lv_slider_t.safe_field("value_to_set"))
@property
def dragging(self):
"""1: the slider is being dragged"""
return int(self._wv.safe_field("dragging", 0))
return int(self._wv_lv_slider_t.safe_field("dragging", 0))
@property
def left_knob_focus(self):
"""1: with encoder now the right knob can be adjusted"""
return int(self._wv.safe_field("left_knob_focus", 0))
return int(self._wv_lv_slider_t.safe_field("left_knob_focus", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,40 +14,41 @@ class LVSpangroup(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_spangroup_t", ptr=True) or self
self._wv_lv_spangroup_t = self.cast("lv_spangroup_t", ptr=True) or self
self._wv = self._wv_lv_spangroup_t
@property
def lines(self):
return int(self._wv.safe_field("lines", 0))
return int(self._wv_lv_spangroup_t.safe_field("lines", 0))
@property
def indent(self):
"""first line indent"""
return int(self._wv.safe_field("indent", 0))
return int(self._wv_lv_spangroup_t.safe_field("indent", 0))
@property
def cache_w(self):
"""the cache automatically calculates the width"""
return int(self._wv.safe_field("cache_w", 0))
return int(self._wv_lv_spangroup_t.safe_field("cache_w", 0))
@property
def cache_h(self):
"""similar cache_w"""
return int(self._wv.safe_field("cache_h", 0))
return int(self._wv_lv_spangroup_t.safe_field("cache_h", 0))
@property
def child_ll(self):
return safe_wrapper(self._wv, "child_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
return safe_wrapper(self._wv_lv_spangroup_t, "child_ll", "lvglgdb.lvgl.misc.lv_ll", "LVList")
@property
def overflow(self):
"""details see lv_span_overflow_t"""
return int(self._wv.safe_field("overflow", 0))
return int(self._wv_lv_spangroup_t.safe_field("overflow", 0))
@property
def refresh(self):
"""the spangroup need refresh cache_w and cache_h"""
return int(self._wv.safe_field("refresh", 0))
return int(self._wv_lv_spangroup_t.safe_field("refresh", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+10 -9
View File
@@ -13,42 +13,43 @@ class LVSpinbox(LVTextarea):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_spinbox_t", ptr=True) or self
self._wv_lv_spinbox_t = self.cast("lv_spinbox_t", ptr=True) or self
self._wv = self._wv_lv_spinbox_t
@property
def value(self):
return int(self._wv.safe_field("value", 0))
return int(self._wv_lv_spinbox_t.safe_field("value", 0))
@property
def range_max(self):
return int(self._wv.safe_field("range_max", 0))
return int(self._wv_lv_spinbox_t.safe_field("range_max", 0))
@property
def range_min(self):
return int(self._wv.safe_field("range_min", 0))
return int(self._wv_lv_spinbox_t.safe_field("range_min", 0))
@property
def step(self):
return int(self._wv.safe_field("step", 0))
return int(self._wv_lv_spinbox_t.safe_field("step", 0))
@property
def digit_count(self):
return int(self._wv.safe_field("digit_count", 0))
return int(self._wv_lv_spinbox_t.safe_field("digit_count", 0))
@property
def dec_point_pos(self):
"""if 0, there is no separator and the number is an integer"""
return int(self._wv.safe_field("dec_point_pos", 0))
return int(self._wv_lv_spinbox_t.safe_field("dec_point_pos", 0))
@property
def rollover(self):
"""Set to true for rollover functionality"""
return int(self._wv.safe_field("rollover", 0))
return int(self._wv_lv_spinbox_t.safe_field("rollover", 0))
@property
def digit_step_dir(self):
"""the direction the digit will step on encoder button press when editing"""
return int(self._wv.safe_field("digit_step_dir", 0))
return int(self._wv_lv_spinbox_t.safe_field("digit_step_dir", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -13,17 +13,18 @@ class LVSpinner(LVArc):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_spinner_t", ptr=True) or self
self._wv_lv_spinner_t = self.cast("lv_spinner_t", ptr=True) or self
self._wv = self._wv_lv_spinner_t
@property
def duration(self):
"""Anim duration in ms"""
return int(self._wv.safe_field("duration", 0))
return int(self._wv_lv_spinner_t.safe_field("duration", 0))
@property
def angle(self):
"""Anim angle in degrees"""
return int(self._wv.safe_field("angle", 0))
return int(self._wv_lv_spinner_t.safe_field("angle", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -13,16 +13,17 @@ class LVSwitch(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_switch_t", ptr=True) or self
self._wv_lv_switch_t = self.cast("lv_switch_t", ptr=True) or self
self._wv = self._wv_lv_switch_t
@property
def anim_state(self):
return int(self._wv.safe_field("anim_state", 0))
return int(self._wv_lv_switch_t.safe_field("anim_state", 0))
@property
def orientation(self):
"""Orientation of switch"""
return int(self._wv.safe_field("orientation", 0))
return int(self._wv_lv_switch_t.safe_field("orientation", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+9 -8
View File
@@ -14,35 +14,36 @@ class LVTable(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_table_t", ptr=True) or self
self._wv_lv_table_t = self.cast("lv_table_t", ptr=True) or self
self._wv = self._wv_lv_table_t
@property
def col_cnt(self):
return int(self._wv.safe_field("col_cnt", 0))
return int(self._wv_lv_table_t.safe_field("col_cnt", 0))
@property
def row_cnt(self):
return int(self._wv.safe_field("row_cnt", 0))
return int(self._wv_lv_table_t.safe_field("row_cnt", 0))
@property
def cell_data(self):
return ptr_or_none(self._wv.safe_field("cell_data"))
return ptr_or_none(self._wv_lv_table_t.safe_field("cell_data"))
@property
def row_h(self):
return ptr_or_none(self._wv.safe_field("row_h"))
return ptr_or_none(self._wv_lv_table_t.safe_field("row_h"))
@property
def col_w(self):
return ptr_or_none(self._wv.safe_field("col_w"))
return ptr_or_none(self._wv_lv_table_t.safe_field("col_w"))
@property
def col_act(self):
return int(self._wv.safe_field("col_act", 0))
return int(self._wv_lv_table_t.safe_field("col_act", 0))
@property
def row_act(self):
return int(self._wv.safe_field("row_act", 0))
return int(self._wv_lv_table_t.safe_field("row_act", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -13,19 +13,20 @@ class LVTabview(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_tabview_t", ptr=True) or self
self._wv_lv_tabview_t = self.cast("lv_tabview_t", ptr=True) or self
self._wv = self._wv_lv_tabview_t
@property
def tab_cur(self):
return int(self._wv.safe_field("tab_cur", 0))
return int(self._wv_lv_tabview_t.safe_field("tab_cur", 0))
@property
def tab_pos(self):
return int(self._wv.safe_field("tab_pos", 0))
return int(self._wv_lv_tabview_t.safe_field("tab_pos", 0))
@property
def tab_bar_size(self):
return int(self._wv.safe_field("tab_bar_size", 0))
return int(self._wv_lv_tabview_t.safe_field("tab_bar_size", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+17 -16
View File
@@ -14,81 +14,82 @@ class LVTextarea(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_textarea_t", ptr=True) or self
self._wv_lv_textarea_t = self.cast("lv_textarea_t", ptr=True) or self
self._wv = self._wv_lv_textarea_t
@property
def label(self):
"""Label of the text area"""
return ptr_or_none(self._wv.safe_field("label"))
return ptr_or_none(self._wv_lv_textarea_t.safe_field("label"))
@property
def placeholder_txt(self):
"""Place holder label. only visible if text is an empty string"""
return safe_string(self._wv, "placeholder_txt")
return safe_string(self._wv_lv_textarea_t, "placeholder_txt")
@property
def placeholder_txt_size(self):
"""Size of the placeholder text"""
return safe_point(self._wv, "placeholder_txt_size")
return safe_point(self._wv_lv_textarea_t, "placeholder_txt_size")
@property
def pwd_tmp(self):
"""Used to store the original text in password mode"""
return safe_string(self._wv, "pwd_tmp")
return safe_string(self._wv_lv_textarea_t, "pwd_tmp")
@property
def pwd_bullet(self):
"""Replacement characters displayed in password mode"""
return safe_string(self._wv, "pwd_bullet")
return safe_string(self._wv_lv_textarea_t, "pwd_bullet")
@property
def accepted_chars(self):
"""Only these characters will be accepted. NULL: accept all"""
return safe_string(self._wv, "accepted_chars")
return safe_string(self._wv_lv_textarea_t, "accepted_chars")
@property
def max_length(self):
"""The max. number of characters. 0: no limit"""
return int(self._wv.safe_field("max_length", 0))
return int(self._wv_lv_textarea_t.safe_field("max_length", 0))
@property
def pwd_show_time(self):
"""Time to show characters in password mode before change them to '*'"""
return int(self._wv.safe_field("pwd_show_time", 0))
return int(self._wv_lv_textarea_t.safe_field("pwd_show_time", 0))
@property
def sel_start(self):
"""Temporary values for text selection"""
return int(self._wv.safe_field("sel_start", 0))
return int(self._wv_lv_textarea_t.safe_field("sel_start", 0))
@property
def sel_end(self):
return int(self._wv.safe_field("sel_end", 0))
return int(self._wv_lv_textarea_t.safe_field("sel_end", 0))
@property
def text_sel_in_prog(self):
"""User is in process of selecting"""
return int(self._wv.safe_field("text_sel_in_prog", 0))
return int(self._wv_lv_textarea_t.safe_field("text_sel_in_prog", 0))
@property
def text_sel_en(self):
"""Text can be selected on this text area"""
return int(self._wv.safe_field("text_sel_en", 0))
return int(self._wv_lv_textarea_t.safe_field("text_sel_en", 0))
@property
def pwd_mode(self):
"""Replace characters with '*'"""
return int(self._wv.safe_field("pwd_mode", 0))
return int(self._wv_lv_textarea_t.safe_field("pwd_mode", 0))
@property
def one_line(self):
"""One line mode (ignore line breaks)"""
return int(self._wv.safe_field("one_line", 0))
return int(self._wv_lv_textarea_t.safe_field("one_line", 0))
@property
def static_accepted_chars(self):
"""1: Only a pointer is saved in `accepted_chars`"""
return int(self._wv.safe_field("static_accepted_chars", 0))
return int(self._wv_lv_textarea_t.safe_field("static_accepted_chars", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -14,11 +14,12 @@ class LVTileview(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_tileview_t", ptr=True) or self
self._wv_lv_tileview_t = self.cast("lv_tileview_t", ptr=True) or self
self._wv = self._wv_lv_tileview_t
@property
def tile_act(self):
return ptr_or_none(self._wv.safe_field("tile_act"))
return ptr_or_none(self._wv_lv_tileview_t.safe_field("tile_act"))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
@@ -13,11 +13,12 @@ class LVTileviewTile(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_tileview_tile_t", ptr=True) or self
self._wv_lv_tileview_tile_t = self.cast("lv_tileview_tile_t", ptr=True) or self
self._wv = self._wv_lv_tileview_tile_t
@property
def dir(self):
return int(self._wv.safe_field("dir", 0))
return int(self._wv_lv_tileview_tile_t.safe_field("dir", 0))
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+2 -1
View File
@@ -13,7 +13,8 @@ class LVWin(LVObject):
def __init__(self, obj):
super().__init__(obj)
self._wv = self.cast("lv_win_t", ptr=True) or self
self._wv_lv_win_t = self.cast("lv_win_t", ptr=True) or self
self._wv = self._wv_lv_win_t
def snapshot(self, include_children=False, include_styles=False):
"""Snapshot with widget-specific fields in widget_data."""
+100
View File
@@ -0,0 +1,100 @@
"""Runtime access to the field metadata behind the generated wrappers.
The wrappers carry the values; what the values *mean* - which fields matter
first, what an enum's numbers are called, how a widget summarises itself - lives
in the generator's widget_specs.json. The dashboard already ships that file to
the browser, so reading the same file here keeps the terminal output and the
HTML inspector telling one story instead of two that drift apart.
Everything degrades to the raw value when the file is missing, which is the case
when the plugin is vendored without its scripts/ directory.
"""
import json
from pathlib import Path
_SPECS_PATH = (
Path(__file__).resolve().parents[3] / "scripts" / "generators" / "widget_specs.json"
)
_specs = None
def _load() -> dict:
global _specs
if _specs is None:
try:
_specs = json.loads(_SPECS_PATH.read_text(encoding="utf-8"))
except (OSError, ValueError):
_specs = {}
return _specs
def widget_spec(class_name: str) -> dict:
"""The spec for a class name, with or without the 'lv_' prefix."""
specs = _load()
return specs.get(class_name) or specs.get("lv_" + class_name) or {}
def field_types(class_name: str) -> dict:
"""Map of field name -> generator field type ('int', 'bool', 'enum:4', ...)."""
return widget_spec(class_name).get("_auto", {}).get("fields", {})
def order_fields(class_name: str, keys) -> list:
"""Order widget_data keys: the spec's primary fields first, rest after."""
primary = widget_spec(class_name).get("primary", [])
keys = list(keys)
head = [k for k in primary if k in keys]
return head + [k for k in keys if k not in head]
def format_value(class_name: str, field: str, value) -> str:
"""Render one widget_data value: enums by name, points and areas inline."""
if value is None:
return "-"
names = widget_spec(class_name).get("enums", {}).get(field)
if names is not None and isinstance(value, int) and not isinstance(value, bool):
if 0 <= value < len(names):
return f"{names[value]} ({value})"
return str(value)
if isinstance(value, dict):
if {"x1", "y1", "x2", "y2"} <= value.keys():
return (
f"({value['x1']},{value['y1']})-({value['x2']},{value['y2']})"
)
if {"x", "y"} <= value.keys():
return f"({value['x']},{value['y']})"
if field_types(class_name).get(field) == "bool":
return str(bool(value))
if isinstance(value, str):
return f'"{escape(value)}"'
return str(value)
def escape(text: str) -> str:
"""Keep a value on one line: a label's text may well be several."""
return text.replace("\\", "\\\\").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
def summary(class_name: str, widget_data: dict, limit: int = 32) -> str:
"""One-line gist of a widget, from the spec's summary template."""
tpl = widget_spec(class_name).get("summary_tpl")
if not tpl or not widget_data:
return ""
import re
def _sub(m):
value = widget_data.get(m.group(1))
if value is None:
return ""
text = escape(str(value))
return text[:limit] + "…" if len(text) > limit else text
return re.sub(r"\{(\w+)\}", _sub, tpl).strip()
+82 -6
View File
@@ -33,6 +33,9 @@ def parse_enum(path: Path, enum_type: str, prefix: str,
raise RuntimeError(f"Cannot find {enum_type} enum in {path}")
entries = {}
# #defines of the header, plus the members already seen: an enum member is
# commonly defined in terms of earlier ones.
known = _defines(text)
current_val = 0
for line in m.group(1).splitlines():
line = line.strip().rstrip(",")
@@ -45,18 +48,38 @@ def parse_enum(path: Path, enum_type: str, prefix: str,
):
continue
match = re.match(
rf"({re.escape(prefix)}\w+)\s*=\s*(0x[\da-fA-F]+|\d+)", line
)
match = re.match(rf"({re.escape(prefix)}\w+)\s*=\s*(.+)$", line)
if match:
name = match.group(1)
current_val = int(match.group(2), 0)
# A comment can start on the same line as the value, and a Doxygen
# one often runs on to the next; only the expression before it is
# the value. LV_EVENT_PREPROCESS = 0x8000 is written that way. A
# comment that closes again can also sit between two operands, so
# drop those first rather than truncating the expression at one.
expression = re.sub(r"/\*.*?\*/", " ", match.group(2))
expression = re.split(r"//|/\*", expression, maxsplit=1)[0]
value = eval_c_expr(expression.rstrip().rstrip(","), known)
if value is None:
# Falling back to the running counter would give this member a
# plausible wrong number and shift every implicit member after
# it, which is impossible to notice in the generated table.
raise ValueError(
f"{name} is assigned `{expression.strip()}`, which this "
f"parser cannot evaluate. Teach eval_c_expr about it rather "
f"than letting the value be guessed."
)
current_val = value
else:
match = re.match(rf"({re.escape(prefix)}\w+)", line)
if not match:
continue
name = match.group(1)
# A skipped member is still a member: a later one may be written in
# terms of it, so it has to be resolvable even though it is left out
# of the table.
known[name] = current_val
if name in skip:
current_val += 1
continue
@@ -68,6 +91,54 @@ def parse_enum(path: Path, enum_type: str, prefix: str,
return entries
_DEFINE = re.compile(r"^#define\s+(_?LV_[A-Z0-9_]+)\s+(.+)$", re.M)
def _defines(text: str) -> dict:
"""The #define constants of a header, so enum values can reference them."""
return {name: value.split("/*")[0].strip()
for name, value in _DEFINE.findall(text)}
def eval_c_expr(expr: str, known: dict) -> "int | None":
"""Evaluate a C constant expression, or None if it cannot be resolved.
Enum members are regularly defined in terms of others -
`LV_FLEX_FLOW_ROW_WRAP = LV_FLEX_FLOW_ROW | LV_FLEX_WRAP` - and a parser
that only accepts literals silently numbers them sequentially instead,
which turns a bitmask into consecutive integers.
"""
expr = expr.strip().rstrip(",")
if not expr:
return None
for _ in range(8):
replaced = re.sub(
r"_?LV_[A-Z0-9_]+",
lambda m: f"({known[m.group(0)]})" if m.group(0) in known else m.group(0),
expr,
)
if replaced == expr:
break
expr = replaced
# Integer suffixes, then blank out hex literals so their letters are not
# mistaken for an unresolved identifier.
expr = re.sub(r"\b(0[xX][0-9a-fA-F]+|\d+)[uUlL]+\b", r"\1", expr)
if re.search(r"[A-Za-z_]", re.sub(r"0[xX][0-9a-fA-F]+", "0", expr)):
return None
if not re.fullmatch(r"[\d\sxXa-fA-F|&^~<>+\-*/()]+", expr):
return None
try:
value = int(eval(expr, {"__builtins__": {}}, {}))
except Exception:
return None
# Python's ints are unbounded, so ~0x0F is -16 where C's 32-bit unsigned
# gives 0xFFFFFFF0. Only complement can produce that difference here: the
# other operators agree for the values LVGL's headers hold.
if "~" in expr and value < 0:
value &= 0xFFFFFFFF
return value
def parse_bitmask_enum(path: Path, enum_type: str, prefix: str,
skip: set[str] | None = None) -> dict[int, str]:
"""Parse a C typedef enum with bitmask values (1u << N) from a header.
@@ -105,9 +176,14 @@ def parse_bitmask_enum(path: Path, enum_type: str, prefix: str,
):
continue
# Match: NAME = (1u << N) or NAME = 1 << N (parens optional)
# Match: NAME = (1u << N) or NAME = 1 << N (parens optional), and only
# that. Without anchoring the end, `NAME = (1 << 5) | (1 << 6)` would
# match its first shift and be recorded as a single-bit flag, which is
# worse than leaving a combination out of a bit table.
match = re.match(
rf"({re.escape(prefix)}\w+)\s*=\s*\(?1u?\s*<<\s*(\d+)\)?", line
rf"({re.escape(prefix)}\w+)\s*=\s*\(?1u?\s*<<\s*(\d+)\)?"
r"\s*,?\s*(?:/[/*].*)?$",
line,
)
if not match:
continue
@@ -6,10 +6,12 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from lvgl_paths import include_dir, lvgl_root
from enum_parser import parse_enum, generate_dict_module
LVGL_SRC = Path(__file__).parent.parent.parent.parent.parent / "src"
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
LVGL_ROOT = lvgl_root(__file__)
LVGL_SRC = LVGL_ROOT / "src"
LVGL_INC = include_dir(LVGL_ROOT)
OUTPUT = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "draw" / "lv_draw_consts.py"
DRAW_H = LVGL_INC / "draw" / "lv_draw.h"
@@ -5,9 +5,10 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from lvgl_paths import include_dir, lvgl_root
from enum_parser import parse_enum, generate_dict_module
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
LVGL_INC = include_dir(lvgl_root(__file__))
OUTPUT = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "misc" / "lv_event_consts.py"
SKIP_EVENTS = {"LV_EVENT_LAST", "LV_EVENT_PREPROCESS", "LV_EVENT_MARKED_DELETING"}
@@ -5,9 +5,10 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from lvgl_paths import include_dir, lvgl_root
from enum_parser import parse_enum, generate_dict_module
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
LVGL_INC = include_dir(lvgl_root(__file__))
OUTPUT = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "core" / "lv_indev_consts.py"
@@ -5,9 +5,10 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from lvgl_paths import include_dir, lvgl_root
from enum_parser import parse_bitmask_enum, generate_dict_module
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
LVGL_INC = include_dir(lvgl_root(__file__))
OUTPUT = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "core" / "lv_obj_flag_consts.py"
@@ -5,9 +5,10 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from lvgl_paths import include_dir, lvgl_root
from enum_parser import parse_bitmask_enum, generate_dict_module
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
LVGL_INC = include_dir(lvgl_root(__file__))
OUTPUT = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "core" / "lv_obj_state_consts.py"
SKIP = {"LV_STATE_ANY"}
@@ -2,16 +2,23 @@
"""Generate style constant tables from LVGL headers."""
import re
import sys
from pathlib import Path
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
LVGL_SRC = Path(__file__).parent.parent.parent.parent.parent / "src"
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from lvgl_paths import include_dir, lvgl_root
from enum_parser import parse_enum
LVGL_ROOT = lvgl_root(__file__)
LVGL_INC = include_dir(LVGL_ROOT)
LVGL_SRC = LVGL_ROOT / "src"
GDB_ROOT = Path(__file__).parent.parent.parent
OUTPUT = GDB_ROOT / "lvglgdb" / "lvgl" / "misc" / "lv_style_consts.py"
STYLE_H = LVGL_INC / "core" / "lv_style.h"
OBJ_STYLE_H = LVGL_INC / "core" / "lv_obj_style.h"
STYLE_GEN_H = LVGL_INC / "core" / "lv_style_gen.h"
AREA_H = LVGL_INC / "core" / "lv_area.h"
SKIP_PROPS = {
"LV_STYLE_PROP_INV",
@@ -113,22 +120,101 @@ def parse_color_props(style_gen_h: Path, prop_map: dict[int, str]) -> set[int]:
return color_ids
def parse_pointer_props(style_gen_h: Path, prop_map: dict[int, str]) -> set[int]:
"""Identify pointer properties from lv_style_gen.h setter signatures."""
def _props_by_value_type(
style_gen_h: Path, prop_map: dict[int, str], type_pattern: str
) -> set[int]:
"""Identify properties whose lv_style_gen.h setter takes the given value type."""
text = style_gen_h.read_text()
name_to_id = {v: k for k, v in prop_map.items()}
ptr_ids = set()
ids = set()
for match in re.finditer(
r"void\s+lv_style_set_(\w+)\s*\([^,]+,\s*(?:const\s+)?(?:void|lv_\w+)\s*\*",
text,
rf"void\s+lv_style_set_(\w+)\s*\([^,]+,\s*{type_pattern}\s+value\s*\)", text
):
prop_name = match.group(1).upper()
if prop_name in name_to_id:
ptr_ids.add(name_to_id[prop_name])
return ptr_ids
ids.add(name_to_id[prop_name])
return ids
def generate(props, parts, states, color_ids, pointer_ids) -> str:
def parse_pointer_props(style_gen_h: Path, prop_map: dict[int, str]) -> set[int]:
"""Identify pointer properties from lv_style_gen.h setter signatures.
Matches any pointer type, so `const int32_t *` (the grid templates) is
classified as a pointer rather than read as a number.
"""
return _props_by_value_type(style_gen_h, prop_map, r"(?:const\s+)?\w+\s*\*")
def parse_src_props(style_gen_h: Path, prop_map: dict[int, str]) -> set[int]:
"""Identify `const void *` properties: the image sources.
Their value may be an `lv_image_dsc_t *`, a file path or an `LV_SYMBOL_*`
string, so the formatter is allowed to try reading them as C strings.
"""
return _props_by_value_type(style_gen_h, prop_map, r"const\s+void\s*\*")
def parse_coord_props(style_gen_h: Path, prop_map: dict[int, str]) -> set[int]:
"""Identify `int32_t` properties: the ones that may hold LV_PCT()/LV_SIZE_CONTENT."""
return _props_by_value_type(style_gen_h, prop_map, r"int32_t")
def parse_bool_props(style_gen_h: Path, prop_map: dict[int, str]) -> set[int]:
"""Identify `bool` properties."""
return _props_by_value_type(style_gen_h, prop_map, r"bool")
def parse_enum_props(
style_gen_h: Path, prop_map: dict[int, str]
) -> dict[int, dict[int, str]]:
"""Map each enum-valued property to its {value: name} table.
The setter signature names the enum type; the type's own header is found by
searching the include tree for its typedef.
"""
text = style_gen_h.read_text()
name_to_id = {v: k for k, v in prop_map.items()}
headers = list(LVGL_INC.rglob("*.h"))
enum_tables: dict[str, dict[int, str]] = {}
result = {}
for match in re.finditer(
r"void\s+lv_style_set_(\w+)\s*\([^,]+,\s*(lv_\w+_t)\s+value\s*\)", text
):
prop_name, enum_type = match.group(1).upper(), match.group(2)
if prop_name not in name_to_id or enum_type in ("lv_color_t", "lv_opa_t"):
continue
if enum_type not in enum_tables:
enum_tables[enum_type] = _parse_named_enum(headers, enum_type)
if enum_tables[enum_type]:
result[name_to_id[prop_name]] = enum_tables[enum_type]
return result
def _parse_named_enum(headers: list[Path], enum_type: str) -> dict[int, str]:
"""Find and parse an enum typedef by name, {} if it cannot be found."""
needle = f"}} {enum_type};"
prefix = enum_type.removesuffix("_t").upper() + "_"
for header in headers:
if needle not in header.read_text():
continue
try:
return parse_enum(header, enum_type, prefix)
except RuntimeError:
return {}
return {}
def parse_coord_shift(area_h: Path) -> int:
"""Read LV_COORD_TYPE_SHIFT, which defines the LV_PCT()/content encoding."""
m = re.search(r"#define\s+LV_COORD_TYPE_SHIFT\s+\((\d+)U?\)", area_h.read_text())
if not m:
raise RuntimeError("Cannot find LV_COORD_TYPE_SHIFT")
return int(m.group(1))
def generate(props, parts, states, color_ids, pointer_ids, src_ids, coord_ids,
bool_ids, enum_values, coord_shift) -> str:
"""Generate Python source for the style constants module."""
lines = [
'"""',
@@ -166,13 +252,32 @@ def generate(props, parts, states, color_ids, pointer_ids) -> str:
lines.append("COLOR_PROPS = set()")
lines.append("")
if pointer_ids:
lines.append("POINTER_PROPS = {")
for v in sorted(pointer_ids):
lines.append(f" {v}, # {props.get(v, '?')}")
lines.append("}")
else:
lines.append("POINTER_PROPS = set()")
for name, ids in (
("POINTER_PROPS", pointer_ids),
("SRC_PROPS", src_ids),
("COORD_PROPS", coord_ids),
("BOOL_PROPS", bool_ids),
):
if ids:
lines.append(f"{name} = {{")
for v in sorted(ids):
lines.append(f" {v}, # {props.get(v, '?')}")
lines.append("}")
else:
lines.append(f"{name} = set()")
lines.append("")
lines.append("ENUM_PROP_VALUES = {")
for prop_id in sorted(enum_values):
lines.append(f" {prop_id}: {{ # {props.get(prop_id, '?')}")
for val in sorted(enum_values[prop_id]):
lines.append(f' {val}: "{enum_values[prop_id][val]}",')
lines.append(" },")
lines.append("}")
lines.append("")
lines.append("# From LV_COORD_TYPE_SHIFT in lv_area.h")
lines.append(f"COORD_TYPE_SHIFT = {coord_shift}")
lines.append("")
return "\n".join(lines)
@@ -184,12 +289,19 @@ def main():
states = parse_states(OBJ_STYLE_H)
color_ids = parse_color_props(STYLE_GEN_H, props)
pointer_ids = parse_pointer_props(STYLE_GEN_H, props)
src_ids = parse_src_props(STYLE_GEN_H, props)
coord_ids = parse_coord_props(STYLE_GEN_H, props)
bool_ids = parse_bool_props(STYLE_GEN_H, props)
enum_values = parse_enum_props(STYLE_GEN_H, props)
coord_shift = parse_coord_shift(AREA_H)
src = generate(props, parts, states, color_ids, pointer_ids)
src = generate(props, parts, states, color_ids, pointer_ids, src_ids,
coord_ids, bool_ids, enum_values, coord_shift)
OUTPUT.write_text(src)
print(
f"Generated {OUTPUT.name} ({len(props)} props, {len(parts)} parts, "
f"{len(states)} states, {len(color_ids)} color, {len(pointer_ids)} pointer)"
f"{len(states)} states, {len(color_ids)} color, {len(pointer_ids)} pointer, "
f"{len(coord_ids)} coord, {len(bool_ids)} bool, {len(enum_values)} enum)"
)
@@ -5,9 +5,10 @@ import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from lvgl_paths import include_dir, lvgl_root
from enum_parser import parse_enum, generate_dict_module
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
LVGL_INC = include_dir(lvgl_root(__file__))
OUTPUT = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "core" / "lv_observer_consts.py"
@@ -24,9 +24,11 @@ from pathlib import Path
from dataclasses import dataclass, field as dc_field
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from lvgl_paths import include_dir, lvgl_root # noqa: E402
LVGL_SRC = Path(__file__).parent.parent.parent.parent.parent / "src"
LVGL_INC = Path(__file__).parent.parent.parent.parent.parent / "include" / "lvgl"
LVGL_ROOT = lvgl_root(__file__)
LVGL_SRC = LVGL_ROOT / "src"
LVGL_INC = include_dir(LVGL_ROOT)
WIDGETS_DIR = LVGL_SRC / "widgets"
OUTPUT_DIR = Path(__file__).parent.parent.parent / "lvglgdb" / "lvgl" / "widgets"
SPECS_JSON = Path(__file__).parent / "widget_specs.json"
@@ -42,7 +44,9 @@ SIMPLE_INT_TYPES = {
def _scan_enum_types() -> set[str]:
"""Scan LVGL headers to find all typedef enum and int-like alias type names."""
result = set()
headers = list(LVGL_SRC.rglob("*.h")) + list(LVGL_INC.rglob("*.h"))
# In a checkout LVGL_INC *is* LVGL_SRC, so the two trees have to be merged
# rather than concatenated or every header is read twice.
headers = sorted({*LVGL_SRC.rglob("*.h"), *LVGL_INC.rglob("*.h")})
for h in headers:
text = h.read_text(errors="ignore")
# typedef enum { ... } lv_xxx_t;
@@ -235,22 +239,28 @@ def parse_widgets() -> dict[str, WidgetDef]:
return widgets
def _field_expr(f: StructField) -> str | None:
"""Return snapshot expression for a field, or None to skip."""
def _field_expr(f: StructField, attr: str = "_wv") -> str | None:
"""Return snapshot expression for a field, or None to skip.
`attr` is the class's own cast of the object. Each class needs its own,
because a widget struct embeds its ancestor as the first member: casting
an lv_slider_t to lv_bar_t is valid, but reading lv_bar_t's fields off an
lv_slider_t cast is not, and safe_field() would quietly return the default.
"""
if f.is_array:
return None
if f.is_obj_pointer:
return f'ptr_or_none(self._wv.safe_field("{f.name}"))'
return f'ptr_or_none(self.{attr}.safe_field("{f.name}"))'
if f.is_string:
return f'safe_string(self._wv, "{f.name}")'
return f'safe_string(self.{attr}, "{f.name}")'
if f.is_pointer:
return f'ptr_or_none(self._wv.safe_field("{f.name}"))'
return f'ptr_or_none(self.{attr}.safe_field("{f.name}"))'
if f.c_type == "lv_color_t":
return f'safe_color(self._wv, "{f.name}")'
return f'safe_color(self.{attr}, "{f.name}")'
if f.c_type == "lv_area_t":
return f'safe_area(self._wv, "{f.name}")'
return f'safe_area(self.{attr}, "{f.name}")'
if f.c_type == "lv_point_t":
return f'safe_point(self._wv, "{f.name}")'
return f'safe_point(self.{attr}, "{f.name}")'
# Known wrapper types — use snapshot() for rich output
_WRAPPER_TYPES = {
"lv_draw_buf_t": ("lvglgdb.lvgl.draw.lv_draw_buf", "LVDrawBuf"),
@@ -260,13 +270,13 @@ def _field_expr(f: StructField) -> str | None:
}
if f.c_type in _WRAPPER_TYPES:
mod, cls = _WRAPPER_TYPES[f.c_type]
return f'safe_wrapper(self._wv, "{f.name}", "{mod}", "{cls}")'
return f'safe_wrapper(self.{attr}, "{f.name}", "{mod}", "{cls}")'
if f.is_bitfield or f.c_type in SIMPLE_INT_TYPES or f.c_type.startswith(("uint", "int")):
return f'int(self._wv.safe_field("{f.name}", 0))'
return f'int(self.{attr}.safe_field("{f.name}", 0))'
# TODO: implement generic struct expansion (Value.to_dict) for
# non-enum lv_*_t types like lv_calendar_date_t.
if f.c_type in _INT_SAFE_TYPES:
return f'int(self._wv.safe_field("{f.name}", 0))'
return f'int(self.{attr}.safe_field("{f.name}", 0))'
return None
@@ -372,20 +382,38 @@ def safe_point(obj, field_name):
def safe_wrapper(obj, field_name, module_path, class_name):
"""Read a struct field using its known Value wrapper, return snapshot dict."""
"""Read a struct field using its known Value wrapper, return snapshot dict.
Not every wrapper has a snapshot(); LVArray does not. Value forwards an
unknown attribute to gdb.Value.__getitem__, so asking for one raises
gdb.error rather than AttributeError, and the caller would otherwise lose
the whole widget - and its subtree - over a single field.
"""
import gdb
val = obj.safe_field(field_name)
if val is None or not getattr(val, 'is_ok', True):
return None
import importlib
mod = importlib.import_module(module_path)
cls = getattr(mod, class_name)
wrapper = cls(val)
return wrapper.snapshot().as_dict()
try:
mod = importlib.import_module(module_path)
cls = getattr(mod, class_name)
wrapper = cls(val)
if getattr(type(wrapper), "snapshot", None) is None:
# An embedded struct - lv_scale_t.needles - is not convertible to an
# int, so the address has to come from the field itself.
if val.type.strip_typedefs().code != gdb.TYPE_CODE_PTR:
val = val.address
return {"addr": hex(int(val)), "type": class_name}
return wrapper.snapshot().as_dict()
except (gdb.error, gdb.MemoryError, ImportError, AttributeError, TypeError):
return None
'''
def gen_widget_file(wdef: WidgetDef, widgets: dict[str, WidgetDef]) -> str:
"""Generate a single widget module file."""
attr = f"_wv_{wdef.c_type}"
lines = [
'"""',
f"Auto-generated wrapper for {wdef.c_type}.",
@@ -406,7 +434,7 @@ def gen_widget_file(wdef: WidgetDef, widgets: dict[str, WidgetDef]) -> str:
# Import helpers only if needed
needs = set()
for f in wdef.fields:
expr = _field_expr(f)
expr = _field_expr(f, attr)
if expr is None:
continue
if "ptr_or_none" in expr:
@@ -435,13 +463,14 @@ def gen_widget_file(wdef: WidgetDef, widgets: dict[str, WidgetDef]) -> str:
lines.append("")
lines.append(f" def __init__(self, obj):")
lines.append(f" super().__init__(obj)")
lines.append(f' self._wv = self.cast("{wdef.c_type}", ptr=True) or self')
lines.append(f' self.{attr} = self.cast("{wdef.c_type}", ptr=True) or self')
lines.append(f" self._wv = self.{attr}")
lines.append("")
# Properties
snapshot_fields = []
for f in wdef.fields:
expr = _field_expr(f)
expr = _field_expr(f, attr)
if expr is None:
continue
lines.append(f" @property")
+48
View File
@@ -0,0 +1,48 @@
"""Where LVGL is, for the generators that read its headers.
Walking a fixed number of parents up from the generator's own file only works
while the plugin sits in the layout it ships in. The plugin is also vendored
into other projects, and an installed LVGL keeps its headers somewhere else
again, so the checkout is searched for instead of assumed - and a generator that
cannot find one stops rather than writing an empty table over a good one.
"""
import os
from pathlib import Path
def _is_lvgl(path):
"""A checkout, or an installed tree that only has the public headers."""
if (path / "lv_version.h").is_file():
return (path / "src").is_dir() or (path / "include" / "lvgl").is_dir()
# An installed tree has no top-level lv_version.h, only the copy that was
# installed with the rest of the public headers.
return (path / "include" / "lvgl" / "lv_version.h").is_file()
def lvgl_root(start=None):
"""The LVGL checkout to read: $LVGL_ROOT, or the nearest one above `start`."""
from_env = os.environ.get("LVGL_ROOT")
if from_env:
root = Path(from_env).expanduser().resolve()
if not _is_lvgl(root):
raise SystemExit(
f"LVGL_ROOT is {root}, which has no lv_version.h beside a src/ "
f"or include/lvgl/, and none inside include/lvgl/ either"
)
return root
here = Path(start or __file__).resolve()
for parent in here.parents:
if _is_lvgl(parent):
return parent
raise SystemExit(
f"no LVGL checkout above {here}. Set LVGL_ROOT to one, for example\n"
f" LVGL_ROOT=~/lvgl python3 {Path(here).name}"
)
def include_dir(root):
"""Where the public headers are: include/lvgl if installed, else src/."""
installed = root / "include" / "lvgl"
return installed if installed.is_dir() else root / "src"
+101
View File
@@ -0,0 +1,101 @@
"""Tests for the generator helpers under scripts/.
These read C headers and the filesystem, so they need no target and no GDB -
unlike test_style_decode.py, nothing here imports lvglgdb.
Run from the GDB script root with:
python3 -m unittest discover tests
"""
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
from enum_parser import parse_enum # noqa: E402
from lvgl_paths import _is_lvgl, include_dir # noqa: E402
def write_enum(directory: Path, body: str) -> Path:
"""A header holding one typedef enum, as parse_enum expects to find it."""
path = directory / "lv_test.h"
path.write_text("typedef enum {\n" + body + "\n} lv_test_t;\n")
return path
class TestParseEnum(unittest.TestCase):
def test_a_trailing_comment_is_not_part_of_the_value(self):
with tempfile.TemporaryDirectory() as tmp:
path = write_enum(Path(tmp), """
LV_TEST_A = 0x10, /** the first one, which runs
* on to the next line */
LV_TEST_B = 0x20,
""")
self.assertEqual(
parse_enum(path, "lv_test_t", "LV_TEST_"),
{0x10: "A", 0x20: "B"},
)
def test_a_comment_between_operands_does_not_truncate_the_value(self):
with tempfile.TemporaryDirectory() as tmp:
path = write_enum(Path(tmp), """
LV_TEST_A = 0x10,
LV_TEST_B = LV_TEST_A /* keep going */ | 0x01,
""")
self.assertEqual(
parse_enum(path, "lv_test_t", "LV_TEST_"),
{0x10: "A", 0x11: "B"},
)
def test_a_skipped_member_can_still_be_referenced(self):
with tempfile.TemporaryDirectory() as tmp:
path = write_enum(Path(tmp), """
LV_TEST_A = 0x10,
LV_TEST_HIDDEN = 0x20,
LV_TEST_B = LV_TEST_HIDDEN | 0x01,
""")
entries = parse_enum(path, "lv_test_t", "LV_TEST_",
skip={"LV_TEST_HIDDEN"})
self.assertEqual(entries, {0x10: "A", 0x21: "B"})
def test_an_unevaluable_value_is_an_error_not_a_guess(self):
with tempfile.TemporaryDirectory() as tmp:
path = write_enum(Path(tmp), """
LV_TEST_A = some_call(3),
""")
with self.assertRaises(ValueError):
parse_enum(path, "lv_test_t", "LV_TEST_")
class TestLvglPaths(unittest.TestCase):
def make_tree(self, tmp: str, *files: str) -> Path:
root = Path(tmp)
for name in files:
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("")
return root
def test_a_checkout_is_found(self):
with tempfile.TemporaryDirectory() as tmp:
root = self.make_tree(tmp, "lv_version.h", "src/lv_init.c")
self.assertTrue(_is_lvgl(root))
self.assertEqual(include_dir(root), root / "src")
def test_an_installed_tree_is_found(self):
"""No top-level lv_version.h, only the installed public headers."""
with tempfile.TemporaryDirectory() as tmp:
root = self.make_tree(tmp, "include/lvgl/lv_version.h")
self.assertTrue(_is_lvgl(root))
self.assertEqual(include_dir(root), root / "include" / "lvgl")
def test_an_unrelated_directory_is_not_lvgl(self):
with tempfile.TemporaryDirectory() as tmp:
self.assertFalse(_is_lvgl(self.make_tree(tmp, "src/main.c")))
if __name__ == "__main__":
unittest.main()
+152
View File
@@ -0,0 +1,152 @@
"""Tests for the style value decoders.
The decoders are pure functions, so they are tested without a live target. GDB
is stubbed because importing lvglgdb.value needs a subclassable gdb.Value.
Run from the GDB script root with:
python3 -m unittest discover tests
"""
import sys
import types
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def _install_stubs():
"""Make lvglgdb.lvgl.misc.lv_style importable outside GDB."""
if "gdb" not in sys.modules:
gdb = types.ModuleType("gdb")
class _Error(Exception):
pass
class _Value:
def __init__(self, *args, **kwargs):
pass
class _Type:
pass
gdb.Value = _Value
gdb.Type = _Type
gdb.error = _Error
gdb.MemoryError = _Error
for code, name in enumerate(
("PTR", "STRUCT", "UNION", "INT", "ENUM", "ARRAY", "TYPEDEF")
):
setattr(gdb, f"TYPE_CODE_{name}", code)
sys.modules["gdb"] = gdb
# Bind the packages to their directories without running their __init__,
# which pulls in the GDB command layer.
for name in ("lvglgdb", "lvglgdb.lvgl", "lvglgdb.lvgl.misc"):
if name not in sys.modules:
module = types.ModuleType(name)
module.__path__ = [str(ROOT.joinpath(*name.split(".")))]
sys.modules[name] = module
sys.path.insert(0, str(ROOT))
_install_stubs()
from lvglgdb.lvgl.misc.lv_style import decode_coord, decode_enum # noqa: E402
from lvglgdb.lvgl.misc.lv_style_consts import ( # noqa: E402
COORD_PROPS,
COORD_TYPE_SHIFT,
ENUM_PROP_VALUES,
POINTER_PROPS,
STYLE_PROP_NAMES,
)
SPEC = 1 << COORD_TYPE_SHIFT
COORD_MAX = SPEC - 1
PCT_POS_MAX = (COORD_MAX - 1) // 2
def lv_pct(value: int) -> int:
"""The LV_PCT() macro, so the tests encode the way LVGL does."""
stored = PCT_POS_MAX - value if value < 0 else value
return SPEC | stored
def prop_id(name: str) -> int:
return next(k for k, v in STYLE_PROP_NAMES.items() if v == name)
class TestDecodeCoord(unittest.TestCase):
def test_plain_pixels(self):
self.assertEqual(decode_coord(0), "0")
self.assertEqual(decode_coord(100), "100")
self.assertEqual(decode_coord(32767), "32767")
def test_negative_pixels(self):
self.assertEqual(decode_coord(-5), "-5")
self.assertEqual(decode_coord(-1), "-1")
def test_percent(self):
self.assertEqual(decode_coord(lv_pct(100)), "100%")
self.assertEqual(decode_coord(lv_pct(50)), "50%")
self.assertEqual(decode_coord(lv_pct(0)), "0%")
def test_negative_percent(self):
self.assertEqual(decode_coord(lv_pct(-20)), "-20%")
def test_size_content(self):
self.assertEqual(decode_coord(SPEC | COORD_MAX), "content")
def test_measured_value_from_a_real_target(self):
"""536871012 is what a widgets-demo screen actually reports for HEIGHT."""
self.assertEqual(decode_coord(536871012), "100%")
def test_a_negative_value_whose_type_bits_read_as_spec(self):
"""LV_COORD_PLAIN() keeps bit 31, so `plain` can exceed the pct range.
LVGL guards the same way - LV_COORD_IS_PCT() also requires
LV_COORD_PLAIN(x) <= LV_PCT_STORED_MAX - and without the guard this
comes out as a nonsense percentage rather than the number it is.
"""
raw = ((1 << 31) | SPEC | 100) - (1 << 32)
self.assertEqual(decode_coord(raw), str(raw))
class TestDecodeEnum(unittest.TestCase):
def test_plain_enum(self):
flex_flow = prop_id("FLEX_FLOW")
self.assertEqual(decode_enum(flex_flow, 0), "ROW")
self.assertEqual(decode_enum(flex_flow, 1), "COLUMN")
def test_bitmask_is_decomposed(self):
border_side = prop_id("BORDER_SIDE")
names = ENUM_PROP_VALUES[border_side]
bottom = next(bit for bit, n in names.items() if n == "BOTTOM")
top = next(bit for bit, n in names.items() if n == "TOP")
self.assertEqual(decode_enum(border_side, bottom | top), "BOTTOM|TOP")
def test_exact_match_wins_over_decomposition(self):
border_side = prop_id("BORDER_SIDE")
full = next(bit for bit, n in ENUM_PROP_VALUES[border_side].items()
if n == "FULL")
self.assertEqual(decode_enum(border_side, full), "FULL")
def test_unknown_value_falls_back_to_the_number(self):
self.assertEqual(decode_enum(prop_id("FLEX_FLOW"), 99), "99")
class TestGeneratedTables(unittest.TestCase):
def test_sizes_are_classified_as_coordinates(self):
for name in ("WIDTH", "HEIGHT", "MIN_WIDTH", "MAX_HEIGHT", "PAD_LEFT"):
self.assertIn(prop_id(name), COORD_PROPS, name)
def test_grid_templates_are_pointers_not_numbers(self):
for name in ("GRID_COLUMN_DSC_ARRAY", "GRID_ROW_DSC_ARRAY"):
self.assertIn(prop_id(name), POINTER_PROPS, name)
def test_a_property_is_never_both_a_coordinate_and_an_enum(self):
self.assertEqual(COORD_PROPS & set(ENUM_PROP_VALUES), set())
if __name__ == "__main__":
unittest.main()
+100
View File
@@ -0,0 +1,100 @@
"""Tests for the widget field metadata used by `info widget`.
The module only reads widget_specs.json and formats values, so it needs no
target and no GDB - it is imported straight from its file to keep the lvglgdb
package (and its `import gdb`) out of the way.
Run from the GDB script root with:
python3 -m unittest discover tests
"""
import importlib.util
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
_spec = importlib.util.spec_from_file_location(
"widget_specs_under_test", ROOT / "lvglgdb" / "lvgl" / "widgets" / "specs.py"
)
specs = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(specs)
class TestSpecLookup(unittest.TestCase):
def test_a_class_name_without_the_prefix_still_finds_its_spec(self):
# Before LVGL 6d999331d the class names carried no 'lv_'.
self.assertEqual(
specs.widget_spec("label"), specs.widget_spec("lv_label")
)
def test_an_unknown_class_has_an_empty_spec_rather_than_raising(self):
self.assertEqual(specs.widget_spec("lv_nope"), {})
class TestFormatValue(unittest.TestCase):
def test_an_enum_is_named(self):
self.assertEqual(
specs.format_value("lv_label", "long_mode", 2), "SCROLL (2)"
)
def test_an_out_of_range_enum_stays_a_number(self):
self.assertEqual(specs.format_value("lv_label", "long_mode", 99), "99")
def test_a_bool_field_reads_as_a_bool_not_as_1(self):
self.assertEqual(specs.format_value("lv_label", "recolor", 1), "True")
def test_points_and_areas_are_rendered_inline(self):
self.assertEqual(
specs.format_value("lv_label", "offset", {"x": 1, "y": 2}), "(1,2)"
)
self.assertEqual(
specs.format_value(
"lv_slider", "left_knob_area",
{"x1": 1, "y1": 2, "x2": 3, "y2": 4},
),
"(1,2)-(3,4)",
)
def test_a_missing_value_is_a_dash(self):
self.assertEqual(specs.format_value("lv_label", "text", None), "-")
def test_a_multiline_text_stays_on_one_line(self):
self.assertEqual(
specs.format_value("lv_label", "text", "a\nb"), '"a\\nb"'
)
class TestSummary(unittest.TestCase):
def test_the_template_is_filled_from_widget_data(self):
self.assertEqual(
specs.summary("lv_arc", {"value": 50, "min_value": 0, "max_value": 100}),
"50 [0..100]",
)
def test_a_missing_field_leaves_a_gap_rather_than_a_none(self):
self.assertEqual(specs.summary("lv_label", {"text": None}), "")
def test_a_long_value_is_cut(self):
self.assertEqual(
specs.summary("lv_label", {"text": "x" * 40}, limit=8), "xxxxxxxx…"
)
def test_a_widget_without_a_template_has_no_summary(self):
self.assertEqual(specs.summary("lv_obj", {"whatever": 1}), "")
class TestOrderFields(unittest.TestCase):
def test_primary_fields_come_first_and_nothing_is_lost(self):
keys = ["rotation", "value", "dragging", "max_value"]
ordered = specs.order_fields("lv_arc", keys)
self.assertEqual(ordered[0], "value")
self.assertEqual(sorted(ordered), sorted(keys))
def test_an_unknown_class_keeps_the_given_order(self):
keys = ["b", "a"]
self.assertEqual(specs.order_fields("lv_nope", keys), keys)
if __name__ == "__main__":
unittest.main()