diff --git a/program/MainWindow.cs b/program/MainWindow.cs index 16b6d65..083aa67 100644 --- a/program/MainWindow.cs +++ b/program/MainWindow.cs @@ -15,6 +15,7 @@ private const int PageSplash = 0; private const int PageSettings = 1; private const int PageSound = 2; + private const int PageMap = 3; // ---- splash page widgets ---- [UI] private Button Option1Button = null; @@ -46,14 +47,15 @@ internal SettingsWindow _settingsWindow; internal Language _Language; - private SoundWindow _soundWindow; + internal SoundWindow _soundWindow; + internal MapWindow _mapWindow; public MainWindow() : this(CreateBuilder()) { } private static Builder CreateBuilder() { var builder = new Builder(); - builder.AddFromFile("homepageallv3.glade"); + builder.AddFromFile("homepageallv5.glade"); return builder; } @@ -122,6 +124,9 @@ _settingsWindow = new SettingsWindow(builder, _arduino, _Language); _settingsWindow.BackRequested += OnSettingsClosed; + _mapWindow = new MapWindow(builder, _Language); + _mapWindow.BackRequested += OnMapClosed; + _Language.Changed += (s, e) => OnArduinoConnectionChanged(_connected); _Language.Apply(LanguageToggle.Active); @@ -133,6 +138,7 @@ RootNoteBook.CurrentPage = PageSplash; _soundWindow = new SoundWindow(builder); _soundWindow.BackRequested += (s, e) => RootNoteBook.CurrentPage = PageSplash; + _soundWindow.CaseSelected += OnCaseSelected; OnArduinoConnectionChanged(false); @@ -272,8 +278,11 @@ private void OnDeleteEvent(object sender, DeleteEventArgs a) { - Shutdown(); + if (_mapWindow != null) _mapWindow.StopSession(); + Application.Quit(); a.RetVal = true; + // Shutdown(); + // a.RetVal = true; } private void OnCloseClicked(object sender, EventArgs e) @@ -317,4 +326,20 @@ // _Language.ApplyLanguage(isEnglish); // _settingsWindow?._Language?.ApplyLanguages(isEnglish); // } + + private void OnCaseSelected(object sender, CaseDefinition def) + { + _mapWindow.ShowCase( + string.Join(" : ", def.TreePath), // no DisplayName on CaseDefinition + SoundWindow.AppFile("body/front.png"), + SoundWindow.AppFile(def.MapFront), + SoundWindow.AppFile(def.SoundPath)); + + RootNoteBook.CurrentPage = PageMap; + } + + private void OnMapClosed(object sender, EventArgs e) + { + RootNoteBook.CurrentPage = PageSound; // back to disease list + } } \ No newline at end of file diff --git a/program/MapWindow.cs b/program/MapWindow.cs new file mode 100644 index 0000000..5c74aa9 --- /dev/null +++ b/program/MapWindow.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using Gtk; +using UI = Gtk.Builder.ObjectAttribute; + +public class MapWindow : IDisposable +{ + [UI] private DrawingArea MapArea = null; + [UI] private Label MapTitleLabel = null; + [UI] private Button MapBackButton = null; + + private readonly Language _lang; + private readonly SoundLooper _sound = new SoundLooper(); + + private Gdk.Pixbuf _body; + private Gdk.Pixbuf _map; + + private const double MapAlpha = 0.7; + + public event EventHandler BackRequested; + + public MapWindow(Builder builder, Language language) + { + builder.Autoconnect(this); + _lang = language; + + MapArea.Drawn += OnMapDrawn; + MapBackButton.Clicked += OnBackClicked; + + _lang.Register("MapBackButton", MapBackButton); + _lang.RegisterDynamic("MapTitleLabel", MapTitleLabel); + } + + /// Called when a disease is picked. Loads images, starts the loop. + public void ShowCase(string title, string bodyPath, string mapPath, string soundPath) + { + MapTitleLabel.Text = title ?? ""; + + DisposePixbufs(); + _body = LoadPixbuf(bodyPath, false); + _map = LoadPixbuf(mapPath, true); // true = key out black + + MapArea.QueueDraw(); + _sound.Start(soundPath); + } + + /// Kills audio without touching the images. Call from OnBackClicked. + public void StopSession() + { + _sound.Stop(); + } + + private void OnBackClicked(object sender, EventArgs e) + { + StopSession(); + var h = BackRequested; + if (h != null) h(this, EventArgs.Empty); + } + + private void OnMapDrawn(object o, DrawnArgs args) + { + var cr = args.Cr; + int w = MapArea.AllocatedWidth; + int h = MapArea.AllocatedHeight; + + DrawFitted(cr, _body, w, h, 1.0); + DrawFitted(cr, _map, w, h, MapAlpha); + + args.RetVal = true; + } + + private static void DrawFitted(Cairo.Context cr, Gdk.Pixbuf pb, int w, int h, double alpha) + { + if (pb == null) return; + + double s = Math.Min((double)w / pb.Width, (double)h / pb.Height); + cr.Save(); + cr.Translate((w - pb.Width * s) / 2, (h - pb.Height * s) / 2); + cr.Scale(s, s); + Gdk.CairoHelper.SetSourcePixbuf(cr, pb, 0, 0); + cr.PaintWithAlpha(alpha); + cr.Restore(); + } + + private static Gdk.Pixbuf LoadPixbuf(string path, bool keyOutBlack) + { + if (string.IsNullOrEmpty(path) || !File.Exists(path)) + { + Console.WriteLine("[MapWindow] image not found: " + path); + return null; + } + + try + { + var pb = new Gdk.Pixbuf(path); + if (!keyOutBlack) return pb; + + var keyed = pb.AddAlpha(true, 0, 0, 0); // == MakeTransparent(Color.Black) + pb.Dispose(); + return keyed; + } + catch (Exception ex) + { + Console.WriteLine("[MapWindow] load failed " + path + ": " + ex.Message); + return null; + } + } + + private void DisposePixbufs() + { + if (_body != null) { _body.Dispose(); _body = null; } + if (_map != null) { _map.Dispose(); _map = null; } + } + + public void Dispose() + { + _sound.Dispose(); + DisposePixbufs(); + } +} \ No newline at end of file diff --git a/program/SoundLopper.cs b/program/SoundLopper.cs new file mode 100644 index 0000000..a0e79cf --- /dev/null +++ b/program/SoundLopper.cs @@ -0,0 +1,117 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; + +public class SoundLooper : IDisposable +{ + private Process _proc; + private Thread _thread; + private volatile bool _running; + private string _path; + + public event Action Log; + + public bool IsPlaying { get { return _running; } } + + public void Start(string wavPath) + { + Stop(); + + if (string.IsNullOrEmpty(wavPath) || !File.Exists(wavPath)) + { + Emit("Sound file not found: " + wavPath); + return; + } + + _path = wavPath; + _running = true; + _thread = new Thread(LoopWorker); + _thread.IsBackground = true; + _thread.Start(); + Emit("Looping started: " + wavPath); + } + + public void Stop() + { + if (!_running) return; + _running = false; + + try + { + var p = _proc; + if (p != null && !p.HasExited) p.Kill(); + } + catch { } + + _proc = null; + + if (_thread != null && _thread.IsAlive) + _thread.Join(500); + + _thread = null; + Emit("Looping stopped"); + } + + private void LoopWorker() + { + while (_running) + { + try + { + var psi = new ProcessStartInfo + { + FileName = PlayerCommand(), + Arguments = "\"" + _path + "\"", + UseShellExecute = false, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + + _proc = Process.Start(psi); + _proc.WaitForExit(); + } + catch (Exception ex) + { + Emit("Playback error: " + ex.Message); + _running = false; + return; + } + } + } + + private static string _player; + private static string PlayerCommand() + { + if (_player != null) return _player; + _player = Exists("paplay") ? "paplay" : "aplay"; + return _player; + } + + private static bool Exists(string cmd) + { + try + { + var p = Process.Start(new ProcessStartInfo + { + FileName = "which", + Arguments = cmd, + UseShellExecute = false, + RedirectStandardOutput = true + }); + p.WaitForExit(); + return p.ExitCode == 0; + } + catch { return false; } + } + + private void Emit(string msg) + { + var h = Log; + if (h != null) h(msg); + else Console.WriteLine("[SoundLooper] " + msg); + } + + public void Dispose() { Stop(); } +} \ No newline at end of file diff --git a/program/SoundWindow.cs b/program/SoundWindow.cs index 98ae77b..e8b4488 100644 --- a/program/SoundWindow.cs +++ b/program/SoundWindow.cs @@ -18,6 +18,9 @@ private readonly List _allCases; private readonly List _path = new List(); + /// Raised when the drill-down reaches a leaf case. + public event EventHandler CaseSelected; + /// Raised when Back is pressed at the top level. public event EventHandler BackRequested; @@ -127,15 +130,20 @@ return; } - string full = AppFile(c.SoundPath); // "sound/SND200.wav" -> absolute - if (!File.Exists(full)) - { - Console.WriteLine("Sound file missing: " + full); - return; - } + WavePlayer.Stop(); // the map page owns audio from here on - ConditionNameLabel.Text = string.Join(" : ", _path); - WavePlayer.Play(full); + 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) diff --git a/program/homepageallv4.glade b/program/homepageallv4.glade new file mode 100644 index 0000000..6f3fd9c --- /dev/null +++ b/program/homepageallv4.glade @@ -0,0 +1,472 @@ + + + + + + False + + + + True + False + + + True + False + True + True + bodyImage + True + + + 1 + 0 + + + + + + + + + + False + + + True + True + False + False + + + + True + False + 3 + 5 + + + Normal Session + True + True + True + 50 + 50 + 50 + 50 + True + True + + + 1 + 1 + + + + + Position Recording Mode + True + True + True + 50 + 50 + 50 + 50 + True + True + + + 1 + 2 + + + + + Exam Mode + True + True + True + 50 + 50 + 50 + 50 + True + True + + + 1 + 3 + + + + + A / あ + True + True + True + True + + + 3 + 0 + + + + + True + False + Language + + + 2 + 0 + + + + + Close + True + True + True + True + + + 4 + 4 + + + + + Settings + True + True + True + + + 4 + 3 + + + + + True + False + network-offline + 6 + + + 4 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + False + 3 + 3 + True + + + True + False + center + True + True + COM Port + + + 0 + 0 + + + + + True + False + center + True + True + + + 1 + 0 + + + + + Connect + True + True + True + start + center + True + True + + + 2 + 0 + + + + + Auto Connect + True + True + True + center + start + True + True + + + 1 + 1 + + + + + Back + True + True + True + end + end + True + True + + + 2 + 2 + + + + + True + False + Log + + + 0 + 2 + + + + + True + True + True + True + in + + + True + True + False + True + + + + + 1 + 2 + + + + + True + False + vertical + + + Refresh Ports + True + True + True + center + start + + + False + True + 0 + + + + + True + False + Disconnected + + + False + True + 1 + + + + + 2 + 1 + + + + + + + + 1 + + + + + True + False + vertical + + + True + False + label + + + + + + False + True + 0 + + + + + True + True + False + False + never + in + + + True + False + + + + True + False + 6 + 6 + True + + + + + + + + + + + + + + + + + + + + + + + + + True + True + 1 + + + + + Back + True + True + True + end + True + right + + + False + True + 4 + + + + + 2 + + + + + + + + + diff --git a/program/homepageallv5.glade b/program/homepageallv5.glade new file mode 100644 index 0000000..dd9fd65 --- /dev/null +++ b/program/homepageallv5.glade @@ -0,0 +1,499 @@ + + + + + + False + + + True + True + False + False + + + + True + False + 3 + 5 + + + Normal Session + True + True + True + 50 + 50 + 50 + 50 + True + True + + + 1 + 1 + + + + + Position Recording Mode + True + True + True + 50 + 50 + 50 + 50 + True + True + + + 1 + 2 + + + + + Exam Mode + True + True + True + 50 + 50 + 50 + 50 + True + True + + + 1 + 3 + + + + + A / あ + True + True + True + True + + + 3 + 0 + + + + + True + False + Language + + + 2 + 0 + + + + + Close + True + True + True + True + + + 4 + 4 + + + + + Settings + True + True + True + + + 4 + 3 + + + + + True + False + network-offline + 6 + + + 4 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + False + 3 + 3 + True + + + True + False + center + True + True + COM Port + + + 0 + 0 + + + + + True + False + center + True + True + + + 1 + 0 + + + + + Connect + True + True + True + start + center + True + True + + + 2 + 0 + + + + + Auto Connect + True + True + True + center + start + True + True + + + 1 + 1 + + + + + Back + True + True + True + end + end + True + True + + + 2 + 2 + + + + + True + False + Log + + + 0 + 2 + + + + + True + True + True + True + in + + + True + True + False + True + + + + + 1 + 2 + + + + + True + False + vertical + + + Refresh Ports + True + True + True + center + start + + + False + True + 0 + + + + + True + False + Disconnected + + + False + True + 1 + + + + + 2 + 1 + + + + + + + + 1 + + + + + True + False + vertical + + + True + False + label + + + + + + False + True + 0 + + + + + True + True + False + False + never + in + + + True + False + + + + True + False + 6 + 6 + True + + + + + + + + + + + + + + + + + + + + + + + + + True + True + 1 + + + + + Back + True + True + True + end + True + right + + + False + True + 4 + + + + + 2 + + + + + True + False + vertical + 6 + + + True + False + label + + + + + + False + True + 0 + + + + + True + False + True + True + + + True + True + 1 + + + + + Back + True + True + True + end + True + + + False + True + 2 + + + + + 3 + + + + + + + + +