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
+ 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
+
+
+
+
+
+
+
+
+