FIX-WC-01: WordClock Redesign - 3 klare Zonen ohne Überlappung
Vorher: 11x8-Grid mit starrem Layout, viele Buchstaben-Cluster überlappten sich gegenseitig, Stunden-Pixel-Font war unleserlich klein/groß gemischt. Nachher: QlockTwo-inspiriertes Layout in 3 diskreten Zonen: - Header (Wochentag + Datum, accent, klein) - Wort-Grid (ES IST + Minuten-Phrase in einer sauberen Zeile) - Stunden-Block (Stunde in großer Aldrich-Schrift) Plus: - Minuten-Dots oben rechts (QlockTwo-typisch: ●●●● für Sektor-Offset) - Bug-Fix: font_key-Fallback überspringt 'clock'-Pixel-Font - Bug-Fix: 12/24h Stunden-Mapping für Wortuhr (16:30 = 'halb fünf' → FÜNF) - Neue Option 'show_es_ist' zum Ein-/Ausblenden des 'ES IST'-Prefixes - Slot-size-aware: 1x1 single-line, 2x2/4x4 zones, small fallback < 240x140 Verifiziert auf Pi-Display: 'VIERTEL NACH VIER' / 'HALB FÜNF' rendern sauber.
This commit is contained in:
+260
-174
@@ -1,47 +1,84 @@
|
||||
"""WordClock-Plugin:Deutsche Wort-Uhr (QWATCHLayout).
|
||||
"""WordClock-Plugin: Deutsche Wort-Uhr im QlockTwo-Stil.
|
||||
|
||||
Reines Wort-Uhr-Display im deutschen Stil:
|
||||
ES IST <Fünf/Zehn/Viertel/Zwanzig> <Minuten> <nach/vor> <Stunde>
|
||||
Layout in 3 klar getrennten Zonen:
|
||||
┌────────────────────────────────────┐
|
||||
│ Header: Wochentag · Datum │ (accent-farbe, klein)
|
||||
├────────────────────────────────────┤
|
||||
│ │
|
||||
│ Wort-Grid: ES IST │
|
||||
│ ──── ZEHN NACH ──── │ (eine zentrale Zeile,
|
||||
│ DREI │ je nach Minute)
|
||||
│ │
|
||||
├────────────────────────────────────┤
|
||||
│ Stunden gross: DREI │ (Aldrich, riesig)
|
||||
└────────────────────────────────────┘
|
||||
|
||||
Unterstützt auch ENGLISCH (US-Layout).
|
||||
|
||||
Layout: fester 4×4-Zeichen-Grid im Quadrat (4×4 Zellen = 800×480 Display).
|
||||
Jede Zelle = 200×120px → Grid = 800×480px.
|
||||
Wir nutzen 11×8 "字符-Zellen" pro Grid (72×60px pro Zeichen).
|
||||
|
||||
Minuten-Zeilen (oberer Block):
|
||||
Zeile 0: [E][S][ ][I][S][T]
|
||||
Zeile 1: [F][Ü][N][F][Z][E][H][N][Z][W][A][N]
|
||||
Zeile 2: [V][I][E][R][T][E][L][Z][W][A][N][Z]
|
||||
Zeile 3: [N][U][L][L]
|
||||
Zeile 4: [N][A][C][H][ ][V][O][R][ ][H][A][L]
|
||||
Zeile 5: [B][ ]
|
||||
Zeile 6: [S][P][R][A][C][H][E]
|
||||
|
||||
Stunden (unterer Block, je 2×2 Zellen für die Ziffern):
|
||||
DieZiffern werden als gefüllte Rechtecke in der unteren Reihe gerendert.
|
||||
|
||||
Das Layout wird intern gecacht bis sich die Minute ändert.
|
||||
Die Minuten-Wörter sind in einer DYNAMISCHEN Zeile, nicht in einem starren
|
||||
11×8-Grid. Das vermeidet Überlappungen und sieht auf jedem Slot gleich aus.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os, sys
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from plugins.base import Widget
|
||||
from palette import FG, BG, OK, BLUE, YELLOW, ORANGE, fill_for, measure
|
||||
|
||||
|
||||
# ---- Deutsche Minuten-Phrasen (QlockTwo-konform) ----
|
||||
# Schlüssel: Sektor-Untergrenze (5, 10, ..., 55). Wert: (minute_words, hour_offset)
|
||||
# hour_offset = +1 bedeutet wir gehen zur nächsten Stunde ("VOR")
|
||||
DE_PHRASES = {
|
||||
0: ("", 0), # ES IST genau
|
||||
5: ("FÜNF NACH", 0), # 5 nach
|
||||
10: ("ZEHN NACH", 0), # 10 nach
|
||||
15: ("VIERTEL NACH", 0), # viertel nach
|
||||
20: ("ZWANZIG NACH", 0), # 20 nach
|
||||
25: ("FÜNF VOR HALB", 1), # 5 vor halb → 0:30
|
||||
30: ("HALB", 1), # halb → 0:30 (zählt zur nächsten Stunde)
|
||||
35: ("FÜNF NACH HALB",1), # 5 nach halb
|
||||
40: ("ZWANZIG VOR", 1), # 20 vor
|
||||
45: ("VIERTEL VOR", 1), # viertel vor
|
||||
50: ("ZEHN VOR", 1), # 10 vor
|
||||
55: ("FÜNF VOR", 1), # 5 vor
|
||||
}
|
||||
|
||||
# Minuten → Sektor (0, 5, 10, ... 55)
|
||||
def _minute_sector(m: int) -> int:
|
||||
return (m // 5) * 5
|
||||
|
||||
|
||||
# Stunden-Namen (QlockTwo). Index 0..11 = Wortuhr-Stunden (1..12 Uhr).
|
||||
# 4 PM wird "VIER" (deutsch), 5 PM = "FÜNF", etc.
|
||||
# Daher: hour 16..23 → DE_HOURS[16%12]=DE_HOURS[4]="VIER". Korrekt.
|
||||
DE_HOURS = [
|
||||
"ZWÖLF", "EINS", "ZWEI", "DREI", "VIER", "FÜNF",
|
||||
"SECHS", "SIEBEN", "ACHT", "NEUN", "ZEHN", "ELF",
|
||||
]
|
||||
|
||||
|
||||
def _hour_name(h24: int, offset: int) -> str:
|
||||
"""Liefert Stunden-Name. offset=+1 → nächste volle Stunde.
|
||||
|
||||
Beispiel: h24=16, offset=0 → DE_HOURS[16%12=4]="VIER" (4 PM)
|
||||
h24=16, offset=1 → DE_HOURS[(16+1)%12=5]="FÜNF" (halb 5 = 5 PM)
|
||||
"""
|
||||
h = (h24 + offset) % 24
|
||||
return DE_HOURS[h % 12]
|
||||
|
||||
|
||||
def _minute_phrase(m: int):
|
||||
"""Returns (phrase_text, hour_offset) für die aktuelle Minute."""
|
||||
sector = _minute_sector(m)
|
||||
return DE_PHRASES[sector]
|
||||
|
||||
|
||||
class Widget(Widget):
|
||||
name = "clock_wordclock"
|
||||
label = "WordClock"
|
||||
description = "Deutsche Wort-Uhr (ES IST …). Nur für 2×2 oder größer."
|
||||
description = "Deutsche Wort-Uhr (ES IST …). QlockTwo-Style, ab 1×1 sauber."
|
||||
category = "info"
|
||||
|
||||
config_schema = [
|
||||
{"key": "lang", "label": "Sprache",
|
||||
"type": "select", "choices": ["de", "en"], "default": "de",
|
||||
"help": "de = Deutsch (Standard), en = Englisch"},
|
||||
{"key": "show_date", "label": "Datum anzeigen",
|
||||
"type": "bool", "default": True},
|
||||
{"key": "show_weekday", "label": "Wochentag anzeigen",
|
||||
@@ -49,176 +86,225 @@ class Widget(Widget):
|
||||
{"key": "accent_color", "label": "Akzentfarbe",
|
||||
"type": "select",
|
||||
"choices": ["accent", "blue", "ok", "warn", "info"],
|
||||
"default": "accent"},
|
||||
"default": "accent",
|
||||
"help": "Farbe für Wochentag + Datum"},
|
||||
{"key": "invert", "label": "Invertiert (dunkel)",
|
||||
"type": "bool", "default": False},
|
||||
{"key": "show_es_ist", "label": "'ES IST' anzeigen",
|
||||
"type": "bool", "default": True,
|
||||
"help": "QlockTwo-Klassiker. Aus = nur Minuten-Text + Stunde."},
|
||||
]
|
||||
default_config = {
|
||||
"lang": "de", "show_date": True, "show_weekday": True,
|
||||
"show_date": True, "show_weekday": True,
|
||||
"accent_color": "accent", "invert": False,
|
||||
"show_es_ist": True,
|
||||
}
|
||||
|
||||
def fetch(self):
|
||||
return {}
|
||||
|
||||
def render(self, draw, fonts, x: int, y: int, w: int, h: int):
|
||||
from palette import fill_for
|
||||
now = datetime.now()
|
||||
m = now.minute
|
||||
h_ = now.hour
|
||||
|
||||
invert = self.cfg("invert", False)
|
||||
accent = fill_for(self.cfg("accent_color", "accent"))
|
||||
fg = BG if invert else FG
|
||||
bg = FG if invert else BG
|
||||
|
||||
pad = 8
|
||||
|
||||
# Mindestgröße: 300px-breit, 240px-hoch für WordClock
|
||||
if w < 300 or h < 240:
|
||||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=bg)
|
||||
draw.text((x + pad, y + pad),
|
||||
"WordClock\n(min 2×2)",
|
||||
font=fonts.get("20", fonts.get("default")), fill=fg)
|
||||
return
|
||||
|
||||
# ---- Wort-Uhr Grid ----
|
||||
# 4×4 Zellen → 800×480px
|
||||
# Wir rendern in ein 11×8 Zeichen-Grid
|
||||
chars_x, chars_y = 11, 8
|
||||
char_w = w // chars_x
|
||||
char_h = h // chars_y
|
||||
font_size = min(char_w, char_h) * 2 // 3
|
||||
font_key = str(font_size)
|
||||
if font_key not in fonts:
|
||||
font_key = str(max(16, min(fonts.keys(), key=lambda k: abs(int(k) - font_size) if k.isdigit() else 9999)) if fonts else "20")
|
||||
fnt = fonts.get(font_key, fonts.get("20", fonts.get("default")))
|
||||
|
||||
def _draw_char(cx, cy, char, color):
|
||||
"""Zeichnet ein Zeichen an Gitterposition (cx, cy)."""
|
||||
px = x + cx * char_w
|
||||
py = y + cy * char_h
|
||||
tw, th = measure(draw, char, fnt)
|
||||
draw.text((px + (char_w - tw) // 2, py + (char_h - th) // 2),
|
||||
char, font=fnt, fill=color)
|
||||
|
||||
def _fill_char(cx, cy, color):
|
||||
"""Füllt eine Gitterzelle mit einer Farbe (z.B. für Stunden-Dots)."""
|
||||
px = x + cx * char_w
|
||||
py = y + cy * char_h
|
||||
draw.rectangle((px + 2, py + 2, px + char_w - 3, py + char_h - 3), fill=color)
|
||||
|
||||
def _lit(cx, cy):
|
||||
_draw_char(cx, cy, LAYOUT_DE[cy][cx], fg)
|
||||
|
||||
def _dim(cx, cy):
|
||||
_draw_char(cx, cy, LAYOUT_DE[cy][cx], (150, 150, 150))
|
||||
|
||||
# ---- Minuten-Logik (Deutsch) ----
|
||||
def lit_minute(m):
|
||||
"""Sektor der Minuten: 0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-44, 45-49, 50-54, 55-59."""
|
||||
if m < 5:
|
||||
return []
|
||||
elif m < 10:
|
||||
return [(0, 1), (1, 1), (2, 1), (3, 1)] # FÜNF
|
||||
elif m < 15:
|
||||
return [(5, 1), (6, 1), (7, 1), (8, 1)] # ZEHN
|
||||
elif m < 20:
|
||||
return [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (6, 2)] # VIERTEL
|
||||
elif m < 25:
|
||||
return [(7, 2), (8, 2), (9, 2), (10, 2), (7, 3), (8, 3), (9, 3), (10, 3)] # ZWANZIG
|
||||
elif m < 30:
|
||||
return [(0, 4), (1, 4), (2, 4), (3, 4)] # NACH
|
||||
elif m < 35:
|
||||
return [(0, 1), (1, 1), (2, 1), (3, 1), # FÜNF
|
||||
(4, 4), (5, 4), (6, 4)] # + HALB
|
||||
elif m < 40:
|
||||
return [(4, 4), (5, 4), (6, 4)] # HALB
|
||||
elif m < 45:
|
||||
return [(7, 3), (8, 3), (9, 3), (10, 3)] # ZWANZIG
|
||||
elif m < 50:
|
||||
return [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (6, 2), # VIERTEL
|
||||
(7, 4), (8, 4), (9, 4)] # + VOR
|
||||
elif m < 55:
|
||||
return [(5, 1), (6, 1), (7, 1), (8, 1)] # ZEHN + VOR
|
||||
else:
|
||||
return [(0, 1), (1, 1), (2, 1), (3, 1), # FÜNF + VOR
|
||||
(7, 4), (8, 4), (9, 4)]
|
||||
|
||||
def hour_name(h, past_half):
|
||||
"""Gibt die Stunde zurück für die Wortuhr."""
|
||||
DE_HOURS = [
|
||||
"ZWÖLF", "EINS", "ZWEI", "DREI", "VIER",
|
||||
"FÜNF", "SECHS", "SIEBEN", "ACHT", "NEUN",
|
||||
"ZEHN", "ELF", "ZWÖLF"
|
||||
]
|
||||
if past_half:
|
||||
h = (h + 1) % 24
|
||||
if h == 0:
|
||||
return "ZWÖLF"
|
||||
return DE_HOURS[h % 12]
|
||||
|
||||
past_half = m >= 20
|
||||
h_display = hour_name(h_, past_half)
|
||||
lit_cells = lit_minute(m)
|
||||
|
||||
# ---- Render ----
|
||||
# Hintergrund
|
||||
# --- Hintergrund ---
|
||||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=bg)
|
||||
|
||||
# "ES IST" immer lit in Spalte 0
|
||||
_lit(0, 0); _lit(1, 0); _lit(3, 0); _lit(4, 0)
|
||||
now = datetime.now()
|
||||
pad = 6
|
||||
inner_w = w - 2 * pad
|
||||
inner_h = h - 2 * pad
|
||||
inner_x = x + pad
|
||||
inner_y = y + pad
|
||||
|
||||
# Minuten-Wörter
|
||||
for (cx, cy) in lit_cells:
|
||||
_lit(cx, cy)
|
||||
# ============================================================
|
||||
# Sehr kleine Slots (1×1 = 200×120): kompakter Single-Line Modus
|
||||
# ============================================================
|
||||
if w < 240 or h < 140:
|
||||
# "ES IST DREI" oder "VIERTEL NACH DREI" als eine Zeile
|
||||
phrase, offset = _minute_phrase(now.minute)
|
||||
hour_str = _hour_name(now.hour, offset)
|
||||
if phrase:
|
||||
text = f"{phrase} {hour_str}" if self.cfg("show_es_ist") else f"{phrase} {hour_str}"
|
||||
else:
|
||||
text = f"ES IST {hour_str}" if self.cfg("show_es_ist") else hour_str
|
||||
font = _fit_font(draw, fonts, text, inner_w, inner_h, prefer=["20","16","default"])
|
||||
from palette import centered_text
|
||||
centered_text(draw, text, inner_x, inner_y, inner_w, inner_h, font, fg)
|
||||
return
|
||||
|
||||
# "VOR" und "NACH" (Zeile 4)
|
||||
if 5 <= m < 30:
|
||||
_lit(0, 4); _lit(1, 4); _lit(2, 4); _lit(3, 4) # NACH
|
||||
elif m >= 35 and m < 55:
|
||||
_lit(7, 4); _lit(8, 4); _lit(9, 4) # VOR
|
||||
# ============================================================
|
||||
# Standard-Layout: 3 Zonen
|
||||
# ============================================================
|
||||
# Höhenverteilung:
|
||||
# Header: bis zu 28px (oder weniger wenn beides aus)
|
||||
# Grid: Rest minus Stunden
|
||||
# Stunden: grosse Schrift, ~30% der Höhe
|
||||
|
||||
# Rest dim
|
||||
for row in range(chars_y):
|
||||
for col in range(chars_x):
|
||||
if (col, row) not in lit_cells and not (row == 0 and col in (0, 1, 3, 4)):
|
||||
_dim(col, row)
|
||||
# Header-Höhe dynamisch: nur soviel wie gebraucht
|
||||
show_wd = self.cfg("show_weekday", True)
|
||||
show_dt = self.cfg("show_date", True)
|
||||
show_es = self.cfg("show_es_ist", True)
|
||||
|
||||
# ---- Stunden-Balken unten ----
|
||||
# Zeile 6+7: Stunden-Name in großen Buchstaben unten zentriert
|
||||
hour_str = h_display
|
||||
hour_font_size = min(w // len(hour_str), h // 4) * 3 // 4
|
||||
hf = fonts.get(str(hour_font_size), fonts.get("60", fonts.get("default")))
|
||||
tw, th = measure(draw, hour_str, hf)
|
||||
hour_y = y + h - th - pad
|
||||
draw.text((x + (w - tw) // 2, hour_y), hour_str, font=hf, fill=fg)
|
||||
if show_wd and show_dt:
|
||||
header_h = 26
|
||||
elif show_wd or show_dt:
|
||||
header_h = 18
|
||||
else:
|
||||
header_h = 0
|
||||
|
||||
# ---- Datum + Wochentag ----
|
||||
if self.cfg("show_date", True):
|
||||
date_str = now.strftime("%d. %b %Y")
|
||||
df = fonts.get("16", fonts.get("default"))
|
||||
dw, dh = measure(draw, date_str, df)
|
||||
draw.text((x + (w - dw) // 2, y + pad), date_str, font=df, fill=accent)
|
||||
# Stunden-Block: fest ~36% der inneren Höhe, aber mindestens 50px
|
||||
hour_block_h = max(50, int(inner_h * 0.36))
|
||||
|
||||
if self.cfg("show_weekday", True):
|
||||
day_str = now.strftime("%A").upper()
|
||||
df = fonts.get("14", fonts.get("default"))
|
||||
dw, dh = measure(draw, day_str, df)
|
||||
draw.text((x + (w - dw) // 2, y + pad + (18 if self.cfg("show_date", True) else 0)),
|
||||
day_str, font=df, fill=accent)
|
||||
# Wort-Block = Rest
|
||||
grid_h = inner_h - header_h - hour_block_h - 6 # 6px gap
|
||||
if grid_h < 40:
|
||||
# Slot zu klein für alles — Stunden-Anteil reduzieren
|
||||
hour_block_h = max(36, int(inner_h * 0.28))
|
||||
grid_h = inner_h - header_h - hour_block_h - 6
|
||||
|
||||
grid_y = inner_y + header_h
|
||||
hour_y = grid_y + grid_h + 6
|
||||
|
||||
# --- Header (Wochentag / Datum) ---
|
||||
cy = inner_y
|
||||
header_font = _fit_font(draw, fonts, "Mittwoch · 31. Aug 2026", inner_w, header_h or 18,
|
||||
prefer=["16", "20", "default"])
|
||||
if header_h:
|
||||
if show_wd and show_dt:
|
||||
day = now.strftime("%A")
|
||||
date = now.strftime("%e. %b %Y").strip()
|
||||
text = f"{day} · {date}"
|
||||
tw, th = measure(draw, text, header_font)
|
||||
draw.text((inner_x + (inner_w - tw) // 2, cy),
|
||||
text, font=header_font, fill=accent)
|
||||
elif show_wd:
|
||||
text = now.strftime("%A")
|
||||
tw, _ = measure(draw, text, header_font)
|
||||
draw.text((inner_x + (inner_w - tw) // 2, cy),
|
||||
text, font=header_font, fill=accent)
|
||||
elif show_dt:
|
||||
text = now.strftime("%e. %b %Y").strip()
|
||||
tw, _ = measure(draw, text, header_font)
|
||||
draw.text((inner_x + (inner_w - tw) // 2, cy),
|
||||
text, font=header_font, fill=accent)
|
||||
|
||||
# --- Wort-Grid: zwei Zeilen ---
|
||||
# Zeile 1 (oben): "ES IST" (klein, accent) — nur wenn show_es_ist
|
||||
# Zeile 2 (mittig, gross): Minuten-Phrase
|
||||
phrase, offset = _minute_phrase(now.minute)
|
||||
|
||||
# Wie viele Zeilen brauchen wir im Grid?
|
||||
# Wenn ES IST → ES IST (Zeile 1 klein) + Phrase (Zeile 2 gross)
|
||||
# Wenn nicht → nur Phrase (eine Zeile)
|
||||
# Bei sehr grossen Slots könnte man auch stacked machen, aber single-line
|
||||
# ist klarer.
|
||||
|
||||
if show_es and phrase:
|
||||
# Zwei-Zeilen-Layout im Grid
|
||||
line1_h = int(grid_h * 0.30)
|
||||
line2_h = grid_h - line1_h - 2
|
||||
es_font = _fit_font(draw, fonts, "ES IST", inner_w, line1_h,
|
||||
prefer=["20", "16", "default"])
|
||||
draw.text((inner_x + (inner_w - measure(draw, "ES IST", es_font)[0]) // 2,
|
||||
grid_y),
|
||||
"ES IST", font=es_font, fill=accent)
|
||||
# Minuten-Phrase in der unteren, größeren Zeile
|
||||
phrase_font = _fit_font(draw, fonts, phrase, inner_w, line2_h,
|
||||
prefer=["48", "36", "32", "28", "24", "20", "default"])
|
||||
tw, th = measure(draw, phrase, phrase_font)
|
||||
draw.text((inner_x + (inner_w - tw) // 2,
|
||||
grid_y + line1_h + (line2_h - th) // 2),
|
||||
phrase, font=phrase_font, fill=fg)
|
||||
else:
|
||||
# Eine-Zeilen-Layout: nur die Phrase (oder "ES IST DREI" wenn keine Phrase)
|
||||
if not phrase:
|
||||
phrase = "ES IST" if show_es else ""
|
||||
phrase_font = _fit_font(draw, fonts, phrase, inner_w, grid_h,
|
||||
prefer=["60", "48", "36", "32", "28", "24", "20", "default"])
|
||||
tw, th = measure(draw, phrase, phrase_font)
|
||||
draw.text((inner_x + (inner_w - tw) // 2,
|
||||
grid_y + (grid_h - th) // 2),
|
||||
phrase, font=phrase_font, fill=fg)
|
||||
|
||||
# --- Stunden-Block (gross, unten) ---
|
||||
hour_str = _hour_name(now.hour, offset)
|
||||
hour_font = _fit_font(draw, fonts, hour_str, inner_w, hour_block_h,
|
||||
prefer=["100", "80", "60", "48", "36", "32", "default"])
|
||||
# Robuste Font-Wahl: lieber kleiner als abgeschnitten
|
||||
tw, th = measure(draw, hour_str, hour_font)
|
||||
if tw > inner_w:
|
||||
# noch kleiner probieren
|
||||
for k in ["80", "60", "48", "36", "32", "28", "24", "20"]:
|
||||
if k not in fonts:
|
||||
continue
|
||||
cand = fonts[k]
|
||||
cw, ch = measure(draw, hour_str, cand)
|
||||
if cw <= inner_w and ch <= hour_block_h:
|
||||
hour_font = cand
|
||||
tw, th = cw, ch
|
||||
break
|
||||
draw.text((inner_x + (inner_w - tw) // 2,
|
||||
hour_y + (hour_block_h - th) // 2),
|
||||
hour_str, font=hour_font, fill=fg)
|
||||
|
||||
# --- Minuten-Dots (4 Dots oben rechts, wie eine echte QlockTwo) ---
|
||||
# Zeigen die genauen Minuten-Module: ● ● ● ● ● — einer pro 1-2 Min
|
||||
# Wenn minute < 5: leer; bei 25: ●; bei 26: ●●; etc.
|
||||
if inner_w >= 200:
|
||||
self._draw_minute_dots(draw, inner_x, inner_y, inner_w, header_h, fg, accent)
|
||||
|
||||
|
||||
# ---- Deutsches WordClock-Layout (11×8) ----
|
||||
# Jede Position ist ein Zeichen das gerendert wird.
|
||||
# ' ' = Leerzeichen, rest = Buchstabe.
|
||||
LAYOUT_DE = [
|
||||
["E", "S", " ", "I", "S", "T", " ", " ", " ", " ", " "],
|
||||
["F", "Ü", "N", "F", " ", "Z", "E", "H", "N", " ", " "],
|
||||
["V", "I", "E", "R", "T", "E", "L", " ", "Z", "W", "A"],
|
||||
["N", "U", "L", "L", " ", "Z", "W", "A", "N", "Z", " "],
|
||||
["N", "A", "C", "H", " ", "V", "O", "R", " ", "H", "A"],
|
||||
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
|
||||
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
|
||||
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
|
||||
]
|
||||
def _draw_minute_dots(self, draw, x, y, w, header_h, fg, accent):
|
||||
"""Vier Minuten-Dots (●) in der Ecke, analog zu echten QlockTwo-Uhren.
|
||||
|
||||
Sie zeigen: ●=1, ●●=2, ●●●=3, ●●●●=4 Minuten innerhalb des 5-Min-Sektors.
|
||||
"""
|
||||
m = datetime.now().minute
|
||||
sector_m = m % 5 # 0..4
|
||||
if sector_m == 0:
|
||||
return # exakt auf 5-Min-Marke → keine Dots
|
||||
|
||||
# Position: oben rechts, in der Header-Zone (oder oben falls kein Header)
|
||||
dot_size = 3
|
||||
spacing = 4
|
||||
total_w = sector_m * (dot_size + spacing) - spacing
|
||||
start_x = x + w - total_w
|
||||
# Position: am unteren Rand des Header-Bereichs
|
||||
if header_h >= 26:
|
||||
dot_y = y + header_h - dot_size - 2
|
||||
elif header_h > 0:
|
||||
dot_y = y + header_h - dot_size
|
||||
else:
|
||||
# kein Header → dots ganz oben, klein
|
||||
dot_y = y + 2
|
||||
|
||||
for i in range(sector_m):
|
||||
cx = start_x + i * (dot_size + spacing)
|
||||
# Eckige Punkte (Mini-Quadrate) statt runder, weil runde auf dem
|
||||
# ePaper oft Matsch produzieren
|
||||
draw.rectangle((cx, dot_y, cx + dot_size - 1, dot_y + dot_size - 1),
|
||||
fill=fg)
|
||||
|
||||
|
||||
def _fit_font(draw, fonts, text, max_w, max_h, prefer=None):
|
||||
"""Wählt den größten Font aus `prefer`, dessen Text in max_w × max_h passt.
|
||||
|
||||
Robuster als die alte Logik — überspringt 'clock' (Pixel-Font) automatisch.
|
||||
"""
|
||||
if prefer is None:
|
||||
prefer = ["60", "48", "36", "32", "28", "24", "20", "16", "default"]
|
||||
for key in prefer:
|
||||
f = fonts.get(key)
|
||||
if f is None:
|
||||
continue
|
||||
tw, th = measure(draw, text, f)
|
||||
if tw <= max_w and th <= max_h:
|
||||
return f
|
||||
# Fallback: das Kleinste
|
||||
for key in reversed(prefer):
|
||||
if key in fonts:
|
||||
return fonts[key]
|
||||
return fonts.get("default") or fonts.get("20")
|
||||
Reference in New Issue
Block a user