""" 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(40, y, anchor='nw', text=text, fill='#111', font=('Helvetica Neue', 22, 'bold')) app._canvas_items.append(item) def render_paragraph(app, text: str, y: int, wrap: int = 860): """Render a paragraph on the main background canvas.""" if getattr(app, 'bg_canvas', None) is None: return item = app.bg_canvas.create_text(40, y, anchor='nw', text=text, fill='#1d1d1f', font=('Helvetica', 12), width=wrap) app._canvas_items.append(item) def canvas_text(app, x: int, y: int, text: str, *, fill: str = '#1d1d1f', font: tuple = ('Helvetica', 12), anchor: str = 'nw', width: int | None = None, justify: 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 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) -> int: """Create an oval on the app's main canvas and track it.""" if getattr(app, 'bg_canvas', None) is None: return -1 item = app.bg_canvas.create_oval(x1, y1, x2, y2, fill=fill or '', outline=outline or '', width=width) 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) -> int: if getattr(app, 'bg_canvas', None) is None: return -1 item = app.bg_canvas.create_rectangle(x1, y1, x2, y2, outline=outline, width=width, fill=fill or '') app._canvas_items.append(item) return item def canvas_line(app, x1: int, y1: int, x2: int, y2: int, *, fill: str = '#1d1d1f', width: int = 2) -> int: if getattr(app, 'bg_canvas', None) is None: return -1 item = app.bg_canvas.create_line(x1, y1, x2, y2, fill=fill, width=width) 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, ttk.Button]: """Create a right-aligned navigation footer with uniform button styling. Parameters: - parent: the container (typically the root installer container) - buttons: list of (button_id, title) in the order to display - commands: optional mapping of button_id -> callback function - style_name: ttk style to apply to all buttons Returns: {button_id: ttk.Button} Examples: create_nav_footer(footer, [(1, 'Next')]) create_nav_footer(footer, [(1, 'Prev'), (2, 'Next')]) create_nav_footer(footer, [(1, 'Exit')]) """ # Ensure a consistent style for navigation buttons try: style = ttk.Style() # Keep padding modest to allow Aqua to size buttons naturally. # Avoid forcing a font so the native Aqua metrics are used. style.configure(style_name, padding=(10, 6)) except Exception: pass footer = ttk.Frame(parent) footer.pack(fill='x', side='bottom') # Let the footer naturally size to its contents to avoid clipping text on macOS # Flexible spacer to push buttons to the right spacer = ttk.Frame(footer) spacer.pack(side='left', expand=True, fill='x') cmds = commands or {} btn_map: dict[int, ttk.Button] = {} for btn_id, title in buttons: cmd = cmds.get(btn_id) b = ttk.Button(footer, text=title, style=style_name, command=cmd) # Right-aligned order (pack to the right in the declared order) # Small extra right padding on the last (right-most) button pad = (0, 20) if title.lower() in ('finish', 'exit', 'done') else (0, 8) # Use modest vertical padding; let Aqua compute proper height. b.pack(side='right', padx=pad, pady=10) 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.""" self.text.configure(state='normal') self.text.insert('end', content) self.text.see('end') self.text.configure(state='disabled') self.update_idletasks() def clear(self): """Clear all content from the console.""" self.text.configure(state='normal') self.text.delete('1.0', 'end') self.text.configure(state='disabled') self.update_idletasks()