prole/installer/screen.py
chrisfu bbeef67a0f merge Dependencies + Deploy into single 'Setup & Deploy' page; add console preview, Next binding, and stable footer"
Live-updating Dependencies (background checks via installer.config.get_dep_info)\n- Final steps retained: pre-completed items + Start Prole Workstation + Install Prole.app\n- Console shows docker run; Enter/Next runs detatched and verifies\n- Drag-to-install popup marks completion on close\n- Fixed-height footer to prevent background geometry shifts\n- Back/Next footer; Next triggers same action as Enter\n\nNote: docker run preview didn’t appear for the user; follow-up fix planned.
2025-12-04 21:03:06 -08:00

186 lines
6.6 KiB
Python

"""
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
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()
# Safe to call configure multiple times; last one wins
style.configure(style_name, padding=(12, 6), font=('Helvetica', 12))
except Exception:
pass
footer = ttk.Frame(parent)
footer.pack(fill='x', side='bottom')
# Ensure stable geometry even when there are no buttons
try:
footer.configure(height=60)
footer.pack_propagate(False)
except Exception:
pass
# 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)
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