From 7982ce68bf3375948191f38151508d0c62c8123b Mon Sep 17 00:00:00 2001 From: aichristabasco Date: Sat, 21 Feb 2026 15:09:49 +0000 Subject: [PATCH] fix: rewrite overlay with ctk.CTkToplevel for reliable rendering on Windows - Replaced tk.Toplevel + polygon smooth trick with ctk.CTkToplevel - Border glow via simple layered rectangles (works on all Windows configs) - Same visual: dark panel, pulsing red mic dot, 32-bar waveform - Fade in/out preserved via window alpha attribute - History copy button (clipboard icon) was already present in _refresh_history --- voxflow/overlay.py | 246 +++++++++++++++++++++------------------------ 1 file changed, 113 insertions(+), 133 deletions(-) diff --git a/voxflow/overlay.py b/voxflow/overlay.py index 60d8f0b..fc8ea33 100644 --- a/voxflow/overlay.py +++ b/voxflow/overlay.py @@ -1,234 +1,214 @@ -"""VoxFlow Recording Overlay - Bottom-center animated waveform bar. +"""VoxFlow Recording Overlay – bottom-center waveform bar. -Shows only while recording (show/hide called from app.py). -Built by AI Evolution Polska +Uses CTkToplevel so it renders reliably on Windows without +canvas transparency tricks. + +show() – called when recording starts +hide() – called when recording stops +set_level(0-1) – audio level (called from recorder callback) """ -import threading import math +import threading import tkinter as tk +import customtkinter as ctk from typing import Optional class RecordingOverlay: - """Floating overlay that shows recording waveform at bottom-center of screen.""" + """Elegant bottom-centre overlay shown only during recording.""" W = 520 - H = 68 - MARGIN_BOTTOM = 70 + H = 70 + BOTTOM_MARGIN = 80 def __init__(self): - self._root: Optional[tk.Toplevel] = None + self._win: Optional[ctk.CTkToplevel] = None self._canvas: Optional[tk.Canvas] = None self._alive = False self._phase = 0.0 self._level = 0.0 - self._alpha = 0.0 # current opacity (for fade in/out) - self._fading_out = False + self._alpha = 0.0 + self._hiding = False self._parent = None + self._after_id = None # ── Public API ──────────────────────────────────────────────── def show(self, parent=None): - """Show the overlay — called when recording starts.""" + """Make overlay visible – call from main thread.""" if self._alive: return self._parent = parent self._alive = True - self._fading_out = False + self._hiding = False self._alpha = 0.0 if parent: - parent.after(0, self._create_window) + parent.after(0, self._create) def hide(self): - """Hide the overlay — called when recording stops.""" + """Start fade-out – call from main thread.""" self._alive = False - self._fading_out = True + self._hiding = True def set_level(self, level: float): - """Update audio level (0.0–1.0) for waveform animation.""" - self._level = min(1.0, max(0.0, level)) + """Update audio amplitude for waveform (0.0–1.0).""" + self._level = max(0.0, min(1.0, level)) - # ── Window ──────────────────────────────────────────────────── + # ── Window lifecycle ────────────────────────────────────────── - def _create_window(self): + def _create(self): try: - self._root = tk.Toplevel(self._parent) - win = self._root + win = ctk.CTkToplevel(self._parent) + self._win = win + win.overrideredirect(True) win.attributes("-topmost", True) - win.attributes("-alpha", 0.0) # start invisible, fade in + win.attributes("-alpha", 0.0) + win.resizable(False, False) sw = win.winfo_screenwidth() sh = win.winfo_screenheight() x = (sw - self.W) // 2 - y = sh - self.H - self.MARGIN_BOTTOM + y = sh - self.H - self.BOTTOM_MARGIN win.geometry(f"{self.W}x{self.H}+{x}+{y}") - # Match canvas background to panel color — no transparent key needed - win.configure(bg="#0d0b1e") + # Dark panel background + win.configure(fg_color="#0d0b1e") + # Canvas fills entire window – draws waveform + mic dot self._canvas = tk.Canvas( win, width=self.W, height=self.H, - bg="#0d0b1e", highlightthickness=0, + bg="#0d0b1e", highlightthickness=0, bd=0, ) - self._canvas.pack() - self._animate() + self._canvas.pack(fill="both", expand=True) + + self._tick() except Exception as e: - print(f"Overlay error: {e}") + print(f"[Overlay] create error: {e}") - def _destroy_window(self): + def _destroy(self): try: - if self._root: - self._root.destroy() + if self._after_id and self._win: + self._win.after_cancel(self._after_id) except Exception: pass - self._root = None + try: + if self._win: + self._win.destroy() + except Exception: + pass + self._win = None self._canvas = None - self._fading_out = False + self._hiding = False + self._after_id = None + + # ── Animation ───────────────────────────────────────────────── + + def _tick(self): + if not self._win or not self._canvas: + return - # ── Animation loop ──────────────────────────────────────────── + # Fade in / fade out + if self._hiding: + self._alpha = max(0.0, self._alpha - 0.085) + elif self._alpha < 0.94: + self._alpha = min(0.94, self._alpha + 0.07) - def _animate(self): - if not self._canvas or not self._root: + try: + self._win.attributes("-alpha", self._alpha) + except Exception: return - # --- Fade in / fade out --- - if self._fading_out: - self._alpha = max(0.0, self._alpha - 0.08) - try: - self._root.attributes("-alpha", self._alpha) - except Exception: - pass - if self._alpha <= 0.0: - if self._parent: - self._parent.after(0, self._destroy_window) - return - elif self._alpha < 0.93: - self._alpha = min(0.93, self._alpha + 0.07) - try: - self._root.attributes("-alpha", self._alpha) - except Exception: - pass + if self._hiding and self._alpha <= 0.0: + if self._parent: + self._parent.after(0, self._destroy) + return self._phase += 0.11 self._draw() try: - self._root.after(35, self._animate) + self._after_id = self._win.after(35, self._tick) except Exception: pass + # ── Drawing ─────────────────────────────────────────────────── + def _draw(self): c = self._canvas W, H = self.W, self.H c.delete("all") - # ── Border glow (layered rectangles, outer→inner) ──────── - glow_colors = ["#3b1f6a", "#4c2181", "#5d28a0", "#6d28d9"] - radii = [16, 14, 13, 12] - for col, r in zip(glow_colors, radii): - pad = radii[0] - r - self._rrect(c, pad, pad, W - pad, H - pad, r=r, fill="", outline=col, width=2) - - # ── Glass panel fill ────────────────────────────────────── - self._rrect(c, 3, 3, W - 3, H - 3, r=12, - fill="#100e28", outline="") + # ── Background panel with purple border ─────────────────── + # Outer glow (multiple rects, darker to lighter) + for i, col in enumerate(["#2a1060", "#3d1a8a", "#5b21b6", "#7c3aed"]): + pad = 3 - i if i < 3 else 0 + c.create_rectangle(pad, pad, W - pad, H - pad, + fill="", outline=col, width=1) - # Inner top-shine strip - self._rrect(c, 4, 4, W - 4, 4 + H // 3, r=11, - fill="#ffffff0a", outline="") + # Filled dark panel + c.create_rectangle(3, 3, W - 3, H - 3, fill="#100e28", outline="") - # Thin bright border top-cap - self._rrect(c, 3, 3, W - 3, H - 3, r=12, - fill="", outline="#7c3aed", width=1) + # Top inner shine + c.create_rectangle(4, 4, W - 4, 18, fill="#ffffff09", outline="") - # ── Pulsing record dot (left) ───────────────────────────── + # ── Pulsing mic dot (left) ──────────────────────────────── pulse = (math.sin(self._phase * 3.0) + 1) / 2 cx, cy = 38, H // 2 - # Outer glow rings - for ri, col in [(22, "#ef444418"), (16, "#ef444438"), (12, "#ef444470")]: + # Glow rings + for ri, col in ((22, "#ef444415"), (16, "#ef444435"), (11, "#ef444465")): r = ri + pulse * 4 c.create_oval(cx - r, cy - r, cx + r, cy + r, fill=col, outline="") # Core dot - dr = 9 + pulse * 2 + dr = int(9 + pulse * 2) c.create_oval(cx - dr, cy - dr, cx + dr, cy + dr, fill="#ef4444", outline="#fca5a5", width=1) - # Mic icon inside dot - c.create_oval(cx - 3, cy - 6, cx + 3, cy, fill="white", outline="") - c.create_line(cx, cy, cx, cy + 5, fill="white", width=2) - c.create_line(cx - 4, cy + 5, cx + 4, cy + 5, fill="white", width=2) + # Mic icon inside + c.create_oval(cx - 3, cy - 7, cx + 3, cy - 1, fill="white", outline="") + c.create_line(cx, cy - 1, cx, cy + 5, fill="white", width=2) + c.create_arc(cx - 6, cy - 3, cx + 6, cy + 7, + start=0, extent=-180, style="arc", outline="white", width=2) # ── Text ────────────────────────────────────────────────── - c.create_text(62, cy - 9, + c.create_text(62, cy - 10, text="Nagrywam...", fill="#fca5a5", font=("Segoe UI", 11, "bold"), anchor="w") - c.create_text(62, cy + 9, + c.create_text(62, cy + 8, text="VoxFlow \u2022 zwolnij klawisz aby zakonczyc", - fill="#6d5fa0", font=("Segoe UI", 8), + fill="#6b5e9b", font=("Segoe UI", 8), anchor="w") - # Thin separator line - c.create_line(230, 12, 230, H - 12, fill="#3d2d70", width=1) + # Thin vertical divider + c.create_line(228, 14, 228, H - 14, fill="#3d2d6d", width=1) # ── Waveform bars ───────────────────────────────────────── - n_bars = 32 - bw = 4 - gap = 2 - x0 = 238 - max_h = 20 - - for i in range(n_bars): - if x0 + i * (bw + gap) > W - 8: - break - t = self._phase * 2.6 + i * 0.38 - wave = abs(math.sin(t)) * 0.6 + abs(math.sin(t * 0.7 + 1.1)) * 0.4 - amp = (wave * 0.45 + self._level * 0.7) * max_h + 3 - amp = min(amp, max_h) + n = 32 + bw, gap = 4, 2 + x0 = 236 + max_h = 22 + for i in range(n): x = x0 + i * (bw + gap) + if x + bw > W - 6: + break + + t = self._phase * 2.5 + i * 0.40 + wave = abs(math.sin(t)) * 0.6 + abs(math.sin(t * 0.73 + 1.1)) * 0.4 + amp = max(3, min(max_h, (wave * 0.45 + self._level * 0.70) * max_h)) ratio = amp / max_h - if ratio > 0.78: - col = "#f43f5e" - elif ratio > 0.50: - col = "#a78bfa" - elif ratio > 0.28: - col = "#7c3aed" - else: - col = "#4c1d95" - - # Bar body + col = ("#f43f5e" if ratio > 0.78 + else "#a78bfa" if ratio > 0.50 + else "#7c3aed" if ratio > 0.28 + else "#4c1d95") + c.create_rectangle(x, cy - amp, x + bw, cy + amp, fill=col, outline="") - # Rounded top cap - c.create_oval(x - 1, cy - amp - 2, x + bw + 1, cy - amp + 2, + # Round top cap + c.create_oval(x, cy - amp - 2, x + bw, cy - amp + 2, fill=col, outline="") - - # ── Helper ──────────────────────────────────────────────────── - - @staticmethod - def _rrect(canvas, x1, y1, x2, y2, r=12, **kw): - """Draw a rounded rectangle on canvas.""" - # Clamp radius - r = min(r, (x2 - x1) // 2, (y2 - y1) // 2) - points = [ - x1 + r, y1, - x2 - r, y1, - x2, y1, - x2, y1 + r, - x2, y2 - r, - x2, y2, - x2 - r, y2, - x1 + r, y2, - x1, y2, - x1, y2 - r, - x1, y1 + r, - x1, y1, - x1 + r, y1, - ] - canvas.create_polygon(points, smooth=True, **kw)