using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Gtk;
using UI = Gtk.Builder.ObjectAttribute;
/// Controller for notebook page 2 (SoundBox). Not a Gtk.Window — same
/// arrangement as SettingsWindow: it drives widgets inside the shared root.
public class SoundWindow
{
[UI] private Label ConditionNameLabel = null;
[UI] private Grid SoundButtonGrid = null;
[UI] private Button SoundBackButton = null;
private const int Columns = 2;
private readonly List<CaseDefinition> _allCases;
private readonly List<string> _path = new List<string>();
/// Raised when the drill-down reaches a leaf case.
public event EventHandler<CaseDefinition> CaseSelected;
/// Raised when Back is pressed at the top level.
public event EventHandler BackRequested;
public SoundWindow(Builder builder)
{
builder.Autoconnect(this);
if (ConditionNameLabel == null || SoundButtonGrid == null || SoundBackButton == null)
throw new InvalidOperationException(
"Glade id mismatch — ConditionNameLabel=" + (ConditionNameLabel != null) +
" SoundButtonGrid=" + (SoundButtonGrid != null) +
" SoundBackButton=" + (SoundBackButton != null));
// GTK3 Builder does not set the CSS name from the glade id — do it here
// so the #ConditionNameLabel / #SoundBackButton rules actually match.
ConditionNameLabel.Name = "ConditionNameLabel";
SoundButtonGrid.Name = "SoundButtonGrid";
SoundBackButton.Name = "SoundBackButton";
_allCases = CaseDefinition.LoadCasesFromCsv(AppFile("cases.csv"));
if (_allCases.Count == 0)
Console.WriteLine("WARNING: no cases loaded — check cases.csv");
SoundBackButton.Clicked += OnBackClicked;
}
/// Call every time the page becomes visible.
public void Reset()
{
_path.Clear();
Render();
}
/// Resolve next to the .exe, NOT the working directory.
public static string AppFile(string relative)
{
string dir = Path.GetDirectoryName(
System.Reflection.Assembly.GetExecutingAssembly().Location);
return Path.Combine(dir, relative);
}
// ── one method covers all three levels ─────────────────────────────
private void Render()
{
var matches = _allCases.Where(MatchesPath).ToList();
// Distinct values one level deeper than where we currently are.
var options = matches
.Where(c => c.TreePath.Length > _path.Count)
.Select(c => c.TreePath[_path.Count])
.Distinct()
.ToList();
ConditionNameLabel.Text = _path.Count == 0
? "種別を選択"
: string.Join(" : ", _path);
BuildButtons(options, picked =>
{
_path.Add(picked);
// Landed on a leaf? Play it and stay put.
var leaf = _allCases.FirstOrDefault(
c => c.TreePath.Length == _path.Count && MatchesPath(c));
if (leaf != null)
{
Play(leaf);
_path.RemoveAt(_path.Count - 1);
return;
}
Render();
});
}
private bool MatchesPath(CaseDefinition c)
{
string[] p = c.TreePath;
if (p.Length < _path.Count) return false;
for (int i = 0; i < _path.Count; i++)
if (!string.Equals(p[i], _path[i], StringComparison.Ordinal)) return false;
return true;
}
private void BuildButtons(IEnumerable<string> labels, Action<string> onPick)
{
// MUST be first. Attach() stacks; without this every Render() adds a layer
// of live buttons whose closures corrupt _path.
foreach (var child in SoundButtonGrid.Children)
{
SoundButtonGrid.Remove(child);
child.Destroy();
}
var list = labels.ToList();
if (list.Count == 0) { SoundButtonGrid.ShowAll(); return; }
bool top = _path.Count == 0;
int rows = (list.Count + Columns - 1) / Columns;
bool compact = !top && rows > 8;
// Colour by branch index rather than a literal, so renaming or
// translating the CSV's top level doesn't silently turn everything blue.
var topLevel = _allCases
.Where(c => c.TreePath.Length > 0)
.Select(c => c.TreePath[0])
.Distinct()
.ToList();
int i = 0;
foreach (string text in list)
{
string captured = text;
var btn = new Button(captured);
btn.Hexpand = true;
btn.Vexpand = false;
int branchIx = topLevel.IndexOf(top ? captured : _path[0]);
var ctx = btn.StyleContext;
ctx.AddClass("sound-clip");
if (top) ctx.AddClass("top");
else if (compact) ctx.AddClass("compact");
if (branchIx == 0) ctx.AddClass("lung");
else if (branchIx == 1) ctx.AddClass("heart");
btn.Clicked += (s, e) => onPick(captured);
// column-major: down the left column, then down the right
SoundButtonGrid.Attach(btn, i / rows, i % rows, 1, 1);
i++;
}
SoundButtonGrid.ShowAll();
}
private void Play(CaseDefinition c)
{
if (string.IsNullOrWhiteSpace(c.SoundPath))
{
Console.WriteLine("No Sound_File for case " + c.Number);
return;
}
WavePlayer.Stop(); // the map page owns audio from here on
var h = CaseSelected;
if (h != null) h(this, c);
// string full = AppFile(c.SoundPath); // "sound/SND200.wav" -> absolute
// if (!File.Exists(full))
// {
// Console.WriteLine("Sound file missing: " + full);
// return;
// }
// ConditionNameLabel.Text = string.Join(" : ", _path);
// WavePlayer.Play(full);
}
private void OnBackClicked(object sender, EventArgs e)
{
if (_path.Count > 0)
{
_path.RemoveAt(_path.Count - 1);
Render();
}
else
{
WavePlayer.Stop();
if (BackRequested != null) BackRequested(this, EventArgs.Empty);
}
}
}