"""Help / Information dialog with collapsible sections."""
from __future__ import annotations
import tkinter as tk
import webbrowser
from tkinter import ttk
from config import (
APP_NAME,
APP_VERSION,
AVAILABLE_STATISTICS,
SOLVER_METHOD_DESCRIPTIONS,
SOLVER_METHODS,
get_env_from_schema,
)
from frontend.ui_dialogs.collapsible_section import CollapsibleSection
from frontend.ui_dialogs.keyboard_nav import setup_arrow_enter_navigation
from frontend.ui_dialogs.scrollable_frame import ScrollableFrame
from frontend.window_utils import bind_wraplength, fit_and_center, make_modal
YOUTUBE_CHANNEL_URL = "https://www.youtube.com/@whenphysics"
# ── Section content (human-readable) ─────────────────────────────────
_ABOUT = (
f"Welcome to {APP_NAME} v{APP_VERSION}!\n\n"
f"{APP_NAME} is a graphical tool for solving and visualising differential "
"equations, recurrence relations, and mathematical transforms. "
"It supports:\n\n"
"\u2022 Scalar ODEs — ordinary differential equations of any order\n"
"\u2022 Vector ODEs — coupled systems (Lorenz attractor, Lotka-Volterra, "
"double pendulum, and more)\n"
"\u2022 Difference equations — recurrence relations (Fibonacci, logistic map, ...)\n"
"\u2022 PDEs — 2-D elliptic equations solved with finite differences "
"(Poisson, Laplace, general operator-based)\n"
"\u2022 Function transforms — Fourier (FFT), Laplace, Taylor series, "
"Hilbert, and Z-transform\n\n"
"Under the hood the application relies on SciPy's solve_ivp integrator "
"for ODEs and finite-difference discretisation for PDEs.\n\n"
"Tip: hover over any button or field to see a tooltip with a short description."
)
_HOW_TO_USE = (
"The main menu has five buttons: Solve Differential Equation, "
"Function Transform, Information, Configuration, and Quit.\n\n"
"Solving an equation step by step:\n"
"1. Click Solve Differential Equation.\n"
"2. Pick a predefined equation from the list, or switch to the Custom "
"tab and write your own expression.\n"
"3. Choose the equation type: ODE, Vector ODE, Difference, or PDE.\n"
"4. Click Next to open the parameters screen.\n"
"5. Set the domain, initial/boundary conditions, evaluation points, "
"solver method (for ODEs), and statistics to compute.\n"
"6. Press Solve to run the computation.\n"
"7. Explore the results: interactive plots (solution curve, phase space, "
"3-D trajectory, animation), computed statistics, and export options "
"(CSV, JSON, PNG, MP4).\n\n"
"Function transforms step by step:\n"
"1. Click Function Transform.\n"
"2. Enter a scalar function f(x) — for example sin(x) or exp(-a*x).\n"
"3. (Optional) Define parameters such as a=1.0, omega=2.\n"
"4. Select a transform: Fourier, Laplace, Taylor, Hilbert, or Z-transform.\n"
"5. View the result and export the data to CSV or save the plot."
)
_CUSTOM_EXPRESSIONS = (
"Expressions use Python / NumPy syntax. Below is the notation for each "
"equation type.\n\n"
"Scalar ODE:\n"
" Independent variable: x\n"
" f[0] = f(x), f[1] = f\u2032(x), f[2] = f\u2033(x), ...\n"
" Write the highest derivative in terms of the lower ones.\n\n"
"Vector ODE:\n"
" f[i, k] where i = component index, k = derivative order.\n"
" f[0,0] = component 0, f[0,1] = its first derivative, etc.\n\n"
"Difference equation:\n"
" Index variable: n\n"
" f[0] = f\u2099, f[1] = f\u2099\u208a\u2081, f[2] = f\u2099\u208a\u2082, ...\n"
" Write f\u2099\u208aorder as an expression of previous terms.\n\n"
"PDE (2-D elliptic):\n"
" Spatial: x, y (or x[0], x[1]). Solution: f. Derivatives: f[0]=f_x, "
"f[1]=f_y, f[0,0]=f_xx, f[0,1]=f_xy, f[1,1]=f_yy.\n"
" Select the LHS operator and write the RHS expression.\n\n"
"Available math functions:\n"
" sin cos tan exp log log10 sqrt abs\n"
" sinh cosh tanh arcsin arccos arctan\n"
" floor ceil sign heaviside pi e\n\n"
"Example — damped harmonic oscillator (ODE, order 2):\n"
" Expression: -2*gamma*f[1] - omega**2*f[0]\n"
" Parameters: omega, gamma\n"
" Meaning: f\u2033 = -2\u03b3 f\u2032 - \u03c9\u00b2 f"
)
_PREDEFINED_EQUATIONS = (
"The app ships with a library of classic equations ready to explore.\n\n"
"ODE (42 equations):\n"
" Harmonic \u00b7 Damped \u00b7 Van der Pol \u00b7 Pendulum \u00b7 Rayleigh \u00b7 "
"Duffing \u00b7 Forced Harmonic \u00b7 Cubic \u00b7 Li\u00e9nard \u00b7 Mathieu \u00b7 "
"Legendre \u00b7 Blasius \u00b7 Emden-Fowler \u00b7 Exponential \u00b7 Logistic \u00b7 "
"Gompertz \u00b7 Allee \u00b7 Fisher-KPP \u00b7 Holling \u00b7 RC/RLC \u00b7 Newton "
"Cooling \u00b7 Michaelis-Menten \u00b7 Riccati \u00b7 Langmuir \u00b7 Airy \u00b7 "
"Landau \u00b7 Bistable \u00b7 Malthus \u00b7 and more\n\n"
"Vector ODE (49 systems):\n"
" Lorenz \u00b7 R\u00f6ssler \u00b7 Chen \u00b7 L\u00fc-Chen \u00b7 Thomas \u00b7 "
"Halvorsen \u00b7 Dadras \u00b7 Sprott \u00b7 Chua \u00b7 Aizawa \u00b7 Qi \u00b7 "
"Wang-Sun \u00b7 Lotka-Volterra \u00b7 Competitive LV \u00b7 Food Chain \u00b7 "
"SIR \u00b7 SIRS \u00b7 SEIR \u00b7 FitzHugh-Nagumo \u00b7 Hindmarsh-Rose \u00b7 "
"Morris-Lecar \u00b7 Wilson-Cowan \u00b7 Brusselator \u00b7 Oregonator \u00b7 "
"Sel'kov \u00b7 Goodwin \u00b7 Leslie-Gower \u00b7 Holling-Tanner \u00b7 "
"R\u00f6ssler/Lorenz Hyperchaos (4D) \u00b7 and more\n\n"
"Difference (11 recurrences):\n"
" Geometric Growth \u00b7 Logistic Map \u00b7 Fibonacci \u00b7 Linear Recurrence "
"\u00b7 Cobweb \u00b7 Ricker \u00b7 Beverton-Holt \u00b7 Tent Map \u00b7 "
"Third-Order Recurrence \u00b7 Gauss Map\n\n"
"PDE (elliptic 2-D, 13 variants):\n"
" Poisson \u00b7 Laplace \u00b7 Poisson (xy, sin, cos, exp-sin, rational, "
"cos2\u03c0, xy\u00b2) \u00b7 General operator-based PDEs"
)
_OUTPUT_FILES = (
"All files are saved on demand from the Results or Transform dialog.\n\n"
"\u2022 Save CSV \u2014 tabular data (x, f, f\u2032, f\u2080, f\u2032\u2080, \u2026) "
"ready for spreadsheets or further analysis.\n"
"\u2022 Save JSON \u2014 full metadata, equation definition, and all computed "
"statistics in a structured format.\n"
"\u2022 Matplotlib toolbar \u2014 use the floppy-disk icon below any plot to "
"save it.\n"
"\u2022 MP4 animation \u2014 export a video of the time evolution for vector "
"ODE systems (requires ffmpeg in your PATH).\n\n"
"The default output directory is output/."
)
_FUNCTION_TRANSFORMS = (
"Open Function Transform from the main menu to apply mathematical "
"transforms to any scalar function f(x).\n\n"
"Available transforms:\n"
"\u2022 Original \u2014 plot f(x) over the chosen range.\n"
"\u2022 Fourier (FFT) \u2014 magnitude spectrum |\u0046(\u03c9)|.\n"
"\u2022 Laplace \u2014 L(s) evaluated along the real axis.\n"
"\u2022 Taylor series \u2014 polynomial expansion around a centre point "
"(order 1\u201315).\n"
"\u2022 Hilbert \u2014 discrete Hilbert transform H[f](x).\n"
"\u2022 Z-transform \u2014 magnitude spectrum on the unit circle.\n\n"
"Display mode: switch between Curve (function vs domain) and "
"Coefficients (a\u1d62 vs index) to inspect individual terms.\n\n"
"Use Export CSV or the Matplotlib toolbar to save results."
)
_CONFIGURATION = (
f"Almost every visual and numerical aspect of {APP_NAME} can be customised: "
"UI colours and fonts, plot styling (colours, line width, markers, DPI), "
"solver defaults (method, tolerances, step size), output paths, and "
"logging verbosity.\n\n"
"Open Configuration from the main menu to edit settings in a graphical "
"form, or edit the .env file directly with any text editor.\n\n"
"After saving, the application restarts automatically so changes take "
"effect immediately."
)
def _solver_methods_text() -> str:
lines: list[str] = []
for m in SOLVER_METHODS:
lines.append(f"\u2022 {m} — {SOLVER_METHOD_DESCRIPTIONS[m]}")
return "\n".join(lines)
def _statistics_text() -> str:
lines: list[str] = []
for key, desc in AVAILABLE_STATISTICS.items():
lines.append(f"\u2022 {key} — {desc}")
return "\n".join(lines)
_KEYBOARD_SHORTCUTS = (
"Navigate the interface with the keyboard as well as the mouse.\n\n"
"\u2022 Arrow keys \u2014 move focus between buttons and fields.\n"
"\u2022 Enter / Space \u2014 activate the focused button.\n"
"\u2022 Tab / Shift+Tab \u2014 cycle through input fields."
)
_SECTIONS: list[tuple[str, str]] = [
("About", _ABOUT),
("How to Use", _HOW_TO_USE),
("Writing Custom Expressions", _CUSTOM_EXPRESSIONS),
("Predefined Equations", _PREDEFINED_EQUATIONS),
("Function Transforms", _FUNCTION_TRANSFORMS),
("Solver Methods", _solver_methods_text()),
("Available Statistics", _statistics_text()),
("Output Files", _OUTPUT_FILES),
("Configuration", _CONFIGURATION),
("Keyboard Shortcuts", _KEYBOARD_SHORTCUTS),
]
[docs]
class HelpDialog:
"""Information window with collapsible sections.
Args:
parent: Parent window.
"""
def __init__(self, parent: tk.Tk | tk.Toplevel) -> None:
self.win = tk.Toplevel(parent)
self.win.title(f"{APP_NAME} — Information")
bg: str = get_env_from_schema("UI_BACKGROUND")
self.win.configure(bg=bg)
self._body_labels: list[ttk.Label] = []
self._build_ui()
fit_and_center(self.win, min_width=1000, min_height=750)
make_modal(self.win, parent)
def _build_ui(self) -> None:
pad: int = get_env_from_schema("UI_PADDING")
bg: str = get_env_from_schema("UI_BACKGROUND")
# Fixed bottom button bar
btn_frame = ttk.Frame(self.win)
btn_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=pad, pady=pad)
btn_youtube = ttk.Button(
btn_frame,
text="Support us on YouTube",
command=lambda: webbrowser.open(YOUTUBE_CHANNEL_URL),
padding=(14, 8),
)
btn_youtube.pack(side=tk.LEFT, padx=(0, pad))
btn_close = ttk.Button(
btn_frame,
text="Close",
style="Cancel.TButton",
command=self.win.destroy,
)
btn_close.pack(side=tk.LEFT)
setup_arrow_enter_navigation([[btn_youtube, btn_close]])
btn_close.focus_set()
# Scrollable content
self._scroll = ScrollableFrame(self.win)
self._scroll.apply_bg(bg)
self._scroll.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
inner = self._scroll.inner
inner.configure(padding=pad)
ttk.Label(
inner,
text=f"{APP_NAME} — Information",
style="Title.TLabel",
).pack(anchor=tk.W, pady=(0, pad))
first = True
for title, body in _SECTIONS:
self._add_section(inner, title, body, expanded=first)
first = False
self._scroll.bind_new_children()
bind_wraplength(inner, self._body_labels, pad=48, min_wrap=200)
def _add_section(
self,
parent: ttk.Frame,
title: str,
body: str,
*,
expanded: bool = False,
) -> None:
"""Add a collapsible section (header + body) wrapped in a container."""
pad: int = get_env_from_schema("UI_PADDING")
section = CollapsibleSection(
parent,
self._scroll,
title,
expanded=expanded,
pad=pad,
)
body_lbl = ttk.Label(
section.content,
text=body,
justify=tk.LEFT,
)
body_lbl.pack(anchor=tk.W, fill=tk.X)
self._body_labels.append(body_lbl)