""" Shared UI helpers for the Prole installer screens. These routines are intentionally lightweight wrappers around the existing Tk Canvas used by the installer, to reduce clutter in install.py. """ from __future__ import annotations import tkinter as tk from tkinter import ttk import platform def render_title(app, text: str, y: int = 40): """Render a section title on the main background canvas.""" if getattr(app, 'bg_canvas', None) is None: return item = app.bg_canvas.create_text(48, y, anchor='nw', text=text, fill='black', font=('SF Pro Text', 18, 'bold')) app._canvas_items.append(item) def render_paragraph(app, text: str, y: int, wrap: int = 800): """Render a paragraph on the main background canvas.""" if getattr(app, 'bg_canvas', None) is None: return item = app.bg_canvas.create_text(48, y, anchor='nw', text=text, fill='black', font=('SF Pro Text', 11), width=wrap) app._canvas_items.append(item) def canvas_text(app, x: int, y: int, text: str, *, fill: str = 'black', font: tuple = ('SF Pro Text', 11), anchor: str = 'nw', width: int | None = None, justify: str | None = None, state: str | None = None) -> int: """Create a text item on the app's main canvas and track it. Returns the created canvas item id. """ if getattr(app, 'bg_canvas', None) is None: return -1 kwargs = dict(anchor=anchor, text=text, fill=fill, font=font) if width is not None: kwargs['width'] = width if justify is not None: kwargs['justify'] = justify if state is not None: kwargs['state'] = state item = app.bg_canvas.create_text(x, y, **kwargs) app._canvas_items.append(item) return item def canvas_oval(app, x1: int, y1: int, x2: int, y2: int, *, fill: str | None = None, outline: str | None = None, width: int = 1, state: str | None = None) -> int: """Create an oval on the app's main canvas and track it.""" if getattr(app, 'bg_canvas', None) is None: return -1 kwargs = dict(fill=fill or '', outline=outline or '', width=width) if state is not None: kwargs['state'] = state item = app.bg_canvas.create_oval(x1, y1, x2, y2, **kwargs) app._canvas_items.append(item) return item def canvas_rectangle(app, x1: int, y1: int, x2: int, y2: int, *, outline: str = '#6e6e73', width: int = 1, fill: str | None = None, state: str | None = None) -> int: if getattr(app, 'bg_canvas', None) is None: return -1 kwargs = dict(outline=outline, width=width, fill=fill or '') if state is not None: kwargs['state'] = state item = app.bg_canvas.create_rectangle(x1, y1, x2, y2, **kwargs) app._canvas_items.append(item) return item def canvas_line(app, x1: int, y1: int, x2: int, y2: int, *, fill: str = 'black', width: int = 2, state: str | None = None) -> int: if getattr(app, 'bg_canvas', None) is None: return -1 kwargs = dict(fill=fill, width=width) if state is not None: kwargs['state'] = state item = app.bg_canvas.create_line(x1, y1, x2, y2, **kwargs) app._canvas_items.append(item) return item def render_link(app, x: int, y: int, text: str, *, color: str = '#0a84ff', font: tuple = ('Helvetica', 12, 'underline')) -> int: """Render a link-styled text on canvas and return its item id.""" return canvas_text(app, x, y, text, fill=color, font=font) def canvas_image(app, x: int, y: int, image, *, anchor: str = 'center') -> int: """Create an image on the app's main canvas. Not tracked in _canvas_items by default.""" if getattr(app, 'bg_canvas', None) is None: return -1 try: item = app.bg_canvas.create_image(x, y, anchor=anchor, image=image) return item except Exception: return -1 # Explicit-canvas helpers for non-background canvases def canvas_delete(app, item_id: int): try: app.bg_canvas.delete(item_id) except Exception: pass def canvas_clear_all(app): try: app.bg_canvas.delete('all') except Exception: pass def canvas_coords(app, item_id: int, *coords): try: app.bg_canvas.coords(item_id, *coords) except Exception: pass def canvas_clear(cnv: tk.Canvas): try: cnv.delete('all') except Exception: pass def canvas_text_on(cnv: tk.Canvas, x: int, y: int, text: str, *, fill: str = '#1d1d1f', font: tuple = ('Helvetica', 12), anchor: str = 'center') -> int: return cnv.create_text(x, y, text=text, fill=fill, font=font, anchor=anchor) def canvas_oval_on(cnv: tk.Canvas, x1: int, y1: int, x2: int, y2: int, *, fill: str | None = None, outline: str | None = None, width: int = 1) -> int: return cnv.create_oval(x1, y1, x2, y2, fill=fill or '', outline=outline or '', width=width) def canvas_line_on(cnv: tk.Canvas, x1: int, y1: int, x2: int, y2: int, *, fill: str = 'white', width: int = 2) -> int: return cnv.create_line(x1, y1, x2, y2, fill=fill, width=width) def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int, callable] | None = None, style_name: str = 'Nav.TButton') -> dict[int, tk.Button]: """Create a right-aligned navigation footer with uniform button styling. Uses tk.Button instead of ttk.Button for better color control on macOS. """ footer = tk.Frame(parent, bg='#F5F5DC', height=64) footer.pack(fill='x', side='bottom') footer.pack_propagate(False) # Top divider line for the footer divider = tk.Frame(footer, bg='#CCCCCC', height=1) divider.pack(side='top', fill='x') # Flexible spacer to push buttons to the right spacer = tk.Frame(footer, bg='#F5F5DC') spacer.pack(side='left', expand=True, fill='x') cmds = commands or {} btn_map: dict[int, tk.Button] = {} for btn_id, title in buttons: cmd = cmds.get(btn_id) # Use tk.Button for full control over background and borders on macOS b = tk.Button(footer, text=title, command=cmd, bg='#F5F5DC', fg='black', activebackground='#E5E5D5', activeforeground='black', highlightbackground='#F5F5DC', # Essential for macOS to avoid black boxes highlightthickness=0, relief='flat', font=('SF Pro Text', 11), padx=16, pady=8) # Right-aligned order (pack to the right in the declared order) pad = (0, 20) if title.lower() in ('finish', 'exit', 'done', 'next') else (0, 8) b.pack(side='right', padx=pad, pady=12) btn_map[btn_id] = b # Return both the frame and button map if needed later by callers btn_map['_footer'] = footer # type: ignore[index] return btn_map class TerminalConsole(ttk.Frame): """A scrollable text widget that mimics a terminal console.""" def __init__(self, parent, **kwargs): super().__init__(parent, **kwargs) self.text = tk.Text(self, bg='#1e1e1e', fg='#f0f0f0', font=('Menlo', 11) if 'Darwin' in platform.system() else ('Consolas', 11), padx=10, pady=10, insertbackground='white') self.scroll = ttk.Scrollbar(self, orient='vertical', command=self.text.yview) self.text.configure(yscrollcommand=self.scroll.set) self.scroll.pack(side='right', fill='y') self.text.pack(side='left', fill='both', expand=True) # Make read-only by default but allow selection and copying self.text.configure(state='disabled') def write(self, content: str): """Append text to the console and scroll to the bottom.""" try: if not self.winfo_exists(): return self.text.configure(state='normal') self.text.insert('end', content) self.text.see('end') self.text.configure(state='disabled') self.update_idletasks() except tk.TclError: pass def clear(self): """Clear all content from the console.""" try: if not self.winfo_exists(): return self.text.configure(state='normal') self.text.delete('1.0', 'end') self.text.configure(state='disabled') self.update_idletasks() except tk.TclError: pass