The Page Editor canvas now paints widgets WYSIWYG instead of as labelled placeholder boxes, following the ontrolOx draw() code and the R-ION firmware: - text drawn with the device's own bitmap fonts (fontBank .bin / fontBankXL .oxfl), Windows font fallback; alignment, clipping at the box edge, half-size subscript digits, transparent vs filled boxes; the line is centred on the box even when the font is taller - each widget shows the text its `in` selects: NumericLabel formats, lookup items, Boolean true/false text and colours, date and time - images from the local image banks, centred or cropped; missing ones drawn as the device's red/white checker; page background image - 10x10 pixel grid over the page colour, with a Grid toggle - widgets hit-tested by layout box so transparent labels stay clickable; hidden widgets shown as a dashed outline - layout row gets an X/Y/Width/Height dialog - scaled device frame cached per zoom (redraws ~15 ms) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
532 lines
22 KiB
Python
532 lines
22 KiB
Python
"""Draws Ox widgets on the Page Editor canvas the way the device's firmware draws them.
|
|
|
|
The rules are the R-ION firmware's (graphics.c, ontrolXx.c) and each widget's draw():
|
|
|
|
text boxes drawn with the device's own bitmap fonts from Sedona home's fontBank
|
|
(a Windows font of the same size when the file is not there). The
|
|
line is centred vertically, aligned by attributes bits 4-5 when the
|
|
box is wider than the text, and cut at the character that would cross
|
|
the right edge. A box whose draw-background bit (3) is clear is
|
|
transparent. The last `subscriptDigits` characters are drawn half size.
|
|
images centred in a box larger than the image, cropped to a smaller one; a
|
|
file the editor cannot find is drawn as the device draws a missing
|
|
one, a red and white checker.
|
|
values each widget shows the text its `in` currently selects, so a live
|
|
page reads as the device does and an offline one shows the defaults.
|
|
|
|
Everything is drawn in canvas units: device pixels times the zoom.
|
|
"""
|
|
|
|
import datetime
|
|
import glob
|
|
import os
|
|
import tkinter as tk
|
|
from tkinter import font as tkfont
|
|
|
|
try:
|
|
from PIL import Image, ImageTk
|
|
_HAS_PIL = True
|
|
except ImportError:
|
|
_HAS_PIL = False
|
|
|
|
import app_state
|
|
import properties
|
|
from ox_widget_properties import (_Model, BACKGROUND_BIT, ALIGN_MASK, SUBSCRIPT_MASK,
|
|
DATE_HIDE_YEAR, DATE_TWO_DIGIT_YEAR, DATE_LEADING_ZERO,
|
|
DATE_MONTH_FIRST, IMAGE_EXTENSIONS, font_names,
|
|
parse_lookup, parse_numeric_format, rgb565_to_hex)
|
|
|
|
PLACEHOLDER_FILL = "#dce6f2"
|
|
PLACEHOLDER_OUTLINE = "#7f9db9"
|
|
HIDDEN_OUTLINE = "#9a9a9a"
|
|
CHECKER_COLOURS = ((232, 60, 60), (255, 255, 255)) # the firmware's RED, WHITE
|
|
CHECKER_CELL = 4
|
|
SEPARATORS = {0: "/", 1: ".", 2: "-", 3: " "}
|
|
FALLBACK_FAMILY = "Segoe UI"
|
|
|
|
_font_cache = {}
|
|
_families = None
|
|
_text_cache = {} # rendered lines, so a redraw does not re-decode every glyph
|
|
TEXT_CACHE_SIZE = 400
|
|
|
|
|
|
def _font(family, pixels, style):
|
|
"""A Tk font `pixels` tall; style is fontAttributes' 0 normal, 1 bold, 2 italic."""
|
|
global _families
|
|
if _families is None:
|
|
_families = {f.lower() for f in tkfont.families()}
|
|
if family.lower() not in _families:
|
|
family = FALLBACK_FAMILY
|
|
key = (family, pixels, style)
|
|
if key not in _font_cache:
|
|
_font_cache[key] = tkfont.Font(
|
|
family=family, size=-max(1, pixels),
|
|
weight="bold" if style == 1 else "normal",
|
|
slant="italic" if style == 2 else "roman")
|
|
return _font_cache[key]
|
|
|
|
|
|
# --- the device's own bitmap fonts ----------------------------------------------------
|
|
|
|
class BitmapFont:
|
|
"""A device font file: fontBank/'32pt Tahoma.bin', or fontBankXL/'.oxfl' (same layout).
|
|
|
|
0..63 header; u16 character count at 32, line height at 34
|
|
64.. 10 bytes per character: code (u8, or u16 above 191), data offset (u24
|
|
at 2), width, height, x offset (signed), y offset, x advance
|
|
data 4-bit grey, two pixels a byte, high nibble first, row by row
|
|
"""
|
|
|
|
def __init__(self, path):
|
|
data = open(path, "rb").read()
|
|
self.data = data
|
|
self.height = data[34]
|
|
count = data[32] | data[33] << 8
|
|
self.glyphs = {}
|
|
for index in range(count):
|
|
e = data[64 + index * 10:74 + index * 10]
|
|
if len(e) < 10:
|
|
break
|
|
code = e[0] if e[0] < 192 and e[1] == 0 else e[0] | e[1] << 8
|
|
self.glyphs[code] = (e[2] | e[3] << 8 | e[4] << 16, e[5], e[6],
|
|
e[7] - 256 if e[7] > 127 else e[7], e[8], e[9])
|
|
|
|
def metrics(self, code):
|
|
"""(printWidth, xOffset) the way Calculate_Text_Width works them out."""
|
|
_, width, _, x_off, _, advance = self.glyphs[code]
|
|
x_offset = 1 if x_off < 0 else x_off + 1
|
|
print_width = max(advance, width, x_offset + width)
|
|
return print_width, x_offset
|
|
|
|
def glyph_image(self, code):
|
|
offset, width, height, _, _, _ = self.glyphs[code]
|
|
img = Image.new("L", (max(1, width), max(1, height)))
|
|
if width and height:
|
|
raw = self.data[offset:offset + (width * height + 1) // 2]
|
|
pixels = bytearray(width * height)
|
|
for i in range(width * height):
|
|
byte = raw[i >> 1] if (i >> 1) < len(raw) else 0
|
|
pixels[i] = ((byte >> 4) if i % 2 == 0 else (byte & 0x0F)) * 17
|
|
img.putdata(pixels)
|
|
return img
|
|
|
|
|
|
_bitmap_fonts = {}
|
|
|
|
|
|
def find_bitmap_font(family, size, style):
|
|
"""The device font for fontName/fontSize/fontAttributes, or None when none is local.
|
|
|
|
get_Font_File_Name's naming: '<size>pt <Name>', prefixed 'bold ' for bold. There is
|
|
no italic cut, so italic falls back to the regular one as the device's lookup would
|
|
find nothing better.
|
|
"""
|
|
home = getattr(app_state, "SEDONA_HOME", None)
|
|
if not home or not _HAS_PIL:
|
|
return None
|
|
stem = f"{'bold ' if style == 1 else ''}{size}pt {family}"
|
|
candidates = [os.path.join(home, "ontrol", "fontBank", stem + ".bin"),
|
|
os.path.join(home, "ontrol", "fontBankXL", stem + ".oxfl")]
|
|
if style == 1:
|
|
plain = f"{size}pt {family}"
|
|
candidates += [os.path.join(home, "ontrol", "fontBank", plain + ".bin"),
|
|
os.path.join(home, "ontrol", "fontBankXL", plain + ".oxfl")]
|
|
for path in candidates:
|
|
if path in _bitmap_fonts:
|
|
return _bitmap_fonts[path]
|
|
if os.path.isfile(path):
|
|
try:
|
|
_bitmap_fonts[path] = BitmapFont(path)
|
|
except (OSError, IndexError):
|
|
_bitmap_fonts[path] = None
|
|
return _bitmap_fonts[path]
|
|
return None
|
|
|
|
|
|
def render_bitmap_text(font, text, box_width, subscript):
|
|
"""(mask, text width): the line as Draw_Text_Box lays it out, in device pixels.
|
|
|
|
Characters the font lacks are skipped, the last `subscript` drawn ones are halved
|
|
in both directions and sit on the lower half of the line (Stretch_Char), and the
|
|
line stops at the first character that would cross the box's right edge.
|
|
"""
|
|
codes = [ord(ch) for ch in text if ord(ch) in font.glyphs]
|
|
stretch_from = len(codes) - subscript if 0 < subscript <= len(codes) else len(codes)
|
|
layout, width = [], 0
|
|
for n, code in enumerate(codes):
|
|
print_width, x_offset = font.metrics(code)
|
|
stretched = n >= stretch_from
|
|
if stretched:
|
|
print_width, x_offset = print_width >> 1, x_offset >> 1
|
|
if width + print_width > box_width:
|
|
break
|
|
layout.append((code, width + x_offset, stretched))
|
|
width += print_width
|
|
|
|
mask = Image.new("L", (max(1, width), max(1, font.height)))
|
|
for code, x, stretched in layout:
|
|
glyph = font.glyph_image(code)
|
|
_, _, height, _, y_offset, _ = font.glyphs[code]
|
|
if stretched:
|
|
glyph = glyph.resize((max(1, glyph.width >> 1), max(1, glyph.height >> 1)),
|
|
Image.BOX)
|
|
y_offset += height >> 1
|
|
mask.paste(glyph, (x, y_offset), glyph)
|
|
return mask, width
|
|
|
|
|
|
def find_image_file(name):
|
|
"""The local file for a device image name, looked for in Sedona home's image banks."""
|
|
if not name:
|
|
return None
|
|
home = getattr(app_state, "SEDONA_HOME", None)
|
|
if not home:
|
|
return None
|
|
for folder in sorted(glob.glob(os.path.join(home, "ontrol", "imageBank*"))):
|
|
for ext in IMAGE_EXTENSIONS:
|
|
path = os.path.join(folder, name + ext)
|
|
if os.path.isfile(path):
|
|
return path
|
|
return None
|
|
|
|
|
|
def _format_value(fmt, value):
|
|
"""sprintf(fmt, value) for the %f formats NumericLabel carries."""
|
|
parts = parse_numeric_format(fmt)
|
|
if parts is not None:
|
|
prefix, show, precision, suffix = parts
|
|
return f"{prefix}{value:.{precision}f}{suffix}" if show else prefix
|
|
try:
|
|
return fmt % value
|
|
except (TypeError, ValueError):
|
|
return fmt
|
|
|
|
|
|
def _lookup_text(text, index):
|
|
"""Process_Text: prefix + the index'th item + suffix, or '<n>:Error'."""
|
|
prefix, items, suffix = parse_lookup(text)
|
|
if 0 <= index < len(items):
|
|
return f"{prefix}{items[index]}{suffix}"
|
|
return f"{index}:Error"
|
|
|
|
|
|
def _date_text(options, separator):
|
|
"""Today's date as DateLabelSet.prepareDateString lays it out."""
|
|
today = datetime.date.today()
|
|
no_zero = bool(options & DATE_LEADING_ZERO) # the firmware drops the zero when set
|
|
day = str(today.day) if no_zero else f"{today.day:02d}"
|
|
month = str(today.month) if no_zero else f"{today.month:02d}"
|
|
sep = SEPARATORS.get(separator, "/")
|
|
first, second = (month, day) if options & DATE_MONTH_FIRST else (day, month)
|
|
text = f"{first}{sep}{second}"
|
|
if not options & DATE_HIDE_YEAR:
|
|
year = today.year % 100 if options & DATE_TWO_DIGIT_YEAR else today.year
|
|
text += f"{sep}{year:02d}"
|
|
return text
|
|
|
|
|
|
def _time_text(prefix, minutes, am_pm):
|
|
"""drawTimeTextBox with digits 0: prefix + HH:MM, optionally 12-hour."""
|
|
minutes = int(minutes) % 1440
|
|
hour, minute = divmod(minutes, 60)
|
|
if not am_pm:
|
|
return f"{prefix}{hour:02d}:{minute:02d}"
|
|
suffix = " AM" if hour < 12 else " PM"
|
|
hour = 12 if hour == 0 else hour - 12 if hour > 12 else hour
|
|
return f"{prefix}{hour:02d}:{minute:02d}{suffix}"
|
|
|
|
|
|
def _as_float(raw, fallback=0.0):
|
|
try:
|
|
return float(raw)
|
|
except (TypeError, ValueError):
|
|
return fallback
|
|
|
|
|
|
def _bool_state(raw):
|
|
return properties.bool_state(raw)
|
|
|
|
|
|
class WidgetPainter:
|
|
"""Paints one page's widgets; holds the PhotoImages the canvas needs kept alive."""
|
|
|
|
def __init__(self, canvas, origin_x, origin_y, zoom, page_colour):
|
|
self.c = canvas
|
|
self.ox = origin_x
|
|
self.oy = origin_y
|
|
self.z = zoom
|
|
self.page_colour = page_colour
|
|
self.images = []
|
|
|
|
# ── primitives ────────────────────────────────────────────────
|
|
|
|
def _xy(self, x, y):
|
|
return self.ox + x * self.z, self.oy + y * self.z
|
|
|
|
def _shape(self, x, y, w, h, fill, shape, tags):
|
|
x1, y1 = self._xy(x, y)
|
|
x2, y2 = self._xy(x + w, y + h)
|
|
maker = self.c.create_oval if shape else self.c.create_rectangle
|
|
maker(x1, y1, x2, y2, fill=fill, outline="", tags=tags)
|
|
|
|
def text_box(self, m, text, x, y, w, h, text_colour, box_colour, tags,
|
|
attributes=None, shape=None):
|
|
"""A label's box and text.
|
|
|
|
The line box (the font's full height) is centred on the widget box even when
|
|
it is the taller of the two, so the glyphs sit in the box as Workbench shows
|
|
them rather than hanging below a short one.
|
|
"""
|
|
attributes = m.int("attributes", 0x08) if attributes is None else attributes
|
|
shape = m.int("shape") if shape is None else shape
|
|
names = font_names(m) if "fontName" in m.slots else ("Tahoma",)
|
|
family_index = m.int("fontName")
|
|
family = names[family_index] if 0 <= family_index < len(names) else names[0]
|
|
size = m.int("fontSize", 16)
|
|
style = m.int("fontAttributes")
|
|
|
|
if attributes & BACKGROUND_BIT:
|
|
self._shape(x, y, w, h, rgb565_to_hex(box_colour), shape, tags)
|
|
if not text:
|
|
return
|
|
|
|
digits = attributes & SUBSCRIPT_MASK
|
|
bitmap = find_bitmap_font(family, size, style)
|
|
if bitmap is not None:
|
|
self._bitmap_text(bitmap, text, x, y, w, h, attributes, digits, text_colour, tags)
|
|
else:
|
|
self._tk_text(family, size, style, text, x, y, w, h, attributes, digits,
|
|
text_colour, tags)
|
|
|
|
@staticmethod
|
|
def _left_space(attributes, box_width, text_width):
|
|
if box_width <= text_width:
|
|
return 0
|
|
alignment = attributes & ALIGN_MASK
|
|
if alignment == 0x20:
|
|
return box_width - text_width
|
|
if alignment == 0x10:
|
|
return (box_width - text_width) / 2
|
|
return 0
|
|
|
|
def _bitmap_text(self, font, text, x, y, w, h, attributes, digits, text_colour, tags):
|
|
key = (id(font), text, w, digits, text_colour, self.z)
|
|
cached = _text_cache.get(key)
|
|
if cached is None:
|
|
mask, width = render_bitmap_text(font, text, w, digits)
|
|
if width == 0:
|
|
return
|
|
size = (max(1, round(mask.width * self.z)), max(1, round(mask.height * self.z)))
|
|
if size != mask.size:
|
|
# Up close the device's pixels are shown as pixels; below 1:1 they blend.
|
|
mask = mask.resize(size, Image.NEAREST if self.z >= 2 else Image.LANCZOS)
|
|
r, g, b = (int(rgb565_to_hex(text_colour)[i:i + 2], 16) for i in (1, 3, 5))
|
|
img = Image.new("RGBA", mask.size, (r, g, b, 0))
|
|
img.putalpha(mask)
|
|
if len(_text_cache) > TEXT_CACHE_SIZE:
|
|
_text_cache.clear()
|
|
cached = _text_cache[key] = (ImageTk.PhotoImage(img), width)
|
|
photo, width = cached
|
|
self.images.append(photo)
|
|
left = self._left_space(attributes, w, width)
|
|
top = (h - font.height) / 2
|
|
self.c.create_image(*self._xy(x + left, y + top), anchor="nw", image=photo, tags=tags)
|
|
|
|
def _tk_text(self, family, size, style, text, x, y, w, h, attributes, digits,
|
|
text_colour, tags):
|
|
"""The fallback when no device font file is found: a Windows font of the size."""
|
|
z = self.z
|
|
main_font = _font(family, round(size * z), style)
|
|
half_font = _font(family, round(size * z / 2), style)
|
|
split = len(text) - digits if 0 < digits < len(text) else len(text)
|
|
chars = [(ch, main_font if i < split else half_font) for i, ch in enumerate(text)]
|
|
|
|
limit = w * z
|
|
drawn, width = [], 0.0
|
|
for ch, fnt in chars:
|
|
advance = fnt.measure(ch)
|
|
if width + advance > limit + 0.5:
|
|
break
|
|
drawn.append((ch, fnt))
|
|
width += advance
|
|
|
|
bx, by = self._xy(x, y)
|
|
cursor = bx + self._left_space(attributes, limit, width)
|
|
top = by + (h * z - main_font.metrics("linespace")) / 2
|
|
colour = rgb565_to_hex(text_colour)
|
|
main_text = "".join(ch for ch, fnt in drawn if fnt is main_font)
|
|
tail_text = "".join(ch for ch, fnt in drawn if fnt is half_font)
|
|
if main_text:
|
|
self.c.create_text(cursor, top, text=main_text, anchor="nw", fill=colour,
|
|
font=main_font, tags=tags)
|
|
cursor += main_font.measure(main_text)
|
|
if tail_text:
|
|
baseline = top + main_font.metrics("ascent") - half_font.metrics("ascent")
|
|
self.c.create_text(cursor, baseline, text=tail_text, anchor="nw", fill=colour,
|
|
font=half_font, tags=tags)
|
|
|
|
def image_box(self, name, x, y, w, h, tags):
|
|
path = find_image_file(name)
|
|
if not _HAS_PIL:
|
|
self._placeholder(x, y, w, h, name or "image", tags)
|
|
return
|
|
if path is None:
|
|
self._checker(x, y, w, h, tags)
|
|
return
|
|
try:
|
|
img = Image.open(path).convert("RGBA")
|
|
except Exception:
|
|
self._checker(x, y, w, h, tags)
|
|
return
|
|
iw, ih = img.size
|
|
crop_w, crop_h = min(iw, w), min(ih, h)
|
|
img = img.crop((0, 0, crop_w, crop_h))
|
|
left = (w - iw) // 2 if w > iw else 0
|
|
top = (h - ih) // 2 if h > ih else 0
|
|
size = (max(1, round(crop_w * self.z)), max(1, round(crop_h * self.z)))
|
|
if size != img.size:
|
|
img = img.resize(size, Image.LANCZOS)
|
|
photo = ImageTk.PhotoImage(img)
|
|
self.images.append(photo)
|
|
self.c.create_image(*self._xy(x + left, y + top), anchor="nw", image=photo, tags=tags)
|
|
|
|
def _checker(self, x, y, w, h, tags):
|
|
cw, ch = max(1, round(w * self.z)), max(1, round(h * self.z))
|
|
cell = max(1, round(CHECKER_CELL * self.z))
|
|
img = Image.new("RGB", (cw, ch))
|
|
pixels = img.load()
|
|
for py in range(ch):
|
|
row = (py // cell) & 1
|
|
for px in range(cw):
|
|
pixels[px, py] = CHECKER_COLOURS[((px // cell) & 1) ^ row]
|
|
photo = ImageTk.PhotoImage(img)
|
|
self.images.append(photo)
|
|
self.c.create_image(*self._xy(x, y), anchor="nw", image=photo, tags=tags)
|
|
|
|
def _placeholder(self, x, y, w, h, label, tags):
|
|
x1, y1 = self._xy(x, y)
|
|
x2, y2 = self._xy(x + w, y + h)
|
|
self.c.create_rectangle(x1, y1, x2, y2, fill=PLACEHOLDER_FILL,
|
|
outline=PLACEHOLDER_OUTLINE, tags=tags)
|
|
self.c.create_text((x1 + x2) / 2, (y1 + y2) / 2, text=label, fill="#000000",
|
|
font=(FALLBACK_FAMILY, max(7, min(10, int(min(x2 - x1, y2 - y1) // 8)))),
|
|
tags=tags)
|
|
|
|
def page_image(self, name, width, height):
|
|
"""OxPage.imageFileName, drawn over the page colour from the top-left corner."""
|
|
if not name or find_image_file(name) is None:
|
|
return
|
|
self.image_box(name, 0, 0, width, height, ("page_image",))
|
|
|
|
# ── widgets ───────────────────────────────────────────────────
|
|
|
|
def draw(self, widget, tags, short_label):
|
|
"""Paints `widget`; returns its box (x, y, w, h) in device pixels."""
|
|
m = _Model(widget)
|
|
x, y = m.int("x"), m.int("y")
|
|
w, h = max(1, m.int("width", 80)), max(1, m.int("height", 80))
|
|
kind = widget.get("type", "").rsplit("::", 1)[-1]
|
|
|
|
if _bool_state(m.raw("visibility")) == "false":
|
|
x1, y1 = self._xy(x, y)
|
|
x2, y2 = self._xy(x + w, y + h)
|
|
self.c.create_rectangle(x1, y1, x2, y2, outline=HIDDEN_OUTLINE, dash=(3, 3),
|
|
tags=tags)
|
|
return x, y, w, h
|
|
|
|
painter = getattr(self, f"_draw_{kind}", None)
|
|
if painter is None:
|
|
self._placeholder(x, y, w, h, f"{widget.get('name', '?')}\n({short_label})", tags)
|
|
else:
|
|
painter(m, x, y, w, h, tags)
|
|
return x, y, w, h
|
|
|
|
def _draw_PushButtonLabel(self, m, x, y, w, h, tags):
|
|
self.text_box(m, m.text("text"), x, y, w, h, m.int("textColor"),
|
|
m.int("backgroundColor", 0xFF00), tags)
|
|
|
|
def _draw_NumericLabel(self, m, x, y, w, h, tags):
|
|
fmt = m.text("text")
|
|
value = _as_float(m.raw("in"))
|
|
tab = m.int("valueTabPosition")
|
|
split = fmt.find("%")
|
|
if tab == 0 or split <= 0:
|
|
self.text_box(m, _format_value(fmt, value), x, y, w, h, m.int("textColor"),
|
|
m.int("backgroundColor", 0xFF00), tags)
|
|
return
|
|
attributes = m.int("attributes", 0x08)
|
|
self.text_box(m, fmt[:split].replace("%%", "%"), x, y, tab, h, m.int("textColor"),
|
|
m.int("backgroundColor", 0xFF00), tags, shape=0)
|
|
self.text_box(m, _format_value(": " + fmt[split:], value), x + tab, y, w - tab, h,
|
|
m.int("textColor"), m.int("backgroundColor", 0xFF00), tags,
|
|
attributes=attributes & 0xCF, shape=0)
|
|
|
|
_draw_NumericLabelSet = _draw_NumericLabel
|
|
|
|
def _draw_NumericSelectLabel(self, m, x, y, w, h, tags):
|
|
text = m.text("text")
|
|
if not text:
|
|
return
|
|
self.text_box(m, _lookup_text(text, m.int("in")), x, y, w, h, m.int("textColor"),
|
|
m.int("backgroundColor", 0xFF00), tags)
|
|
|
|
def _draw_BooleanLabel(self, m, x, y, w, h, tags):
|
|
state = _bool_state(m.raw("in"))
|
|
if state == "true":
|
|
text, fg, bg = m.text("trueText"), m.int("trueTextColor"), m.int("trueBackgroundColor")
|
|
else:
|
|
text = m.text("falseText") if state == "false" else "Null"
|
|
fg, bg = m.int("falseTextColor"), m.int("falseBackgroundColor")
|
|
attributes = m.int("attributes", 0x08)
|
|
if not text:
|
|
attributes &= 0xF7
|
|
self.text_box(m, text, x, y, w, h, fg, bg, tags, attributes=attributes)
|
|
|
|
def _draw_ScheduleLabelSet(self, m, x, y, w, h, tags):
|
|
state = _bool_state(m.raw("in"))
|
|
if state == "true":
|
|
img, fg, bg, index = (m.text("trueImageFileName"), m.int("trueTextColor"),
|
|
m.int("trueBackgroundColor"), 1)
|
|
else:
|
|
img = m.text("falseImageFileName") if state == "false" else ""
|
|
fg, bg = m.int("falseTextColor"), m.int("falseBackgroundColor")
|
|
index = 0 if state == "false" else 2
|
|
if img:
|
|
self.image_box(img, x, y, w, h, tags)
|
|
elif m.text("text"):
|
|
self.text_box(m, _lookup_text(m.text("text"), index), x, y, w, h, fg, bg, tags)
|
|
|
|
def _draw_DateLabelSet(self, m, x, y, w, h, tags):
|
|
text = _date_text(m.int("dateFormatOptions"), m.int("dateSeparatorCharacter"))
|
|
self.text_box(m, text, x, y, w, h, m.int("textColor"),
|
|
m.int("backgroundColor", 0xFF00), tags)
|
|
|
|
def _draw_TimeLabelSet(self, m, x, y, w, h, tags):
|
|
am_pm = _bool_state(m.raw("amPmFormat")) == "true"
|
|
text = _time_text(m.text("text"), m.int("in"), am_pm)
|
|
self.text_box(m, text, x, y, w, h, m.int("textColor"),
|
|
m.int("backgroundColor", 0xFF00), tags,
|
|
attributes=m.int("attributes", 0x08) & 0xF8)
|
|
|
|
def _draw_Geometry(self, m, x, y, w, h, tags):
|
|
if _bool_state(m.raw("transparent")) != "true":
|
|
self._shape(x, y, w, h, rgb565_to_hex(m.int("backgroundColor", 0xFF00)),
|
|
m.int("shape"), tags)
|
|
|
|
def _draw_CalibrationButton(self, m, x, y, w, h, tags):
|
|
self._shape(x, y, w, h, rgb565_to_hex(m.int("backgroundColor", 0xFF00)),
|
|
m.int("shape"), tags)
|
|
|
|
def _draw_PushButtonImage(self, m, x, y, w, h, tags):
|
|
self.image_box(m.text("imageFileName"), x, y, w, h, tags)
|
|
|
|
def _draw_BooleanImage(self, m, x, y, w, h, tags):
|
|
state = _bool_state(m.raw("in"))
|
|
if state == "null":
|
|
return
|
|
name = m.text("trueImageFileName" if state == "true" else "falseImageFileName")
|
|
self.image_box(name, x, y, w, h, tags)
|
|
|
|
def _draw_NumericSelectImage(self, m, x, y, w, h, tags):
|
|
self.image_box(f"{m.text('imageFileNamePrefix')}{m.int('in')}", x, y, w, h, tags)
|