"""WordClock-Plugin: Deutsche Wort-Uhr im QlockTwo-Stil. 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) └────────────────────────────────────┘ 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 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 …). QlockTwo-Style, ab 1×1 sauber." category = "info" config_schema = [ {"key": "show_date", "label": "Datum anzeigen", "type": "bool", "default": True}, {"key": "show_weekday", "label": "Wochentag anzeigen", "type": "bool", "default": True}, {"key": "accent_color", "label": "Akzentfarbe", "type": "select", "choices": ["accent", "blue", "ok", "warn", "info"], "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 = { "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): 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 # --- Hintergrund --- draw.rectangle((x, y, x + w - 1, y + h - 1), fill=bg) now = datetime.now() pad = 6 inner_w = w - 2 * pad inner_h = h - 2 * pad inner_x = x + pad inner_y = y + pad # ============================================================ # 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 # ============================================================ # 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 # 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) if show_wd and show_dt: header_h = 26 elif show_wd or show_dt: header_h = 18 else: header_h = 0 # Stunden-Block: fest ~36% der inneren Höhe, aber mindestens 50px hour_block_h = max(50, int(inner_h * 0.36)) # 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) 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")