diff --git a/OS/.gitkeep b/OS/.gitkeep
deleted file mode 100644
index e69de29..0000000
--- a/OS/.gitkeep
+++ /dev/null
diff --git a/OS/BACKUP/Readme.md b/OS/BACKUP/Readme.md
deleted file mode 100644
index 4578fef..0000000
--- a/OS/BACKUP/Readme.md
+++ /dev/null
@@ -1,243 +0,0 @@
-# Restoring Windows on ASUS T101HA
-
-Complete guide to reverting from Xubuntu back to the original Windows installation.
-
----
-
-## What you have
-
-| Item | Location | Purpose |
-|---|---|---|
-| `t101ha.img.zst` | PC: `E:\git\02 cSharp using mono\main\EARS-LINUX\OS\BACKUP\`
USB: BACKUP partition | Full byte-exact disk image, 17GB compressed |
-| `parts.txt` | Same locations | Partition table reference |
-| `window-key.txt` | Same locations | Windows product key |
-
-**Verified:** SHA256 `CBD4348ACB6777D5110D00F15FD67A6CBABE96152B628E503A8F2F6127E4EB63` (both copies identical)
-
-**Image expands to:** 62,537,072,640 bytes — exact match to device size
-
----
-
-## Device reference
-
-- **Internal eMMC:** `/dev/mmcblk1` — ~58GiB (62,537,072,640 bytes)
-- **Firmware:** 64-bit UEFI (confirmed via `fw_platform_size`)
-- **Original layout:** ESP, Microsoft reserved, Windows (`p3`), recovery (`p4`)
-
-> **WARNING:** Verify the device with `lsblk` every time. Device names can shift between boots.
-
----
-
-## Method 1 — Full image restore (recommended)
-
-Restores the exact original system: Windows activated, ASUS recovery partition, everything.
-
-### Preparation
-
-If the image lives on the PC, copy it back to the USB stick's BACKUP partition first. Restoring over a network is not recommended.
-
-### Steps
-
-**1. Boot the Ventoy live session**
-
-Power off fully. Hold F2, tap power, keep holding. Confirm Secure Boot is disabled, boot from USB, select the Xubuntu ISO, choose "Try Xubuntu".
-
-**2. Identify the devices**
-
-```bash
-lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINT
-```
-
-Confirm:
-- eMMC = the **~58GiB** device (`mmcblk1`)
-- USB = the **~115GiB** device with Ventoy/VTOYEFI/BACKUP partitions
-
-**3. Mount the backup**
-
-```bash
-sudo mkdir -p /mnt/backup
-sudo mount /dev/sda3 /mnt/backup
-ls -lh /mnt/backup
-```
-
-Adjust `sda3` to match the BACKUP partition from step 2.
-
-**4. Verify the image before writing**
-
-```bash
-zstd -t /mnt/backup/t101ha.img.zst
-```
-
-Must complete without error. If it fails, use the PC copy instead — do not proceed with a corrupt image.
-
-**5. Restore**
-
-```bash
-sudo blockdev --getsize64 /dev/mmcblk1
-```
-
-Confirm this prints `62537072640`. Then:
-
-```bash
-zstd -dc /mnt/backup/t101ha.img.zst | sudo dd of=/dev/mmcblk1 bs=4M status=progress
-sync
-```
-
-> **WARNING:** `of=` is the destructive direction. Everything on that device is erased. Read the command twice before pressing Enter.
-
-Takes 1–2 hours. Do not interrupt it, and do not run anything else — 2GB RAM.
-
-**6. Finish**
-
-```bash
-sync
-sudo umount /mnt/backup
-sudo poweroff
-```
-
-Remove the USB stick. Power on. Windows should boot, activated, exactly as it was.
-
-### With a progress bar
-
-```bash
-sudo apt install -y pv
-sudo sh -c 'zstd -dc /mnt/backup/t101ha.img.zst | pv -s 62537072640 | dd of=/dev/mmcblk1 bs=4M'
-```
-
----
-
-## Method 2 — Recover files only (non-destructive)
-
-To extract files from the Windows image without wiping Xubuntu. Requires ~58GB free space.
-
-```bash
-zstd -d /path/to/t101ha.img.zst -o /tmp/full.img
-sudo losetup -fP --show /tmp/full.img
-```
-
-Note the loop device it prints (e.g. `/dev/loop0`), then:
-
-```bash
-sudo mkdir -p /mnt/win
-sudo mount -o ro /dev/loop0p3 /mnt/win
-```
-
-Windows lives on partition 3. Browse `/mnt/win`, copy what you need.
-
-Cleanup:
-
-```bash
-sudo umount /mnt/win
-sudo losetup -d /dev/loop0
-rm /tmp/full.img
-```
-
-If mount fails with a BitLocker error, the partition is encrypted — you'll need the recovery key from https://account.microsoft.com/devices/recoverykey
-
----
-
-## Method 3 — Clean Windows install (image lost)
-
-Last resort. The product key is stored in firmware (ACPI MSDM table), so a clean install self-activates without entering anything.
-
-1. Download the Windows 10 ISO from Microsoft on the PC
-2. Write it to USB with Rufus — **GPT**, **UEFI** target
-3. Boot the tablet from it, install to the internal eMMC
-4. Activation happens automatically from firmware
-
-**Caveats:**
-- The ASUS recovery partition is not restored
-- Cherry Trail drivers may need manual installation from ASUS support
-- **Windows 10 is past end of support (October 2025), and this hardware cannot run Windows 11** — a restored image is a supported-nothing situation either way, but a clean install is strictly worse
-
----
-
-## Recovering the product key
-
-From Linux, on the running tablet:
-
-```bash
-sudo strings /sys/firmware/acpi/tables/MSDM | tail -1
-```
-
-From the saved file:
-
-```bash
-cat /mnt/backup/window-key.txt; echo
-```
-
-From Windows:
-
-```powershell
-(Get-CimInstance SoftwareLicensingService).OA3xOriginalProductKey
-```
-
-**Note:** viewing `window-key.txt` in Notepad may show CJK garbage — it's an encoding guess, not corruption. Read it on Linux with `cat`, or open it in VS Code and set encoding to UTF-8.
-
----
-
-## Verifying image integrity
-
-Quick check (structural):
-
-```bash
-zstd -t t101ha.img.zst
-```
-
-Full check (byte count vs device):
-
-```bash
-DEV=$(sudo blockdev --getsize64 /dev/mmcblk1)
-IMG=$(zstd -dc /path/to/t101ha.img.zst | wc -c)
-echo "device: $DEV"
-echo "image: $IMG"
-test "$DEV" = "$IMG" && echo MATCH || echo MISMATCH
-```
-
-Compare copies (PowerShell):
-
-```powershell
-Get-FileHash 'path\to\t101ha.img.zst' -Algorithm SHA256
-```
-
-> **WARNING:** `[ "$DEV" = "$IMG" ]` requires spaces inside the brackets. Without them the shell reports "command not found" and prints MISMATCH regardless of the actual values. Use `test` to avoid this.
-
----
-
-## Troubleshooting
-
-**Restore finishes but won't boot**
-
-Check Secure Boot in BIOS — re-enable it, since the original Windows install expects it. Confirm the boot order lists Windows Boot Manager.
-
-**`dd` reports "No space left on device"**
-
-Wrong target device. Verify with `lsblk` that you're writing to the ~58GiB eMMC.
-
-**Image fails `zstd -t`**
-
-Use the other copy. If both fail, fall back to Method 3.
-
-**Progress appears frozen**
-
-Check it's actually stalled before interrupting:
-
-```bash
-ps -eo pid,stat,cmd | grep -v grep | grep -E 'dd|zstd'
-sudo dmesg | tail -30
-```
-
-STAT `D` plus `mmc` errors in dmesg indicates bad sectors. STAT `R`/`S` with a clean dmesg means it's just slow — eMMC on this tablet throttles when warm.
-
-**"read kernel buffer failed"**
-
-`dmesg` needs root here: `sudo dmesg`
-
----
-
-## Notes
-
-- Keep both copies of the image. The USB stick is a single point of failure.
-- Restoring reverts to the disk state as of **27 July 2026**. Nothing created in Xubuntu survives.
-- Do a full `zstd -t` on whichever copy you plan to use *before* wiping anything.
-- Original image created with: `sudo zstd -1 -T0 -f -o /mnt/backup/t101ha.img.zst /dev/mmcblk1`
\ No newline at end of file
diff --git a/OS/Readme.md b/OS/Readme.md
deleted file mode 100644
index c9bb2f5..0000000
--- a/OS/Readme.md
+++ /dev/null
@@ -1,746 +0,0 @@
-# Xubuntu Setup — Asus T101HA (`ears-tablet`)
-
-Working notes for setting up an Asus T101HA (Atom x5-Z8350, 2 GB RAM / ~1.8 GB
-usable, eMMC) as a Mono + GTK#3 development and runtime machine.
-
-User: `ears` · Hostname: `ears-tablet`
-
----
-
-## Contents
-
-- [0. Recommended baseline](#0-recommended-baseline)
-- [1. Installing Xubuntu](#1-installing-xubuntu)
- - [1.1 Check firmware bitness first](#11-check-firmware-bitness-first--this-determines-everything)
- - [1.2 Build the USB](#12-build-the-usb)
- - [1.3 BIOS](#13-bios)
- - [1.4 Install](#14-install)
- - [1.5 Post-install GRUB fix (IA32 only)](#15-post-install-grub-fix-ia32-machines-only)
- - [1.6 Expect to fix afterwards](#16-expect-to-fix-afterwards)
-- [2. First boot / recovery mode](#2-first-boot--recovery-mode)
-- [3. Low-RAM tuning](#3-low-ram-tuning)
- - [Option A — `zram-tools`](#option-a--zram-tools-simple)
- - [Option B — `systemd-zram-generator`](#option-b--systemd-zram-generator-current-preferred-on-2404)
- - [Swappiness](#swappiness)
-- [4. apt: repair and sources](#4-apt-repair-and-sources)
- - [4.1 The modern layout](#41-the-modern-layout)
- - [4.2 Correct stock sources](#42-correct-stock-sources-2510-example--substitute-your-codename)
- - [4.3 Which host? archive vs old-releases](#43-which-host-archive-vs-old-releases)
- - [4.4 Check for genuinely broken packages](#44-check-for-genuinely-broken-packages)
- - [4.5 Don't do these](#45-dont-do-these)
- - [4.6 `full-upgrade` checklist](#46-full-upgrade-checklist)
- - [4.7 Reboot needed?](#47-reboot-needed)
-- [5. Mono + GTK#3](#5-mono--gtk3)
- - [Install](#install)
- - [Optional](#optional)
- - [Verify](#verify)
- - [Smoke test](#smoke-test)
- - [Notes vs. the WSL guide](#notes-vs-the-wsl-guide)
-- [6. Display rotation + touchscreen](#6-display-rotation--touchscreen)
- - [Identify devices](#identify-devices)
- - [Matrices](#matrices)
- - [Persistence script](#persistence-script)
- - [Login screen (LightDM)](#login-screen-lightdm--separate-from-your-session)
- - [Better: rotate before the session starts](#better-rotate-before-the-session-starts)
- - [Wallpaper breaks after rotating](#wallpaper-breaks-after-rotating)
- - [Auto-rotation (optional)](#auto-rotation-optional)
-- [7. Audio](#7-audio)
- - [Install and test](#install-and-test)
- - [Choosing the right card](#choosing-the-right-card)
- - [Renumbering at the kernel level](#renumbering-at-the-kernel-level)
- - [Caveats](#caveats)
-- [8. Running a script at boot](#8-running-a-script-at-boot)
-- [9. File transfer to/from Windows](#9-file-transfer-tofrom-windows)
-- [10. Quick reference](#10-quick-reference)
-
----
-
-## 0. Recommended baseline
-
-**Target: Xubuntu 24.04 LTS (Minimal ISO).**
-
-Reasons, from the apt/Mono investigation:
-- The machine was found running **Ubuntu 25.10 "questing"**, which hit end of
- life on **9 July 2026**. No further updates will ever be published for it.
-- `gtk-sharp3` is **not packaged on 25.10**, and upgrading to 26.04 would not
- bring it back.
-- `gtk-sharp3` **is** packaged on 24.04 (noble), supported to 2029.
-- Xfce on 24.04 idles around 500–600 MB, which matters at 1.8 GB RAM.
-
-Mono + `mcs` + GTK# is the correct toolchain for this hardware — the .NET SDK
-(~800 MB) and Avalonia are far heavier.
-
-Before reinstalling, back up: `*.cs`, `*.glade`, SSH keys, browser profile.
-
-```bash
-lsblk # is /home a separate partition?
-df -h /
-free -h
-mkdir -p ~/backup && cp ~/*.cs ~/*.glade ~/backup/
-```
-
-If `/home` is its own partition, it can be preserved during install (assign
-`/home` **without** format). Use the same username to keep permissions clean.
-
----
-
-## 1. Installing Xubuntu
-
-### 1.1 Check firmware bitness first — this determines everything
-
-In Windows: **Settings → System → About → System type**
-
-- *32-bit operating system, x64-based processor* → **IA32 UEFI**, needs the
- bootia32 workaround below.
-- *64-bit* → skip the GRUB steps.
-
-### 1.2 Build the USB
-
-Ventoy includes IA32 UEFI support, so try it first. If it won't boot, fall back
-to Rufus and copy a 32-bit `bootia32.efi` (GRUB) into `/EFI/BOOT` on the stick.
-
-From Linux, writing directly:
-
-```bash
-sudo dd if=xubuntu-24.04-minimal-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync
-```
-
-> Verify the target with `lsblk` first. `dd` to the wrong disk destroys it
-> silently.
-
-Verify the ISO: `sha256sum` against the checksum on the download page.
-
-### 1.3 BIOS
-
-- F2 at power-on
-- **Disable Secure Boot** (the ia32 GRUB is unsigned)
-- Boot menu is usually F12 / Esc
-
-### 1.4 Install
-
-Choose **Install**, not *Try* — the live session eats RAM you don't have.
-Don't open Firefox or anything else during install. Let it create swap.
-
-### 1.5 Post-install GRUB fix (IA32 machines only)
-
-The installer writes a 64-bit `grubx64.efi` the firmware can't read. Boot the
-live session, chroot in, and:
-
-```bash
-grub-install --target=i386-efi --efi-directory=/boot/efi
-```
-
-This is **not optional** on IA32 firmware.
-
-### 1.6 Expect to fix afterwards
-
-| Component | Notes |
-|---|---|
-| Wi-Fi | RTL8723BS SDIO — finicky |
-| Audio | ESS ES8316 codec, needs UCM configs |
-| Touchscreen | Works, but rotation needs manual matrix (§6) |
-| Auto-rotation | Needs `iio-sensor-proxy` + custom handler |
-
-A recent 6.x kernel fixes most of these.
-
----
-
-## 2. First boot / recovery mode
-
-If the user account lacks admin rights:
-
-```bash
-usermod -aG sudo ears
-groups ears # confirm 'sudo' appears
-hostnamectl set-hostname ears-tablet
-localectl set-locale LANG=en_US.UTF-8 # optional, cosmetic
-exit # then 'resume'
-```
-
-`usermod -aG sudo` is the one that isn't optional — without it you can log in
-but can't run any admin command.
-
----
-
-## 3. Low-RAM tuning
-
-### Option A — `zram-tools` (simple)
-
-```bash
-sudo apt install zram-tools
-```
-
-Edit `/etc/default/zramswap`:
-```
-PERCENT=60
-ALGO=zstd
-```
-
-### Option B — `systemd-zram-generator` (current, preferred on 24.04+)
-
-```bash
-sudo apt install systemd-zram-generator
-
-sudo tee /etc/systemd/zram-generator.conf > /dev/null <<'EOF'
-[zram0]
-zram-size = min(ram / 2, 4096)
-compression-algorithm = zstd
-swap-priority = 100
-EOF
-
-sudo systemctl daemon-reload
-sudo systemctl start systemd-zram-setup@zram0.service
-swapon --show # expect /dev/zram0
-zramctl
-```
-
-The package does nothing without a config file containing at least one section.
-
-### Swappiness
-
-Swapping to zram is cheap, so raise it so the kernel actually uses it:
-
-```bash
-echo 'vm.swappiness=100' | sudo tee /etc/sysctl.d/99-zram.conf
-```
-
-Reboot. Verdict: worth it at 2–4 GB, marginal at 8 GB, skip above 16 GB.
-zram compresses **RAM** — it does nothing for low disk space.
-
----
-
-## 4. apt: repair and sources
-
-### 4.1 The modern layout
-
-Since 24.04, archive config lives in **`/etc/apt/sources.list.d/ubuntu.sources`**
-(deb822 format). `/etc/apt/sources.list` being empty is normal, not a symptom.
-Most apt advice online predates this and edits a file that does nothing.
-
-### 4.2 Correct stock sources (25.10 example — substitute your codename)
-
-```bash
-sudo tee /etc/apt/sources.list.d/ubuntu.sources > /dev/null <<'EOF'
-Types: deb
-URIs: http://archive.ubuntu.com/ubuntu/
-Suites: questing questing-updates questing-backports
-Components: main restricted universe multiverse
-Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
-
-Types: deb
-URIs: http://security.ubuntu.com/ubuntu/
-Suites: questing-security
-Components: main restricted universe multiverse
-Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
-EOF
-
-sudo apt update
-```
-
-Do not drop the `Signed-By:` line — without it apt falls back to the legacy
-trusted-keyring and you get confusing "not signed" errors later.
-
-`ubuntu.sources.curtin.orig` is the installer's pristine backup. apt ignores
-`.orig` files. Useful as a reference, don't copy blindly.
-
-### 4.3 Which host? archive vs old-releases
-
-An EOL release only moves to `old-releases.ubuntu.com` when the migration
-actually runs — which can lag the EOL date by weeks. Pointing there too early
-gives four clean 404s. Test before switching:
-
-```bash
-for p in questing questing-updates questing-backports questing-security; do
- echo -n "$p: "
- curl -sI "http://old-releases.ubuntu.com/ubuntu/dists/$p/Release" | head -1
-done
-
-curl -sI http://archive.ubuntu.com/ubuntu/dists/questing/Release | head -1
-```
-
-`200` = pocket exists. `404` = drop it or use the other host.
-
-### 4.4 Check for genuinely broken packages
-
-All read-only, cheapest first:
-
-```bash
-sudo dpkg --audit # clean = no output at all
-sudo apt-get check # verifies the dependency tree
-dpkg -l | grep -v '^ii' | grep -v '^rc'
-apt-mark showhold # held packages are silently skipped on upgrade
-```
-
-dpkg status codes (desired state, then actual):
-
-| Code | Meaning |
-|---|---|
-| `ii` | Healthy |
-| `rc` | Removed, config left — harmless |
-| `iU` | Unpacked, never configured |
-| `iF` | Half-configured |
-| `iH` | Half-installed |
-| `iW` / `it` | Waiting on / pending triggers |
-| `*R*` | Reinstall required → `apt install --reinstall ` |
-
-Then repair:
-
-```bash
-sudo dpkg --configure -a
-sudo apt --fix-broken install
-sudo apt full-upgrade
-```
-
-### 4.5 Don't do these
-
-```bash
-# WRONG — killall receives 'sudo', 'rm', paths etc. as process names
-sudo killall apt apt-get dpkg 2>/dev/null sudo rm /var/lib/apt/lists/lock ...
-
-# DANGEROUS — rm -rf also targets files named sudo, apt, clean, update in $PWD
-sudo rm -rf /var/lib/apt/lists/* sudo apt clean sudo apt update
-```
-
-These must be separate lines. Also: killing `dpkg` mid-transaction is what
-*creates* the broken state. Check first with `ps aux | grep -E 'apt|dpkg'`.
-
-Don't remove `universe` — it's an Ubuntu component, not a PPA, and
-`gtk-sharp3` lives there. And never run
-`add-apt-repository --remove ppa:repository-name/ppa` literally; that's a
-placeholder from a copied guide.
-
-### 4.6 `full-upgrade` checklist
-
-- Use `full-upgrade`, not `upgrade` — it permits removals where deps changed
-- Read the summary line; mass removals of desktop/kernel packages = stop
-- Plug the tablet in; power loss mid-`dpkg` recreates the broken state
-- On config prompts, keeping the local version (the default) is safe unless
- you know you edited it
-- Then `sudo apt autoremove` — but answer `n` if anything with `efi` appears
-- Reboot before `do-release-upgrade` so the newest kernel is running
-
-### 4.7 Reboot needed?
-
-```bash
-[ -f /var/run/reboot-required ] && cat /var/run/reboot-required.pkgs
-sudo needrestart # interactive; shows what's running old libs
-```
-
-Kernel / initramfs / systemd / glibc → reboot. Everything else → restart the
-service.
-
----
-
-## 5. Mono + GTK#3
-
-### Install
-
-```bash
-sudo add-apt-repository universe
-sudo apt update
-sudo apt install mono-complete gtk-sharp3
-```
-
-| Package | Contents | Size |
-|---|---|---|
-| `mono-runtime` | JIT + `mscorlib` only | ~15 MB |
-| `mono-devel` | Compiler + the BCL you need | ~200–400 MB |
-| `mono-complete` | Runtime + `mcs` + entire class library + F#/VB | ~300–500 MB |
-
-**Avoid `mono-runtime`.** The BCL is split across dozens of
-`libmono-system-*-cil` packages; hello-world runs, but anything touching LINQ,
-XML, HTTP, or `System.IO.Ports` (the COM-port combo box in `homepage.glade`)
-throws `Could not load file or assembly`. On a tight disk, `mono-devel` saves
-~100 MB over `mono-complete` and still covers everything this project needs.
-
-### Optional
-
-```bash
-sudo apt install glade # visual designer — skip on this machine
-sudo apt install libgdiplus # only if using System.Drawing
-sudo apt install libcanberra-gtk3-module # silences a harmless console warning
-```
-
-### Verify
-
-```bash
-mono --version
-mcs --version
-pkg-config --list-all | grep gtk-sharp # want gtk-sharp-3.0
-apt policy gtk-sharp3
-```
-
-### Smoke test
-
-```bash
-cat > gtkcheck.cs <<'EOF'
-using System;
-using Gtk;
-
-class GtkCheck {
- static void Main() {
- Application.Init();
- var win = new Window("Hello GTK#3");
- win.SetDefaultSize(300, 200);
- win.DeleteEvent += (o, args) => Application.Quit();
-
- var button = new Button("Click me");
- button.Clicked += (o, args) => Console.WriteLine("Clicked!");
- win.Add(button);
-
- win.ShowAll();
- Application.Run();
- }
-}
-EOF
-
-mcs -pkg:gtk-sharp-3.0 gtkcheck.cs
-mono gtkcheck.exe
-```
-
-Do the throwaway test first — it isolates "is gtk-sharp3 working" from "does my
-app compile."
-
-Then the real thing:
-
-```bash
-mcs -pkg:gtk-sharp-3.0 program.cs MainWindow.cs ArduinoConnection.cs -out:test.exe
-mono test.exe
-```
-
-(`ArduinoConnection.cs` is needed because `MainWindow` calls
-`_arduino.Disconnect()`; omitting it gives `CS0246`.)
-
-### Notes vs. the WSL guide
-
-- No WSLg, no `$DISPLAY` fiddling — Xfce is itself GTK3, so the app picks up the
- system theme and the window just appears.
-- Only exception: over SSH you need `ssh -X`.
-- Companion files (`-r:` DLLs, `homepage.glade`) must sit next to the `.exe`, or
- you get `FileNotFoundException`.
-- `homepage.glade` targets GTK 3.24, which 22.04 and 24.04 both ship.
-- `gtk-sharp2` is gone from 24.04 onward — use `gtk-sharp3`.
-
----
-
-## 6. Display rotation + touchscreen
-
-X11 does **not** rotate touch input when you rotate the display. The touch
-device needs its own coordinate transformation matrix.
-
-### Identify devices
-
-```bash
-xrandr # output name — DSI-1, or None-1 on this unit
-xinput list --name-only # look for Silead / GSL / SIS, NOT 'Touchpad'
-```
-
-> On this tablet the output reported as **`None-1`** (xrandr's fallback when the
-> connector type isn't identified — it can change between kernels) and the
-> touchscreen as **`SIS0457:00 0457:11ED`**.
-
-### Matrices
-
-| Rotation | Matrix |
-|---|---|
-| normal | `1 0 0 0 1 0 0 0 1` |
-| right (90° CW) | `0 1 0 -1 0 1 0 0 1` |
-| left (90° CCW) | `0 -1 1 1 0 0 0 0 1` |
-| inverted (180°) | `-1 0 1 0 -1 1 0 0 1` |
-
-```bash
-xrandr --output None-1 --rotate right
-xinput set-prop "SIS0457:00 0457:11ED" "Coordinate Transformation Matrix" 0 1 0 -1 0 1 0 0 1
-```
-
-### Persistence script
-
-```bash
-nano ~/rotate.sh
-```
-
-```bash
-#!/bin/bash
-sleep 3
-OUT=$(xrandr | awk '/ connected/{print $1; exit}')
-xrandr --output "$OUT" --rotate right
-TS=$(xinput list --name-only | grep -iE 'silead|gsl|touchscreen|SIS' | grep -vi 'touchpad' | head -1)
-[ -n "$TS" ] && xinput set-prop "$TS" "Coordinate Transformation Matrix" 0 1 0 -1 0 1 0 0 1
-xset s off
-xset -dpms
-```
-
-```bash
-chmod +x ~/rotate.sh
-~/rotate.sh # test before automating
-```
-
-Then **Settings → Session and Startup → Application Autostart → Add**, command
-`/home/ears/rotate.sh`.
-
-> **Gotcha:** grepping bare `touch` matches the **trackpad** and rotates the
-> pointer instead. Use `touchscreen` and exclude `touchpad`. To undo:
-> ```bash
-> TP=$(xinput list --name-only | grep -i -m1 touchpad)
-> xinput set-prop "$TP" "Coordinate Transformation Matrix" 1 0 0 0 1 0 0 0 1
-> ```
-> Or just log out — the matrix isn't saved anywhere.
-
-The `sleep 3` matters; the touchscreen isn't registered the instant the session
-starts. Raise to 5 if touch still comes out wrong.
-
-**Try Settings → Display first.** Its rotation dropdown often persists on its
-own, leaving the script to handle only the `xinput` part.
-
-### Login screen (LightDM — separate from your session)
-
-```bash
-sudo nano /etc/lightdm/lightdm.conf
-```
-```ini
-[Seat:*]
-display-setup-script=/usr/bin/xrandr --output None-1 --rotate right
-```
-
-### Better: rotate before the session starts
-
-```bash
-sudo nano /etc/X11/xorg.conf.d/10-monitor.conf
-```
-```
-Section "Monitor"
- Identifier "DSI-1"
- Option "Rotate" "right"
-EndSection
-```
-
-Whole machine including the text console — add to `/etc/default/grub`:
-`fbcon=rotate:1` plus `video=DSI-1:panel_orientation=right_side_up`, then
-`sudo update-grub`.
-
-### Wallpaper breaks after rotating
-
-The desktop paints the wallpaper onto a surface sized for the old geometry and
-doesn't repaint after `xrandr`. On Xfce:
-
-```bash
-xfdesktop --reload
-```
-
-Also set the image style to **scaled** — `spanned` and `zoom` misbehave badly
-when the aspect ratio flips. Rotating before the session starts avoids this
-entirely.
-
-### Auto-rotation (optional)
-
-Xfce has no built-in handler. Confirm the sensor works before writing anything:
-
-```bash
-sudo apt install -y iio-sensor-proxy
-monitor-sensor # tilt the tablet; orientation should print
-```
-
-If nothing prints, this unit needs a kernel quirk — not worth chasing.
-
-```bash
-monitor-sensor | while read -r line; do
- case "$line" in
- *normal*) R=normal; M="1 0 0 0 1 0 0 0 1" ;;
- *right-up*) R=right; M="0 1 0 -1 0 1 0 0 1" ;;
- *left-up*) R=left; M="0 -1 1 1 0 0 0 0 1" ;;
- *bottom-up*) R=inverted; M="-1 0 1 0 -1 1 0 0 1" ;;
- *) continue ;;
- esac
- xrandr --output DSI-1 --rotate "$R"
- xinput set-prop "SIS0457:00 0457:11ED" "Coordinate Transformation Matrix" $M
-done
-```
-
-> Most community auto-rotate scripts only call `xrandr` and leave touch behind.
-> If a script doesn't contain `xinput set-prop`, it won't rotate touch.
-
----
-
-## 7. Audio
-
-### Install and test
-
-```bash
-sudo apt install alsa-utils # this is what provides aplay
-
-aplay --version
-aplay -l # list playback devices
-aplay /usr/share/sounds/alsa/Front_Center.wav
-speaker-test -c 2 -t wav -l 1
-```
-
-PipeWire/PulseAudio layer:
-
-```bash
-pactl info # "Server Name" tells you which
-pactl list short sinks
-paplay /usr/share/sounds/alsa/Front_Center.wav
-```
-
-### Choosing the right card
-
-Neither config file exists by default — create whichever you prefer:
-
-```bash
-nano ~/.asoundrc # per-user; wins over the system file
-# or
-sudo nano /etc/asound.conf # system-wide, incl. root and systemd services
-```
-
-```
-defaults.pcm.card 1
-defaults.ctl.card 1
-```
-
-`ctl` is what `alsamixer` reads, `pcm` is what `aplay` reads — set both. No
-reload needed; it's read per-process.
-
-With `plughw` conversion behaviour as the default:
-
-```
-pcm.!default {
- type plug
- slave.pcm "hw:1,0"
-}
-ctl.!default {
- type hw
- card 1
-}
-```
-
-Verify — `-v` prints the resolved device:
-
-```bash
-aplay -D default -v /usr/share/sounds/alsa/Front_Center.wav 2>&1 | head -20
-```
-
-### Renumbering at the kernel level
-
-```bash
-cat /proc/asound/modules
-sudo nano /etc/modprobe.d/alsa-card-order.conf
-```
-```
-options snd_soc_sst_bytcr_rt5640 index=0
-options snd_hda_intel index=1
-```
-```bash
-sudo update-initramfs -u && sudo reboot
-```
-
-### Caveats
-
-- **PipeWire ignores all of the above.** `~/.asoundrc` only governs programs
- talking to ALSA directly (`aplay`, `alsamixer`). For desktop apps use
- `pactl set-default-sink` or `pavucontrol`.
-- Save your mixer levels or they come back muted: `sudo alsactl store 1`
-- Device busy → use `aplay -D default file.wav`, not `-D hw:0,0`
-- Permission errors → `sudo usermod -aG audio $USER`, then re-login
-- `aplay` handles WAV/AU/RAW only — use `mpg123` or `ffplay` for MP3/FLAC
-
----
-
-## 8. Running a script at boot
-
-```bash
-sudo nano /usr/local/bin/myscript.sh
-sudo chmod +x /usr/local/bin/myscript.sh
-```
-
-Start with `#!/bin/bash` and use **absolute paths** — there's almost no
-environment at boot.
-
-```bash
-sudo nano /etc/systemd/system/myscript.service
-```
-
-```ini
-[Unit]
-Description=My boot script
-After=network-online.target
-Wants=network-online.target
-
-[Service]
-Type=oneshot
-ExecStart=/usr/local/bin/myscript.sh
-RemainAfterExit=yes
-
-[Install]
-WantedBy=multi-user.target
-```
-
-```bash
-sudo systemctl daemon-reload
-sudo systemctl enable --now myscript.service
-systemctl status myscript.service
-journalctl -u myscript.service
-```
-
-Variations:
-
-- Long-running daemon → `Type=simple`, drop `RemainAfterExit`
-- Before the login screen → `Before=display-manager.service` in `[Unit]`,
- `WantedBy=display-manager.service` in `[Install]`
-- Quick and dirty → `sudo crontab -e`, then
- `@reboot /usr/local/bin/myscript.sh` (no logging, no ordering)
-- Drop `After=`/`Wants=` if networking isn't needed — it'll run earlier
-
-> **A root systemd service cannot rotate the display** — no `DISPLAY`, no
-> `XAUTHORITY`. Use the config-level methods in §6 instead. Same for audio: a
-> root service has no audio session, so use `systemctl --user` or target the
-> hardware device directly.
-
----
-
-## 9. File transfer to/from Windows
-
-**Tailscale is not required.** It creates a private network *across the
-internet* — only needed to reach the tablet from outside your home. For a
-tablet and PC on the same Wi-Fi it's just another background daemon on a
-2 GB machine.
-
-Lightest option — SSH:
-
-```bash
-sudo apt install -y openssh-server
-```
-
-Connect from Windows with WinSCP or `scp` in PowerShell. No configuration
-beyond the above.
-
-Samba is only worth it if you want the tablet's folders to appear in Windows
-Explorer as a normal network drive.
-
----
-
-## 10. Quick reference
-
-```bash
-# state
-free -h; df -h /; lsblk; swapon --show
-cat /etc/os-release
-. /etc/os-release; echo $VERSION_CODENAME
-
-# apt health
-sudo dpkg --audit; sudo apt-get check; apt-mark showhold
-ls /etc/apt/sources.list.d/
-cat /etc/apt/sources.list.d/ubuntu.sources
-
-# display
-xrandr; xinput list --name-only
-~/rotate.sh; xfdesktop --reload
-
-# audio
-aplay -l; pactl list short sinks
-aplay /usr/share/sounds/alsa/Front_Center.wav
-sudo alsactl store 1
-
-# build
-mcs -pkg:gtk-sharp-3.0 program.cs MainWindow.cs ArduinoConnection.cs -out:test.exe
-mono test.exe
-```
\ No newline at end of file
diff --git a/Readme.md b/Readme.md
index 0c777af..6981827 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,883 +1,274 @@
-# Mono + GTK# + Glade — WSL Setup Guide
+# EARS Kiosk Mode
-## Table of Contents
-- [Mono](#mono)
- - [1. Prerequisites](#1-prerequisites)
- - [2. Install Mono](#2-install-mono)
- - [3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)](#3-install-gtk-gtk-30-for-ubuntu-2404)
- - [4. Install Glade (visual UI designer)](#4-install-glade-visual-ui-designer)
- - [5. Optional: libgdiplus (only if using System.Drawing)](#5-optional-libgdiplus-only-if-using-systemdrawing)
- - [6. Compiling & Running](#6-compiling--running)
- - [7. Minimal Working Examples](#7-minimal-working-examples)
- - [8. Custom Output Names, Targets & Exporting DLLs](#8-custom-output-names-targets--exporting-dlls)
- - [9. Common Errors & Fixes](#9-common-errors--fixes)
- - [10. Notes on Alternatives](#10-notes-on-alternatives)
-- [OS](#os)
- - [Workflow](#workflow)
- - [OS images checked](#os-images-checked)
- - [OS images to check](#os-images-to-check)
-- [EARS Application](#ears-application)
- - [11. Project Layout](#11-project-layout)
- - [12. Single-Window Notebook Architecture](#12-single-window-notebook-architecture)
- - [13. Glade Rules That Bite](#13-glade-rules-that-bite)
- - [14. Generating Widgets From Code](#14-generating-widgets-from-code)
- - [15. Loading cases.csv](#15-loading-casescsv)
- - [16. File Paths — AppFile()](#16-file-paths--appfile)
- - [17. Audio Playback](#17-audio-playback)
- - [18. Build & Run](#18-build--run)
- - [19. Debugging Autoconnect Failures](#19-debugging-autoconnect-failures)
- - [20. Windows Deployment Differences](#20-windows-deployment-differences)
+Runs the EARS application fullscreen on tty3 at boot, with no desktop
+environment around it. The normal Xubuntu desktop remains available on
+tty7 for development and troubleshooting.
+**Status: semi-permanent.** Nothing here replaces the Xubuntu desktop.
+lightdm still runs, the default systemd target is unchanged, and GRUB is
+untouched. Removing two files returns the machine to stock.
---
-## Mono
+## Quick reference
-### 1. Prerequisites
-- Windows 11 (or updated Windows 10) with WSL2 and **WSLg** enabled for GUI support
-- Ubuntu 24.04 (noble) or similar WSL distro
-
-Check WSLg is active:
-```bash
-echo $DISPLAY
-```
-Should return something like `:0`. If empty, GUI apps won't display.
-
----
-
-### 2. Install Mono
-
-```bash
-sudo apt update
-sudo apt install mono-complete
-```
-
-Verify:
-```bash
-mono --version
-mcs --version
-```
-
----
-
-### 3. Install GTK# (GTK# 3.0, for Ubuntu 24.04)
-
-> Note: `gtk-sharp2` is **not available** on Ubuntu 24.04 (noble). Use `gtk-sharp3` instead.
-
-```bash
-sudo add-apt-repository universe
-sudo apt update
-sudo apt install gtk-sharp3
-```
-
-Verify the package is registered:
-```bash
-pkg-config --list-all | grep gtk-sharp
-```
-Expected output:
-gtk-sharp-3.0 Gtk - Gtk
----
-
-### 4. Install Glade (visual UI designer)
-
-```bash
-sudo apt install glade
-```
-
-Launch it:
-```bash
-glade
-```
-Design your UI visually, then save as `yourfile.glade` (XML format).
-
----
-
-### 5. Optional: libgdiplus (only if using System.Drawing)
-
-Only needed if your code uses `System.Drawing.Bitmap`, `Graphics`, etc. (not required for plain GTK#/Glade apps):
-
-```bash
-sudo apt install libgdiplus
-```
-
----
-
-### 6. Compiling & Running
-
-#### Plain console C# program
-```bash
-mcs myprogram.cs
-mono myprogram.exe
-```
-
-#### GTK# 3.0 program (no Glade)
-```bash
-mcs -pkg:gtk-sharp-3.0 myapp.cs
-mono myapp.exe
-```
-
-#### GTK# 3.0 program using a Glade file
-```bash
-mcs -pkg:gtk-sharp-3.0 -pkg:glade-sharp-3.0 myapp.cs
-mono myapp.exe
-```
-Make sure the `.glade` file referenced in code sits in the same folder (or update the path in `new Builder("...")`).
-
----
-
-### 7. Minimal Working Examples
-
-#### Hello World (console)
-```csharp
-using System;
-
-class Hello {
- static void Main() {
- Console.WriteLine("Hello from Mono in WSL!");
- }
-}
-```
-```bash
-mcs hello.cs
-mono hello.exe
-```
-
-#### Hello World (GTK# window, no Glade)
-```csharp
-using System;
-using Gtk;
-
-class GtkCheck {
- static void Main() {
- Application.Init();
- var win = new Window("Hello GTK#3");
- win.SetDefaultSize(300, 200);
- win.DeleteEvent += (o, args) => Application.Quit();
-
- var button = new Button("Click me");
- button.Clicked += (o, args) => Console.WriteLine("Clicked!");
- win.Add(button);
-
- win.ShowAll();
- Application.Run();
- }
-}
-```
-```bash
-mcs -pkg:gtk-sharp-3.0 gtkcheck.cs
-mono gtkcheck.exe
-```
-
-#### Loading a UI built in Glade
-```csharp
-using System;
-using Gtk;
-
-class Program {
- static void Main() {
- Application.Init();
- var builder = new Builder("myui.glade");
- builder.Autoconnect(new Program());
-
- var win = (Window)builder.GetObject("window1");
- win.DeleteEvent += (o, args) => Application.Quit();
- win.ShowAll();
-
- Application.Run();
- }
-}
-```
-```bash
-mcs -pkg:gtk-sharp-3.0 -pkg:glade-sharp-3.0 program.cs
-mono program.exe
-```
-
----
-
-### 8. Custom Output Names, Targets & Exporting DLLs
-
-By default, `mcs file.cs` names the output after the source file (`file.exe`). You can control this with `-out:` and change what kind of binary is produced with `-target:`.
-
-#### 8.1 Custom output name
-```bash
-mcs -pkg:gtk-sharp-3.0 Program.cs -out:MyCustomApp.exe
-```
-Run it the same way:
-```bash
-mono MyCustomApp.exe
-```
-
-#### 8.2 `-target` options
-| Target | Produces | Notes |
-|---|---|---|
-| `exe` (default) | Console executable | Shows a console window when run on Windows |
-| `winexe` | Windows GUI executable | No console window — correct choice for GTK#/Glade GUI apps distributed on Windows |
-| `library` | `.dll` class library | No `Main` entry point required to run directly; used by other programs |
-| `module` | `.netmodule` | Rarely used; for combining into a single assembly later |
-
-Your example, building a windowed GUI executable with a custom name:
-```bash
-mcs -target:winexe -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe
-```
-This produces `programwin.exe`, which on Windows will run without popping up a console window alongside your GTK window.
-
-#### 8.3 Exporting a DLL
-
-If you want to package reusable code (helper classes, business logic, etc.) as a library instead of a standalone app:
-
-```bash
-mcs -target:library Program.cs -out:MyLibrary.dll
-```
-
-- No `Main()` method is required in a `library` target (though it's fine if one class in your project has one — it just won't be used as an entry point for the DLL itself).
-- This creates `MyLibrary.dll`, a Mono/.NET assembly that other C# programs can reference.
-
-#### 8.4 Using a DLL in another program
-
-Suppose `MyLibrary.dll` contains:
-```csharp
-// MyLibrary.cs
-namespace MyLib {
- public class Greeter {
- public string Greet(string name) => $"Hello, {name}!";
- }
-}
-```
-Compiled with:
-```bash
-mcs -target:library MyLibrary.cs -out:MyLibrary.dll
-```
-
-Then in a separate consumer program:
-```csharp
-// ConsumerApp.cs
-using System;
-using MyLib;
-
-class ConsumerApp {
- static void Main() {
- var greeter = new Greeter();
- Console.WriteLine(greeter.Greet("World"));
- }
-}
-```
-
-Compile it while referencing the DLL with `-r:`:
-```bash
-mcs ConsumerApp.cs -r:MyLibrary.dll -out:ConsumerApp.exe
-```
-
-Run it (make sure `MyLibrary.dll` is in the same folder as `ConsumerApp.exe`, or on Mono's library search path):
-```bash
-mono ConsumerApp.exe
-```
-
-#### 8.5 Combining `-target:library` with GTK# packages
-If your DLL itself uses GTK# types (e.g., a shared custom widget), include the package flag when building the library too:
-```bash
-mcs -target:library -pkg:gtk-sharp-3.0 MyGtkWidgets.cs -out:MyGtkWidgets.dll
-```
-And reference both the package and the DLL when compiling the consumer:
-```bash
-mcs -pkg:gtk-sharp-3.0 ConsumerApp.cs -r:MyGtkWidgets.dll -out:ConsumerApp.exe
-```
-
-#### 8.6 Quick reference
-```bash
-# Console exe, custom name
-mcs Program.cs -out:myapp.exe
-
-# Windowed (no console) exe, custom name
-mcs -target:winexe -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe
-
-# Class library (DLL)
-mcs -target:library Library.cs -out:MyLibrary.dll
-
-# Program referencing that DLL
-mcs Consumer.cs -r:MyLibrary.dll -out:Consumer.exe
-```
-
-#### 8.7 Running the compiled .exe on Windows (outside WSL)
-
-Compiling in WSL produces a `.exe` that targets the .NET/Mono runtime — it is **not** a native Windows binary, and it will not run on Windows by itself. Two things are needed:
-
-**1. Install a runtime on the Windows machine**
-
-Pick one:
-- **Mono runtime for Windows** — install the [Mono for Windows](https://www.mono-project.com/download/stable/) package so `.exe` files built with `mcs` can run via the Mono runtime. After install, you can either double-click the `.exe` (Mono associates itself with `.exe` files) or run it explicitly:
-```powershell
- mono programwin.exe
-```
-- **.NET Framework / .NET runtime** — for simple GTK#-free console apps, the .exe may also run under an installed .NET runtime, but GTK# apps specifically require Mono's GTK# libraries, so the Mono for Windows install is the reliable option.
-
-**2. Copy the required DLLs alongside the .exe**
-
-Your compiled `.exe` references managed DLLs at runtime (not just at compile time). Anything you referenced with `-r:` or `-pkg:` must physically travel with the `.exe`:
-
-- Any custom library you built, e.g. `MyLibrary.dll` or `MyGtkWidgets.dll`
-- GTK# binding assemblies your app depends on (typically already provided by installing Mono for Windows, since it bundles the GTK# runtime — but if you used a nonstandard package, copy its DLL too)
-- Your `.glade` file, if the app loads one via `Builder("myui.glade")` — this isn't a DLL but is just as required; it must sit next to the `.exe` at the same relative path your code expects
-
-Minimum folder layout to copy over to Windows:
-programwin.exe
-MyLibrary.dll (any custom DLLs you referenced with -r:)
-myui.glade (if using Glade)
-
-**Common pitfall:** a program that runs fine in WSL but fails on Windows with `FileNotFoundException` or `Could not load file or assembly` almost always means a referenced DLL (or the `.glade` file) wasn't copied over, or Mono for Windows isn't installed.
-
----
-
-### 9. Common Errors & Fixes
-
-| Error | Cause | Fix |
-|---|---|---|
-| `File does not contain a valid CIL image` | Ran `mono` on `.cs` source instead of compiled `.exe` | Compile first: `mcs file.cs`, then `mono file.exe` |
-| `CS0246: type or namespace 'Gtk' could not be found` | Missing package reference during compile | Add `-pkg:gtk-sharp-3.0` to `mcs` command |
-| `CS0103: name 'Console' does not exist` | Missing `using System;` | Add `using System;` at top of file |
-| `Unable to locate package gtk-sharp2` | Package dropped in Ubuntu 24.04 | Use `gtk-sharp3` instead |
-| `DllNotFoundException: libgdiplus` | Code uses `System.Drawing` without the lib installed | `sudo apt install libgdiplus` |
-| `Could not load file or assembly 'MyLibrary.dll'` | DLL not in same folder / not found | Place DLL next to the `.exe`, or set `MONO_PATH` |
-| `FileNotFoundException` / `Could not load file or assembly` (on Windows) | Runtime not installed, or referenced DLL/.glade not copied alongside the .exe | Install Mono for Windows; copy all `-r:` DLLs and `.glade` files into the same folder as the `.exe` |
-
----
-
-### 10. Notes on Alternatives
-
-GTK# is a legacy, lightly-maintained binding. For new projects, consider:
-- **Avalonia UI** — modern, XAML-based, cross-platform, actively maintained
-```bash
- sudo apt install dotnet-sdk-8.0
- dotnet new install Avalonia.Templates
- dotnet new avalonia.app -o MyApp
-```
-
-Use GTK#/Mono/Glade for legacy projects or learning; use Avalonia + `dotnet` for anything new and long-term.
-
-
-Personal note:
-
-Compile note:
- to compile the cs file
- mcs NAME_OF_THE_FILE.cs
- for GTK appilcation
- mcs -pkg:gtk-sharp-3.0 NAME_OF_THE_FILE.cs
- custom output name / windowed exe
- mcs -target:winexe -pkg:gtk-sharp-3.0 NAME_OF_THE_FILE.cs -out:CUSTOM_NAME.exe
- export as DLL
- mcs -target:library NAME_OF_THE_FILE.cs -out:CUSTOM_NAME.dll
- use a DLL in another program
- mcs Consumer.cs -r:CUSTOM_NAME.dll -out:Consumer.exe
-
-
-
-to run note:
- same for all cases
- mono NAME_OF_THE_FILE.exe
-
-## OS
-
-To test the application/setup across different Linux distributions, I used **Ventoy** to create a multiboot USB drive. Ventoy lets you copy multiple ISO files onto a single USB stick and choose which one to boot at startup, without needing to reformat or re-flash the drive for each OS.
-
-### Workflow
-1. Install Ventoy on the target USB drive.
-2. Copy the desired `.iso` files directly onto the Ventoy partition (no extraction needed).
-3. Boot from the USB, select the ISO from the Ventoy boot menu, and test the OS live or install it.
-
-### OS images checked
-- Fedora-Workstation-Live-44-1.7.x86_64
-- lubuntu-26.04-desktop-amd64
-- xubuntu-25.10-desktop-amd64
-
-### OS images to check
-- antiX-26_x64-full
-- xubuntu-26.04-minimal-amd64
-
-
----
-
-# EARS Application
-
-Notes covering the GTK# port of the auscultation trainer: dynamic UI
-generation from CSV, the notebook page structure, and the failure modes
-that cost the most time.
-
----
-
-## 11. Project Layout
-
-Source lives in `program/`, build output in `program/bin/linux/`.
-Runtime assets must sit **next to the .exe**, not next to the source.
-
-```
-program/
-├── Program.cs
-├── MainWindow.cs root window + splash page
-├── SettingWindow.cs settings page controller
-├── SoundWindow.cs sound page controller
-├── CaseDefinition.cs CSV row model + loader
-├── WavePlayer.cs cross-platform wav playback
-├── ArduinoConnection.cs
-├── Language.cs
-├── homepageallv3.glade
-├── cases.csv
-├── map/
-├── sound/
-└── bin/linux/ ← everything above (minus .cs) copied here
- ├── program.exe
- ├── homepageallv3.glade
- ├── cases.csv
- ├── map/
- └── sound/
-```
-
----
-
-## 12. Single-Window Notebook Architecture
-
-One `GtkWindow` (`Root`) holds one `GtkNotebook` (`RootNoteBook`) with
-tabs hidden. Each "screen" is a notebook page; navigation is just
-`RootNoteBook.CurrentPage = N`. No second window is ever created.
-
-| Page | Index | Root widget | Controller |
-|---|---|---|---|
-| Splash | 0 | `SplashGrid` | `MainWindow` |
-| Settings | 1 | `SettingGird` | `SettingsWindow` |
-| Sound | 2 | `SoundBox` | `SoundWindow` |
-
-`SettingsWindow` and `SoundWindow` are **not** `Gtk.Window` subclasses
-despite the names — they're plain controller classes that receive the
-shared `Builder` and bind their own widgets:
-
-```csharp
-public class SoundWindow
-{
- [UI] private Label ConditionNameLabel = null;
- [UI] private Grid SoundButtonGrid = null;
- [UI] private Button SoundBackButton = null;
-
- public event EventHandler BackRequested;
-
- public SoundWindow(Builder builder)
- {
- builder.Autoconnect(this);
- ...
- }
-}
-```
-
-Wired up in `MainWindow`'s private constructor:
-
-```csharp
-_soundWindow = new SoundWindow(builder);
-_soundWindow.BackRequested += (s, e) => RootNoteBook.CurrentPage = PageSplash;
-Option1Button.Clicked += OnNormalSessionClicked;
-```
-
-Controllers never touch the notebook directly — they raise
-`BackRequested` and let `MainWindow` decide. Keeps navigation in one place.
-
-**All three controllers share one `Builder`.** Each `Autoconnect` call
-binds only the ids matching that class's `[UI]` fields, which is exactly
-why ids must be unique across the entire file (§13).
-
----
-
-## 13. Glade Rules That Bite
-
-### IDs must be unique file-wide and valid C# identifiers
-
-`Autoconnect` maps glade ids to field names by string match across the
-**whole** builder, not per page. Two widgets sharing an id bind
-unpredictably.
-
-| Broken | Why | Fixed |
-|---|---|---|
-| `Condition Name` | Space — never matches a field name | `ConditionNameLabel` |
-| `BackButto` | Typo | `SoundBackButton` |
-| `BackButton` on two pages | Duplicate across pages | `SettingBackButton` + `SoundBackButton` |
-
-A mismatch **fails silently** — the field stays `null` and you get a
-`NullReferenceException` later, often several clicks away from the cause.
-See §19 for the guard that catches this at startup.
-
-Verify before running:
-
-```bash
-grep -o 'id="[^"]*"' homepageallv3.glade | sort | uniq -d
-```
-
-Any output is a duplicate id.
-
-### Placeholders are design-time only
-
-The hatched empty cells Glade shows in a `GtkGrid` save as
-`` and are **ignored at load time**. A button placed at
-`left-attach=2` in a designer grid with two empty columns to its left
-ends up at column 0 in the running app — this is the classic
-"button jumps to the left" bug.
-
-Never use placeholders for spacing or alignment. Use `halign` + `hexpand`.
-
-### Right-aligning a button
-
-Both properties are required:
-
-```xml
-end
-True
-```
-
-`hexpand` makes the cell consume the full row width; `halign=end` parks
-the button at the right edge of that cell. `halign` alone does nothing
-when the cell is only as wide as the button.
-
-In Glade: **Common** tab → *Horizontal Alignment* = `End`,
-*Expand → Horizontal* = checked.
-
-### Scrolling a grid needs a Viewport
-
-`GtkGrid` doesn't implement `GtkScrollable`. Dropping one into a
-`GtkScrolledWindow` requires an intermediate `GtkViewport` — Glade
-inserts it automatically. Don't delete it.
-
-```
-SoundScroller GtkScrolledWindow hexpand + vexpand, packing expand=True
-└─ GtkViewport (auto-added, required)
- └─ SoundButtonGrid GtkGrid empty, column-homogeneous=True
-```
-
-`GtkFlowBox` *is* scrollable and wraps children automatically, but the
-`gtk-sharp3` binding on Ubuntu is 2.99.x and may not expose it. Check
-before relying on it:
-
-```bash
-monop -r:/usr/lib/cli/gtk-sharp-3.0/gtk-sharp.dll Gtk.FlowBox
-```
-
-### Expand appears in two tabs
-
-For a widget inside a `GtkBox`, **Common → Expand** sets the widget's own
-`hexpand`/`vexpand`, while **Packing → Expand** sets the box child
-property. They are different things and both usually need setting.
-
----
-
-## 14. Generating Widgets From Code
-
-Leave the container **empty** in Glade and fill it at runtime. Three
-rules, all of which produce silent failures when broken:
-
-```csharp
-private void BuildButtons(IEnumerable labels, Action onPick)
-{
- // 1. Remove AND destroy — Remove alone leaks the widget
- foreach (var child in SoundButtonGrid.Children)
- {
- SoundButtonGrid.Remove(child);
- child.Destroy();
- }
-
- int i = 0;
- foreach (string text in labels)
- {
- // 2. Capture the loop variable — otherwise every handler
- // sees the final value
- string captured = text;
-
- var btn = new Button(captured);
- btn.Hexpand = true;
- btn.Clicked += (s, e) => onPick(captured);
- SoundButtonGrid.Attach(btn, i % Columns, i / Columns, 1, 1);
- i++;
- }
-
- // 3. Widgets created in code start HIDDEN. Without this the grid
- // stays blank with no error of any kind.
- SoundButtonGrid.ShowAll();
-}
-```
-
-`ShowAll()` on the container is the single most common cause of
-"my buttons didn't appear" in GTK#.
-
-### Drill-down navigation
-
-All three modes (type → condition → play) use one grid and one render
-method. State is a `List` path; Back pops one level:
-
-```csharp
-private readonly List _path = new List();
-
-private void Render()
-{
- var matches = _allCases.Where(MatchesPath).ToList();
-
- 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); /* leaf? play : Render(); */ });
-}
-
-private void OnBackClicked(object sender, EventArgs e)
-{
- if (_path.Count > 0) { _path.RemoveAt(_path.Count - 1); Render(); }
- else { WavePlayer.Stop(); BackRequested(this, EventArgs.Empty); }
-}
-```
-
-Tree depth varies per row (`Tree_Level3` is often empty), so
-`CaseDefinition.TreePath` returns only the non-empty levels and the
-drill-down adapts automatically.
-
----
-
-## 15. Loading cases.csv
-
-`CaseDefinition.LoadCasesFromCsv(path)` returns `List`.
-Column → property mapping:
-
-| CSV column | Property |
+| Action | How |
|---|---|
-| `Number` | `Number` |
-| `Type` | `Type` (心音 / 呼吸音) — also drives `IsHeart` |
-| `Category` | `CategoryJp` |
-| `Subcategory` | `SubcategoryJp` |
-| `Location` | `LocationJp` |
-| `Tree_Level1..3` | `TreeLevel1..3` |
-| `Image_File` | `MapFront` |
-| `Sound_File` | `SoundPath` |
-| `Image_Right` / `Image_Left` / `Image_Back` | `MapRight` / `MapLeft` / `MapBack` |
-
-Four things the loader must get right:
-
-**Encoding.** The file contains Japanese. Read with `Encoding.UTF8` and
-BOM detection — the default ANSI codepage produces mojibake that only
-shows up on the rendered buttons.
-
-**Delimiter.** The file is TAB separated. Auto-detect so a re-export
-from Excel as comma-separated doesn't break it:
-
-```csharp
-char sep = lines[0].Contains("\t") ? '\t' : ',';
-```
-
-**Header mapping.** Build `name → index` from row 0 rather than
-hardcoding positions, so adding or reordering columns is safe.
-
-**InvariantCulture on every numeric parse.** Same failure class as the
-CSS scaling bug in §"scaling" — under `ja_JP`/`de_DE` the
-culture-sensitive default silently misparses:
-
-```csharp
-int.TryParse(get(f, "Number"), NumberStyles.Integer,
- CultureInfo.InvariantCulture, out number)
-```
-
-The loader prints its result on every run:
-
-```
-Loaded 98 cases from /path/to/bin/linux/cases.csv
-```
-
-`Loaded 0 cases` points at the path or the delimiter, not the UI.
+| Switch to kiosk | `Ctrl+Alt+F3` |
+| Switch to desktop | `Ctrl+Alt+F7` |
+| Exit kiosk to a shell | Press **Close** in the app |
+| Start kiosk manually | `~/kiosk/kiosk-start.sh` (from a text console) |
+| Disable kiosk at next boot | `rm ~/kiosk/enabled` |
+| Re-enable | `touch ~/kiosk/enabled` |
+| Live log | `tail -f ~/kiosk/logs/current.log` |
+| Kill the app | `pkill -f program.exe` (relaunches automatically) |
---
-## 16. File Paths — AppFile()
+## Layout
-**Never use bare relative paths.** `Path.GetFullPath("cases.csv")`
-resolves against the *working directory*, so the app works when launched
-from `program/` and silently loads nothing from anywhere else.
-
-```csharp
-public static string AppFile(string relative)
-{
- string dir = Path.GetDirectoryName(
- System.Reflection.Assembly.GetExecutingAssembly().Location);
- return Path.Combine(dir, relative);
-}
+```
+~/kiosk/
+├── kiosk-start.sh launches the X server on the current VT
+├── kiosk-session.sh the session: hardware setup, WM, app loop
+├── display-setup.sh symlink to the rotation/touch script
+├── enabled presence of this file enables boot autostart
+├── README.md this file
+└── logs/
+ ├── current.log symlink to the newest session log
+ ├── session-*.log one per session, 20 kept
+ └── startx.log X server output (appended, never rotated)
```
-Use it for **everything**: the glade file, the CSV, maps, sounds.
-
-```csharp
-builder.AddFromFile(SoundWindow.AppFile("homepageallv3.glade"));
-```
-
-Mixing the two conventions is how you end up editing
-`program/homepageallv3.glade` while the app reads
-`bin/linux/homepageallv3.glade` — every fix appears to do nothing.
-
-**Linux is case-sensitive.** `sound/snd200.wav` ≠ `sound/SND200.wav`,
-which was fine on Windows and isn't now. Audit the CSV against the disk:
-
-```bash
-cut -f10 cases.csv | tail -n +2 | while read f; do
- [ -n "$f" ] && [ -f "$f" ] || echo "MISSING: $f"
-done
-```
+Application: `/home/ears/Desktop/Kazi/v3/linux/program.exe`
---
-## 17. Audio Playback
+## How it boots
-XAudio2/SharpDX from the WinForms build does **not** load under Mono on
-Linux. `WavePlayer` shells out instead:
+```
+power on
+ → GRUB → systemd → graphical.target
+ ├── lightdm on vt7 (normal Xubuntu, untouched)
+ └── getty@tty3 autologin as `ears`
+ → ~/.bash_profile sees tty3 + ~/kiosk/enabled
+ → exec kiosk-start.sh
+ → startx on vt3
+ → kiosk-session.sh
+ → display-setup.sh, matchbox, unclutter
+ → loop: mono program.exe
+```
-| Platform | Mechanism |
+`kiosk-start.sh` calls `fgconsole` to find the VT it was launched from,
+so it works whether started at boot on tty3 or by hand from any console.
+
+### Exit codes
+
+| Code | Meaning |
|---|---|
-| Linux / WSL | `paplay`, falling back to `aplay` |
-| Windows | `System.Media.SoundPlayer` |
-
-Install both helpers — WSLg routes through PulseAudio, so `paplay` is
-the one that works:
-
-```bash
-sudo apt install pulseaudio-utils alsa-utils
-```
-
-Verify outside the app before blaming the C#:
-
-```bash
-paplay bin/linux/sound/SND200.wav
-echo $PULSE_SERVER # empty under WSLg → wsl --shutdown and retry
-```
-
-**Format matters on Windows.** `SoundPlayer` handles PCM WAV only:
-
-```bash
-file sound/SND200.wav # want: RIFF ... WAVE audio, Microsoft PCM
-```
-
-**`Stop()` is a no-op on Windows** — `SoundPlayer.Play()` returns no
-handle. If stop-on-back is needed there, use `PlaySync()` on a
-background thread or add NAudio.
+| 0 | Clean exit (Close button) — session ends, shell on tty3 |
+| other | Crash — relaunched after 2s |
+| 3 crashes in <20s each | Recovery console (xterm) opens with the log |
---
-## 18. Build & Run
-
-Debug build (keeps the console so `Console.WriteLine` diagnostics show):
+## Dependencies
```bash
-mcs -pkg:gtk-sharp-3.0 *.cs -out:./bin/linux/program.exe \
- && cp homepageallv3.glade cases.csv ./bin/linux/ \
- && cp -r map sound ./bin/linux/ \
- && mono ./bin/linux/program.exe
+sudo apt install --no-install-recommends \
+ xinit matchbox-window-manager unclutter xterm x11-xserver-utils
```
-The `cp` steps are not optional — stale assets in `bin/linux/` are a
-recurring source of phantom bugs (§16).
+Plus `mono-complete` for the application itself.
-Re-copying every wav on each build gets slow. Once stable, symlink and
-drop the `cp -r`:
+---
+
+## Installation from scratch
+
+### 1. Scripts
+
+Place `kiosk-start.sh` and `kiosk-session.sh` in `~/kiosk/`, then:
```bash
-ln -s ../../map bin/linux/map
-ln -s ../../sound bin/linux/sound
+chmod +x ~/kiosk/kiosk-start.sh ~/kiosk/kiosk-session.sh
+mkdir -p ~/kiosk/logs
```
-Release build (`-target:winexe` suppresses the console — don't use it
-while debugging):
+Confirm the app path at the top of `kiosk-session.sh`:
```bash
-mcs -target:winexe -pkg:gtk-sharp-3.0 *.cs -out:./bin/windows/program.exe
+APP_DIR="/home/ears/Desktop/Kazi/v3/linux"
+APP_CMD="mono program.exe"
+```
+
+No `$` before the path, no trailing slash.
+
+### 2. Display and touch
+
+The rotation script is shared with the Xubuntu session
+(`~/.config/autostart/Rotate Display.desktop`). Symlink it:
+
+```bash
+ln -s /path/to/rotate-display.sh ~/kiosk/display-setup.sh
+```
+
+It handles: 90° right rotation, the matching touch coordinate matrix
+for `SIS0457:00 0457:11ED`, and disabling blanking/DPMS.
+
+### 3. Autologin on tty3
+
+```bash
+sudo systemctl edit getty@tty3
+```
+
+```ini
+[Service]
+ExecStart=
+ExecStart=-/sbin/agetty --autologin ears --noclear %I $TERM
+```
+
+```bash
+sudo systemctl enable getty@tty3.service
+```
+
+### 4. Boot hook
+
+Append to `~/.bash_profile` (or `~/.profile` if that's the one bash reads):
+
+```bash
+# --- kiosk autostart on tty3 only ---
+if [[ -z $DISPLAY && $(tty) == /dev/tty3 && -f $HOME/kiosk/enabled ]]; then
+ sleep 3 # let lightdm claim vt7 first
+ exec "$HOME/kiosk/kiosk-start.sh"
+fi
+```
+
+### 5. Enable and reboot
+
+```bash
+touch ~/kiosk/enabled
+sudo reboot
```
---
-## 19. Debugging Autoconnect Failures
+## Troubleshooting
-An unbound `[UI]` field is `null` with no warning. Add an explicit guard
-to every controller constructor so the failure names the widget at
-startup:
+Always start here:
-```csharp
-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));
+```bash
+tail -60 ~/kiosk/logs/current.log # app + session errors
+tail -40 ~/kiosk/logs/startx.log # X server failed to start
+grep -E '\(EE\)' ~/.local/share/xorg/Xorg.1.log
```
-Output looks like:
+| Symptom | Cause / fix |
+|---|---|
+| `Only console users are allowed to run the X server` | Launched from a terminal inside Xfce. Use `Ctrl+Alt+F3` and run from the text console. |
+| `Cannot open virtual console N (Permission denied)` | Asking for a VT you don't own. `kiosk-start.sh` uses `fgconsole` to avoid this — check it wasn't edited to a hardcoded `vt8`. |
+| `Server is already active for display 1` | Stale lock: `sudo rm -f /tmp/.X1-lock /tmp/.X11-unix/X1` |
+| `cd: $/home/... No such file` | Stray `$` in `APP_DIR`. Path must be literal. |
+| Black screen, then "session ended" | X started, session script exited. Read `current.log`. |
+| App runs but not fullscreen | `matchbox-window-manager` missing. |
+| Rotation right, touch wrong | `display-setup.sh` not executable, or `xinput` device name changed. Check with `xinput list --name-only`. |
+| Boot lands on vt7 not vt3 | lightdm won the race. Increase `sleep 3` to `sleep 6`. |
+| No sound | See below. |
-```
-System.InvalidOperationException: Glade id mismatch —
- ConditionNameLabel=False SoundButtonGrid=True SoundBackButton=True
+`xf86EnableIO: failed to enable I/O ports` in the Xorg log is normal and
+can be ignored — rootless X doesn't need legacy port access.
+
+### Getting a shell inside the kiosk
+
+Add temporarily to `kiosk-session.sh`, before the `while true` loop:
+
+```bash
+xterm -fa Monospace -fs 14 -geometry 100x30 &
```
-For classes with many fields, loop a dictionary instead:
+### Audio
-```csharp
-foreach (var pair in new Dictionary {
- { "ComPortLabel", ComPortLabel }, { "ConnectButton", ConnectButton },
- { "SettingBackButton", SettingBackButton }, /* ... */ })
- if (pair.Value == null)
- throw new InvalidOperationException("Glade id not bound: " + pair.Key);
+The kiosk session does not start an audio server; on the desktop that's
+done by Xfce's autostart. Check from inside the kiosk:
+
+```bash
+pactl info # "Connection refused" = no server
+aplay /usr/share/sounds/alsa/Front_Center.wav
```
-### Reading a GTK# stack trace
+If a server is needed, add to `kiosk-session.sh` before `matchbox`:
-Exceptions inside signal handlers arrive wrapped:
-
-```
-System.Reflection.TargetInvocationException ---> System.NullReferenceException
- at SoundWindow.Render () [0x0007b]
+```bash
+if command -v pipewire >/dev/null && ! pactl info &>/dev/null; then
+ pipewire & pipewire-pulse & wireplumber &
+ sleep 2
+elif command -v pulseaudio >/dev/null && ! pactl info &>/dev/null; then
+ pulseaudio --start
+ sleep 2
+fi
```
-Ignore the `GLib.SignalClosure` / `MarshalCallback` frames — they're
-plumbing. The first frame naming **your** class is the real site, and
-the `[0x...]` IL offset distinguishes lines within it.
-
-### Common failures
-
-| Symptom | Cause | Fix |
-|---|---|---|
-| NRE in controller ctor | `[UI]` field id mismatch | §19 guard, then fix the glade id |
-| NRE on first click, no CSV log line | Controller never constructed | Assign it in the **private** `MainWindow(Builder)` ctor |
-| Buttons don't appear, no error | Missing `ShowAll()` | Call it on the container after `Attach` |
-| Every button does the same thing | Closure over loop variable | `string captured = text;` |
-| `Loaded 0 cases` | Wrong path or delimiter | Use `AppFile()`; check tab vs comma |
-| Button sits left despite `halign=end` | Relying on grid placeholders | Add `hexpand=True` |
-| Fix appears to do nothing | Editing a different copy of the glade | Use `AppFile()` + `cp` on build |
-| Japanese renders as garbage | Wrong encoding | `File.ReadAllLines(path, Encoding.UTF8)` |
-| GTK warning about scrolling | Grid directly in ScrolledWindow | Keep the `GtkViewport` |
+**Test with the Xfce session logged out**, not merely switched away.
+Both sessions run as `ears`, so an audio server started by the desktop
+will serve the kiosk too — giving a false pass that breaks later when
+lightdm is removed.
---
-## 20. Windows Deployment Differences
+## Updating the application
-The C#, the glade file, and the CSV are **identical**. Four differences:
+Replace files in `/home/ears/Desktop/Kazi/v3/linux/`, then from a shell:
-1. **Audio** — see §17. PCM-only, and `Stop()` doesn't work.
-2. **Build target** — `-target:winexe` for no console window.
-3. **Runtime** — Mono for Windows must be installed on the target
- machine; it bundles the GTK# runtime.
-4. **Serial ports** — `ArduinoConnection` enumerates `COM*` rather than
- `/dev/ttyUSB*` / `/dev/ttyACM*`. Mono's Linux `SerialPort.GetPortNames()`
- has historically missed devices; verify against `ls /dev/tty*`.
-
-Ship this folder:
-
-```
-program.exe
-homepageallv3.glade
-cases.csv
-map/
-sound/
+```bash
+pkill -f program.exe
```
-Not problems, for the record: forward slashes in CSV paths work fine on
-Windows, and Windows' case-insensitivity means anything working on Linux
-also works there — never the reverse. Develop on Linux and Windows
-comes free.
\ No newline at end of file
+The session loop treats this as a crash and relaunches within 2 seconds
+with the new build. No reboot needed.
+
+---
+
+## Removal
+
+Temporary (next boot only):
+
+```bash
+rm ~/kiosk/enabled
+```
+
+Complete:
+
+```bash
+sudo systemctl disable getty@tty3.service
+sudo rm -rf /etc/systemd/system/getty@tty3.service.d/
+# remove the kiosk block from ~/.bash_profile
+rm -rf ~/kiosk
+```
+
+Nothing else was modified. lightdm, the default target, GRUB, and
+`~/.config/xfce4/` are all untouched.
+
+---
+
+## Known limitations
+
+- Both lightdm and the kiosk X server run simultaneously — roughly 200MB
+ of RAM that will be freed when lightdm is eventually dropped.
+- No lockdown yet: VT switching, GRUB editing, and USB boot are all open.
+- No in-app power control; shutdown is done from the desktop or a shell.
+- `startx.log` grows without bound; truncate occasionally.
+
+---
+
+## Next steps toward a permanent kiosk
+
+In this order, testing each before moving on:
+
+1. `systemctl set-default multi-user.target`, disable lightdm
+2. Move autologin from tty3 to tty1
+3. In-app power menu (sleep/shutdown/restart) with a polkit rule
+4. Developer unlock: PIN pad, `exit 42` → Xfce session
+5. Lockdown: GRUB password, `DontVTSwitch`, mask tty4–6, BIOS password
+6. Move the app out of `Desktop/` to `/opt/ears/`
+
+Keep a transcript (`script -a ~/kiosk/setup-transcript.txt`) through each
+stage — it becomes the provisioning script for the remaining tablets.
\ No newline at end of file
diff --git a/ReadmeQuick.md b/ReadmeQuick.md
deleted file mode 100644
index 768dd98..0000000
--- a/ReadmeQuick.md
+++ /dev/null
@@ -1,22 +0,0 @@
-### 8.6 Quick reference
-```bash
-# Console exe, custom name
-mcs Program.cs -out:myapp.exe
-
-# Console exe, custom name
-mcs -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe
-
-# Windowed (no console) exe, custom name
-mcs -target:winexe -pkg:gtk-sharp-3.0 Program.cs -out:programwin.exe
-
-# Class library (DLL)
-mcs -target:library Library.cs -out:MyLibrary.dll
-
-# Program referencing that DLL
-mcs Consumer.cs -r:MyLibrary.dll -out:Consumer.exe
-
-#run with edit
-GTK_DEBUG=interactive mono ./bin/linux/program.exe
-
-```
-
diff --git a/dll/atk-sharp.dll b/dll/atk-sharp.dll
deleted file mode 100644
index cb11a7b..0000000
--- a/dll/atk-sharp.dll
+++ /dev/null
Binary files differ
diff --git a/dll/cairo-sharp.dll b/dll/cairo-sharp.dll
deleted file mode 100644
index ae71a7a..0000000
--- a/dll/cairo-sharp.dll
+++ /dev/null
Binary files differ
diff --git a/dll/gdk-sharp.dll b/dll/gdk-sharp.dll
deleted file mode 100644
index 08f0654..0000000
--- a/dll/gdk-sharp.dll
+++ /dev/null
Binary files differ
diff --git a/dll/gio-sharp.dll b/dll/gio-sharp.dll
deleted file mode 100644
index 818134d..0000000
--- a/dll/gio-sharp.dll
+++ /dev/null
Binary files differ
diff --git a/dll/glib-sharp.dll b/dll/glib-sharp.dll
deleted file mode 100644
index f480530..0000000
--- a/dll/glib-sharp.dll
+++ /dev/null
Binary files differ
diff --git a/dll/gtk-sharp.dll b/dll/gtk-sharp.dll
deleted file mode 100644
index ece3ff2..0000000
--- a/dll/gtk-sharp.dll
+++ /dev/null
Binary files differ
diff --git a/dll/pango-sharp.dll b/dll/pango-sharp.dll
deleted file mode 100644
index 07fc422..0000000
--- a/dll/pango-sharp.dll
+++ /dev/null
Binary files differ
diff --git a/program/Appconfig.cs b/program/Appconfig.cs
deleted file mode 100644
index 8eb2fec..0000000
--- a/program/Appconfig.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using System.Linq;
-
-public static class AppConfig
-{
- // // Computed once, cached forever — reading it anywhere costs nothing.
- // public static readonly bool DebugMode =
- // Environment.GetCommandLineArgs().Contains("--debug");
-
- public static readonly bool DebugMode = Compute();
-
- private static bool Compute()
- {
- var argv = Environment.GetCommandLineArgs();
- Console.WriteLine("argv = [" + string.Join(" | ", argv) + "]");
- return argv.Contains("--debug")
- || Environment.GetEnvironmentVariable("EARS_DEBUG") == "1";
- }
-
-}
\ No newline at end of file
diff --git a/program/ArduinoConnection.cs b/program/ArduinoConnection.cs
deleted file mode 100644
index 6edc385..0000000
--- a/program/ArduinoConnection.cs
+++ /dev/null
@@ -1,373 +0,0 @@
-using System;
-using System.IO.Ports;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-public class ArduinoConnection : IDisposable
-{
- public const int BaudRate = 115200;
-
- // Must match what your sketch replies to "REQUEST_ID" (Form2.cs: EXPECTED_DEVICE_ID)
- // public const string ExpectedDeviceId = "EARS_READER";
- public const string ExpectedDeviceId = "N_EARS_ESP32C3";
-
- private const int HeartbeatTimeoutMs = 10000;
- private const int HeartbeatCheckMs = 2000;
- private const int MaxReconnectAttempts = 5;
- private const int ResetSettleMs = 1500; // board reboots when DTR asserts
-
- private readonly object _gate = new object();
- private SerialPort _port;
- private Thread _reader;
- private volatile bool _reading;
-
- private DateTime _lastData = DateTime.MinValue;
- private uint _heartbeatId;
- private bool _autoReconnect;
- private int _reconnectAttempts;
- private DateTime _nextReconnect = DateTime.MinValue;
-
- /// Raised on the GTK main thread for every complete line from the device.
- public event Action LineReceived;
- /// Raised on the GTK main thread with human-readable status text.
- public event Action Log;
- /// Raised on the GTK main thread when the link goes up or down.
- public event Action ConnectionChanged;
-
- public string PortName { get; private set; }
-
- public bool IsOpen
- {
- get { lock (_gate) return _port != null && _port.IsOpen; }
- }
-
- public static string[] ListPorts()
- {
- return SerialPort.GetPortNames()
- .Distinct()
- .OrderBy(p => p, StringComparer.Ordinal)
- .ToArray();
- }
-
- // ---------- public API ----------
-
- public bool Connect(string portName)
- {
- if (string.IsNullOrEmpty(portName))
- {
- Emit(Log, "No port selected");
- return false;
- }
-
- ClosePort(silent: true);
-
- if (!OpenPort(portName))
- return false;
-
- _autoReconnect = false;
- _reconnectAttempts = 0;
- StartHeartbeat();
-
- Emit(Log, $"Connected to {portName} @ {BaudRate}");
- Emit(ConnectionChanged, true);
-
- // Give the board time to finish booting before the first command.
- Task.Delay(ResetSettleMs).ContinueWith(_ => Send("MODE3"));
- return true;
- }
-
- public void Disconnect()
- {
- _autoReconnect = false;
- StopHeartbeat();
- ClosePort(silent: false);
- }
-
- public void Send(string command)
- {
- try
- {
- lock (_gate)
- {
- if (_port == null || !_port.IsOpen)
- {
- Emit(Log, $"Cannot send '{command}' — not connected");
- return;
- }
- _port.Write(command + "\n");
- }
- Emit(Log, $"Sent: {command}");
- }
- catch (Exception ex)
- {
- Emit(Log, $"Send failed: {ex.Message}");
- }
- }
-
- ///
- /// Probes every port for a device answering REQUEST_ID with the expected ID.
- /// Returns the port name, or null. Runs off the UI thread; the caller
- /// connects on the main thread so no GLib call happens from a worker.
- ///
- public Task DetectPortAsync()
- {
- return Task.Run(() =>
- {
- foreach (string p in ListPorts())
- {
- if (IsOpen && p == PortName) continue;
-
- Emit(Log, $"Probing {p}...");
- string id;
- if (Probe(p, out id))
- {
- Emit(Log, $"Found {ExpectedDeviceId} on {p}");
- return p;
- }
- if (id != null)
- Emit(Log, $" {p}: reported '{id}' (not a match)");
- }
- return (string)null;
- });
- }
-
- // ---------- port plumbing ----------
-
- private bool OpenPort(string portName)
- {
- try
- {
- lock (_gate)
- {
- _port = new SerialPort(portName, BaudRate)
- {
- ReadTimeout = 500,
- WriteTimeout = 1000,
- NewLine = "\n",
- DtrEnable = true,
- RtsEnable = true,
- };
- _port.Open();
- }
-
- PortName = portName;
- _lastData = DateTime.Now;
- StartReader();
- return true;
- }
- catch (Exception ex)
- {
- Emit(Log, $"Open failed on {portName}: {ex.Message}");
- lock (_gate)
- {
- _port?.Dispose();
- _port = null;
- }
- return false;
- }
- }
-
- private void ClosePort(bool silent)
- {
- _reading = false;
-
- Thread t = _reader;
- _reader = null;
- if (t != null && t.IsAlive && t != Thread.CurrentThread)
- t.Join(1000);
-
- lock (_gate)
- {
- try { if (_port != null && _port.IsOpen) _port.Close(); }
- catch (Exception ex) { Emit(Log, $"Close error: {ex.Message}"); }
- _port?.Dispose();
- _port = null;
- }
-
- if (!silent)
- {
- Emit(Log, "Disconnected");
- Emit(ConnectionChanged, false);
- }
- }
-
- private void StartReader()
- {
- _reading = true;
- _reader = new Thread(ReaderLoop)
- {
- IsBackground = true,
- Name = "arduino-reader"
- };
- _reader.Start();
- }
-
- private void ReaderLoop()
- {
- var pending = new StringBuilder();
- var chunk = new byte[4096];
-
- while (_reading)
- {
- SerialPort p;
- lock (_gate) p = _port;
-
- if (p == null || !p.IsOpen) { Thread.Sleep(100); continue; }
-
- try
- {
- int n = p.Read(chunk, 0, chunk.Length);
- if (n <= 0) continue;
-
- _lastData = DateTime.Now;
- pending.Append(Encoding.ASCII.GetString(chunk, 0, n));
-
- string buffered = pending.ToString();
- int nl;
- while ((nl = buffered.IndexOf('\n')) >= 0)
- {
- string line = buffered.Substring(0, nl).Trim();
- buffered = buffered.Substring(nl + 1);
- if (line.Length > 0)
- Emit(LineReceived, line);
- }
- pending.Clear();
- pending.Append(buffered);
- }
- catch (TimeoutException) { /* normal when idle */ }
- catch (Exception ex)
- {
- if (_reading) Emit(Log, $"Read error: {ex.Message}");
- Thread.Sleep(200);
- }
- }
- }
-
- private bool Probe(string portName, out string deviceId)
- {
- deviceId = null;
- SerialPort test = null;
- try
- {
- test = new SerialPort(portName, BaudRate)
- {
- ReadTimeout = 500,
- WriteTimeout = 1000,
- NewLine = "\n",
- DtrEnable = true,
- RtsEnable = true,
- };
- test.Open();
- Thread.Sleep(ResetSettleMs);
- test.DiscardInBuffer();
- test.Write("REQUEST_ID\n");
-
- DateTime deadline = DateTime.Now.AddSeconds(3);
- while (DateTime.Now < deadline)
- {
- try
- {
- string line = test.ReadLine().Trim();
- if (line.StartsWith("DEVICE_ID:"))
- {
- deviceId = line.Substring("DEVICE_ID:".Length).Trim();
- return deviceId == ExpectedDeviceId;
- }
- }
- catch (TimeoutException) { }
- }
- }
- catch (Exception ex)
- {
- Emit(Log, $" {portName}: {ex.Message}");
- }
- finally
- {
- try { test?.Close(); } catch { }
- test?.Dispose();
- }
- return false;
- }
-
- // ---------- heartbeat / auto-reconnect (runs on the GTK main loop) ----------
-
- private void StartHeartbeat()
- {
- StopHeartbeat();
- _heartbeatId = GLib.Timeout.Add(HeartbeatCheckMs, OnHeartbeatTick);
- }
-
- private void StopHeartbeat()
- {
- if (_heartbeatId != 0)
- {
- GLib.Source.Remove(_heartbeatId);
- _heartbeatId = 0;
- }
- }
-
- private bool OnHeartbeatTick()
- {
- if (_autoReconnect && !IsOpen)
- {
- if (_reconnectAttempts >= MaxReconnectAttempts)
- {
- _autoReconnect = false;
- Log?.Invoke($"Reconnection failed after {MaxReconnectAttempts} attempts. Check USB cable, port and power, then reconnect manually.");
- StopHeartbeat();
- return false;
- }
-
- if (DateTime.Now >= _nextReconnect)
- {
- _reconnectAttempts++;
- Log?.Invoke($"Reconnect attempt {_reconnectAttempts}/{MaxReconnectAttempts} on {PortName}...");
-
- if (OpenPort(PortName))
- {
- _autoReconnect = false;
- _reconnectAttempts = 0;
- Log?.Invoke("Reconnected");
- ConnectionChanged?.Invoke(true);
- Task.Delay(ResetSettleMs).ContinueWith(_ => Send("MODE3"));
- }
- else
- {
- _nextReconnect = DateTime.Now.AddSeconds(2);
- }
- }
- return true;
- }
-
- if (!IsOpen) return true;
-
- if ((DateTime.Now - _lastData).TotalMilliseconds >= HeartbeatTimeoutMs)
- {
- Log?.Invoke("Heartbeat timeout — no device activity");
- ClosePort(silent: true);
- ConnectionChanged?.Invoke(false);
- _autoReconnect = true;
- _reconnectAttempts = 0;
- _nextReconnect = DateTime.Now.AddSeconds(2);
- }
- return true;
- }
-
- // ---------- helpers ----------
-
- private static void Emit(Action handler, T arg)
- {
- Action h = handler;
- if (h == null) return;
- Gtk.Application.Invoke((s, e) => h(arg));
- }
-
- public void Dispose()
- {
- _autoReconnect = false;
- StopHeartbeat();
- ClosePort(silent: true);
- }
-}
\ No newline at end of file
diff --git a/program/CaseDefinition.cs b/program/CaseDefinition.cs
deleted file mode 100644
index 39b9c85..0000000
--- a/program/CaseDefinition.cs
+++ /dev/null
@@ -1,143 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-
-/// One row of cases.csv.
-public class CaseDefinition
-{
- public int Number { get; set; } // Number
- public string Type { get; set; } // Type 心音 / 呼吸音
- public string CategoryJp { get; set; } // Category
- public string SubcategoryJp { get; set; } // Subcategory
- public string LocationJp { get; set; } // Location
- public string TreeLevel1 { get; set; } // Tree_Level1
- public string TreeLevel2 { get; set; } // Tree_Level2
- public string TreeLevel3 { get; set; } // Tree_Level3
- public string MapFront { get; set; } // Image_File
- public string SoundPath { get; set; } // Sound_File
- public string MapRight { get; set; } // Image_Right
- public string MapLeft { get; set; } // Image_Left
- public string MapBack { get; set; } // Image_Back
-
- public bool IsHeart
- {
- get { return !string.IsNullOrEmpty(Type) && Type.Contains("心"); }
- }
-
- /// Non-empty levels only, e.g. 呼吸音 → 正常呼吸音 → 気管音.
- /// Depth varies per row, which is what drives the drill-down.
- public string[] TreePath
- {
- get
- {
- var parts = new List();
- foreach (string s in new[] { Type, TreeLevel1, TreeLevel2, TreeLevel3 })
- if (!string.IsNullOrWhiteSpace(s)) parts.Add(s.Trim());
- return parts.ToArray();
- }
- }
-
- public override string ToString()
- {
- return Number + ": " + string.Join(" : ", TreePath);
- }
-
- // ── loader ─────────────────────────────────────────────────────────
-
- public static List LoadCasesFromCsv(string path)
- {
- var list = new List();
-
- if (!File.Exists(path))
- {
- Console.WriteLine("CSV not found: " + path);
- return list;
- }
-
- // Encoding.UTF8 with BOM detection. Reading Japanese as the default
- // ANSI codepage gives mojibake that only shows up on the buttons.
- string[] lines;
- try
- {
- lines = File.ReadAllLines(path, Encoding.UTF8);
- }
- catch (Exception ex)
- {
- Console.WriteLine("Failed to read " + path + ": " + ex.Message);
- return list;
- }
-
- if (lines.Length < 2)
- {
- Console.WriteLine("CSV has no data rows: " + path);
- return list;
- }
-
- // Your file is TAB separated; fall back to comma if it gets re-exported.
- char sep = lines[0].Contains("\t") ? '\t' : ',';
-
- // Header name -> column index, so adding or reordering columns is safe.
- // string[] header = lines[0].TrimStart('\uFEFF').Split(sep);
- string[] header = lines[0].TrimStart(new[] { '\uFEFF' }).Split(sep);
- var idx = new Dictionary(StringComparer.OrdinalIgnoreCase);
- for (int i = 0; i < header.Length; i++)
- {
- string key = header[i].Trim();
- if (key.Length > 0 && !idx.ContainsKey(key)) idx[key] = i;
- }
-
- if (!idx.ContainsKey("Number"))
- {
- Console.WriteLine("CSV header has no 'Number' column — wrong file or wrong delimiter?");
- return list;
- }
-
- Func get = (fields, name) =>
- {
- int i;
- if (!idx.TryGetValue(name, out i) || i >= fields.Length) return "";
- return fields[i].Trim();
- };
-
- int skipped = 0;
-
- for (int r = 1; r < lines.Length; r++)
- {
- if (string.IsNullOrWhiteSpace(lines[r])) continue;
-
- string[] f = lines[r].Split(sep);
-
- int number;
- if (!int.TryParse(get(f, "Number"), NumberStyles.Integer,
- CultureInfo.InvariantCulture, out number))
- {
- skipped++;
- Console.WriteLine("Line " + (r + 1) + ": bad Number, skipped");
- continue;
- }
-
- list.Add(new CaseDefinition
- {
- Number = number,
- Type = get(f, "Type"),
- CategoryJp = get(f, "Category"),
- SubcategoryJp = get(f, "Subcategory"),
- LocationJp = get(f, "Location"),
- TreeLevel1 = get(f, "Tree_Level1"),
- TreeLevel2 = get(f, "Tree_Level2"),
- TreeLevel3 = get(f, "Tree_Level3"),
- MapFront = get(f, "Image_File"),
- SoundPath = get(f, "Sound_File"),
- MapRight = get(f, "Image_Right"),
- MapLeft = get(f, "Image_Left"),
- MapBack = get(f, "Image_Back")
- });
- }
-
- Console.WriteLine("Loaded " + list.Count + " cases from " + path +
- (skipped > 0 ? " (" + skipped + " skipped)" : ""));
- return list;
- }
-}
\ No newline at end of file
diff --git a/program/JacketData.cs b/program/JacketData.cs
deleted file mode 100644
index fd9883c..0000000
--- a/program/JacketData.cs
+++ /dev/null
@@ -1,215 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-
-/// One chip position. Mirrors Form2.Circle.
-public class Circle
-{
- public float Number { get; set; }
- public int X { get; set; } // screen-space: OriginalX * 50 + MarginX
- public int Y { get; set; }
- public float OriginalX { get; set; }
- public float OriginalY { get; set; }
- public float OriginalZ { get; set; }
- public bool Inner { get; set; } // non-integer Number
-}
-
-/// Loads the 8 CSVs from JacketData/ — front|back|right|left x L|XL.
-public class JacketData
-{
- public const int CoordScale = 50; // Form2: originalX * 50
- public const int MarginX = 30;
- public const int MarginY = 30;
-
- private const string ConfigFile = "config.txt";
- private const string DefaultDir = "JacketData";
-
- private static readonly string[] Views = { "front", "back", "right", "left" };
- private static readonly string[] Sizes = { "L", "XL" };
-
- private readonly Dictionary> _l = new Dictionary>();
- private readonly Dictionary> _xl = new Dictionary>();
-
- public string Folder { get; private set; }
- public bool Loaded { get; private set; }
-
- /// Form2.GetCurrentCSVData()
- public List Get(string size, string view)
- {
- var dict = (size == "XL") ? _xl : _l;
- List list;
- return dict.TryGetValue(view, out list) ? list : new List();
- }
-
- public bool Has(string size, string view)
- {
- var dict = (size == "XL") ? _xl : _l;
- return dict.ContainsKey(view) && dict[view].Count > 0;
- }
-
- // ── folder resolution: config.txt, else JacketData/ next to the exe ──
-
- public static string ResolveFolder()
- {
- string cfg = SoundWindow.AppFile(ConfigFile);
-
- if (File.Exists(cfg))
- {
- try
- {
- string saved = File.ReadAllText(cfg).Trim();
- if (Directory.Exists(saved) && Validate(saved))
- {
- Logger.Write("jacket", "folder from config.txt: " + saved);
- return saved;
- }
-
- Logger.Write("jacket", "config.txt path invalid, ignoring: " + saved);
- }
- catch (Exception ex)
- {
- Logger.Write("jacket", "config.txt unreadable: " + ex.Message);
- }
- }
-
- return SoundWindow.AppFile(DefaultDir);
- }
-
- public static void SaveFolder(string folder)
- {
- try { File.WriteAllText(SoundWindow.AppFile(ConfigFile), folder); }
- catch (Exception ex) { Logger.Write("jacket", "could not save config.txt: " + ex.Message); }
- }
-
- /// Form1.ValidateCSVFiles — all 8 must be present.
- public static bool Validate(string folder)
- {
- foreach (string v in Views)
- foreach (string s in Sizes)
- if (!File.Exists(Path.Combine(folder, v + "_" + s + ".csv")))
- return false;
- return true;
- }
-
- public static string[] MissingFiles(string folder)
- {
- var missing = new List();
- foreach (string v in Views)
- foreach (string s in Sizes)
- {
- string name = v + "_" + s + ".csv";
- if (!File.Exists(Path.Combine(folder, name))) missing.Add(name);
- }
- return missing.ToArray();
- }
-
- // ── loading ─────────────────────────────────────────────────────────
-
- public static JacketData Load()
- {
- return Load(ResolveFolder());
- }
-
- public static JacketData Load(string folder)
- {
- var data = new JacketData();
- data.Folder = folder;
-
- if (!Directory.Exists(folder))
- {
- Logger.Write("jacket", "folder not found: " + folder);
- return data;
- }
-
- string[] missing = MissingFiles(folder);
- if (missing.Length > 0)
- Logger.Write("jacket", "missing " + missing.Length + " file(s): " + string.Join(", ", missing));
-
- foreach (string view in Views)
- {
- foreach (string size in Sizes)
- {
- string path = Path.Combine(folder, view + "_" + size + ".csv");
- var circles = ReadCsv(path);
- if (circles.Count == 0) continue;
-
- if (size == "XL") data._xl[view] = circles;
- else data._l[view] = circles;
-
- Logger.Write("jacket", "loaded " + circles.Count + " chips from " + view + "_" + size + ".csv");
- }
- }
-
- data.Loaded = (data._l.Count > 0 || data._xl.Count > 0);
- if (!data.Loaded) Logger.Write("jacket", "NO chip data loaded from " + folder);
-
- return data;
- }
-
- /// Columns: Number, OriginalX, OriginalY, OriginalZ. Header optional.
- private static List ReadCsv(string path)
- {
- var circles = new List();
-
- if (!File.Exists(path))
- {
- Logger.Write("jacket", "file not found: " + path);
- return circles;
- }
-
- string[] lines;
- try { lines = File.ReadAllLines(path); }
- catch (Exception ex)
- {
- Logger.Write("jacket", "read failed " + path + ": " + ex.Message);
- return circles;
- }
-
- int start = 0;
- if (lines.Length > 0)
- {
- string h = lines[0].ToLower();
- if (h.Contains("number") || h.Contains("originalx")) start = 1;
- }
-
- for (int i = start; i < lines.Length; i++)
- {
- string line = lines[i].Trim();
- if (line.Length == 0) continue;
-
- string[] p = line.Split(',');
- if (p.Length < 4)
- {
- Logger.Write("jacket", path + " line " + (i + 1) + ": only " + p.Length + " columns");
- continue;
- }
-
- float number, ox, oy, oz;
- var inv = CultureInfo.InvariantCulture;
-
- // InvariantCulture matters — under ja_JP/de_DE, "1.5" can fail to parse.
- if (!float.TryParse(p[0].Trim(), NumberStyles.Float, inv, out number) ||
- !float.TryParse(p[1].Trim(), NumberStyles.Float, inv, out ox) ||
- !float.TryParse(p[2].Trim(), NumberStyles.Float, inv, out oy) ||
- !float.TryParse(p[3].Trim(), NumberStyles.Float, inv, out oz))
- {
- Logger.Write("jacket", path + " line " + (i + 1) + ": bad number format");
- continue;
- }
-
- circles.Add(new Circle
- {
- Number = number,
- X = (int)(ox * CoordScale) + MarginX,
- Y = (int)(oy * CoordScale) + MarginY,
- OriginalX = ox,
- OriginalY = oy,
- OriginalZ = oz,
- Inner = (number % 1 != 0)
- });
- }
-
- return circles;
- }
-}
\ No newline at end of file
diff --git a/program/Language.cs b/program/Language.cs
deleted file mode 100644
index e6a7bfc..0000000
--- a/program/Language.cs
+++ /dev/null
@@ -1,161 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using Gtk;
-
-public class Language
-{
- private const string EngCssPath = "lang_eng.css";
- private const string JpnCssPath = "lang_jpn.css";
- private const string EngCsvPath = "info_eng.csv";
- private const string JpnCsvPath = "info_jpn.csv";
- private const string TouchCssPath = "touch_friendly.css"; // NEW
-
- private readonly Dictionary _engText;
- private readonly Dictionary _jpnText;
- private Dictionary _text;
-
- // widgets whose label is a plain CSV lookup
- private readonly Dictionary _widgets = new Dictionary();
-
- public readonly CssProvider _languageCssProvider = new CssProvider();
- private readonly CssProvider _touchCssProvider = new CssProvider(); // NEW
-
- public bool IsEnglish { get; private set; }
-
- /// Raised after every language change, so callers can rebuild state-dependent text.
- public event EventHandler Changed;
-
- public Language(bool isEnglish)
- {
- // Touch-friendly sizing loads once, at a lower priority than the
- // language CSS (600) so per-language #id rules can still override
- // fonts/colors if needed, while touch sizing stays constant.
- if (File.Exists(TouchCssPath))
- {
- _touchCssProvider.LoadFromPath(TouchCssPath);
- Console.WriteLine("Touch CSS loaded from: " + System.IO.Path.GetFullPath(TouchCssPath));
- }
- else
- {
- Console.WriteLine("Warning: CSS file not found: " + TouchCssPath);
- }
- StyleContext.AddProviderForScreen(Gdk.Screen.Default, _touchCssProvider, 1000);
- StyleContext.AddProviderForScreen(Gdk.Screen.Default, _languageCssProvider, 600);
-
- _engText = LoadText(EngCsvPath);
- _jpnText = LoadText(JpnCsvPath);
-
- // set before any widget is built, so ctor-time lookups are already correct
- IsEnglish = isEnglish;
- _text = isEnglish ? _engText : _jpnText;
- }
-
- // ---------- registration ----------
-
- /// Label comes straight from the CSV key. Applied automatically on every change.
- public void Register(string key, Widget widget)
- {
- if (widget == null) return;
- widget.Name = key; // enables #Key selectors in the lang CSS
- _widgets[key] = widget;
- }
-
- /// Widget whose text the owner sets itself (state-dependent). Name only, no auto-apply.
- public void RegisterDynamic(string key, Widget widget)
- {
- if (widget != null) widget.Name = key;
- }
-
- // ---------- lookup ----------
-
- public string this[string key] { get { return Get(key); } }
-
- public string Get(string key)
- {
- string value;
- if (_text.TryGetValue(key, out value)) return value;
-
- Console.WriteLine("Warning: missing text key '" + key + "'");
- return key;
- }
-
- public string Format(string key, params object[] args)
- {
- string template = Get(key);
- try
- {
- return string.Format(CultureInfo.InvariantCulture, template, args);
- }
- catch (FormatException)
- {
- Console.WriteLine("Warning: bad placeholder in text key '" + key + "'");
- return template;
- }
- }
-
- // ---------- apply ----------
-
- public void Apply(bool isEnglish)
- {
- IsEnglish = isEnglish;
- _text = isEnglish ? _engText : _jpnText;
-
- foreach (var kvp in _widgets)
- {
- string value;
- if (_text.TryGetValue(kvp.Key, out value))
- SetText(kvp.Value, value);
- else
- Console.WriteLine("Warning: missing text key '" + kvp.Key + "'");
- }
-
- string cssPath = isEnglish ? EngCssPath : JpnCssPath;
- if (File.Exists(cssPath))
- _languageCssProvider.LoadFromPath(cssPath);
- else
- Console.WriteLine("Warning: CSS file not found: " + cssPath);
-
- EventHandler handler = Changed;
- if (handler != null) handler(this, EventArgs.Empty);
- }
-
- private static void SetText(Widget widget, string value)
- {
- Button button = widget as Button; // also covers ToggleButton
- if (button != null) { button.Label = value; return; }
-
- Label label = widget as Label;
- if (label != null) { label.Text = value; return; }
-
- Console.WriteLine("Warning: don't know how to set text on " + widget.GetType().Name);
- }
-
- // ---------- csv ----------
-
- private Dictionary LoadText(string path)
- {
- var result = new Dictionary();
-
- if (!File.Exists(path))
- {
- Console.WriteLine("Warning: text file not found: " + path);
- return result;
- }
-
- string[] lines = File.ReadAllLines(path);
- for (int i = 1; i < lines.Length; i++) // row 0 is the header
- {
- string line = lines[i].Trim();
- if (string.IsNullOrEmpty(line) || line.StartsWith("#")) continue;
-
- string[] parts = line.Split(new[] { ',' }, 2);
- if (parts.Length < 2) continue;
-
- result[parts[0].Trim()] = parts[1]; // value keeps any commas
- }
-
- return result;
- }
-}
\ No newline at end of file
diff --git a/program/Logger.cs b/program/Logger.cs
deleted file mode 100644
index 85b0efe..0000000
--- a/program/Logger.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System;
-using System.IO;
-using System.Text;
-
-/// Append-only text log next to the .exe: logs/ears-YYYYMMDD.log
-public static class Logger
-{
- private static readonly object _gate = new object();
- private static string _path;
- private static bool _failed;
-
- public static void Write(string message)
- {
- Write(null, message);
- }
-
- public static void Write(string tag, string message)
- {
- if (_failed) return;
-
- string line = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") +
- (string.IsNullOrEmpty(tag) ? " " : " [" + tag + "] ") +
- message;
-
- lock (_gate)
- {
- try
- {
- if (_path == null) _path = Init();
- File.AppendAllText(_path, line + Environment.NewLine, Encoding.UTF8);
- }
- catch (Exception ex)
- {
- _failed = true; // never let logging crash the app
- Console.WriteLine("[Logger] disabled: " + ex.Message);
- }
- }
- }
-
- public static void Write(string tag, string format, params object[] args)
- {
- Write(tag, string.Format(format, args));
- }
-
- public static void Exception(string tag, Exception ex)
- {
- Write(tag, ex.GetType().Name + ": " + ex.Message);
- Write(tag, ex.StackTrace ?? "(no stack)");
- }
-
- private static string Init()
- {
- string dir = Path.Combine(
- Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location),
- "logs");
-
- Directory.CreateDirectory(dir);
-
- string path = Path.Combine(dir, "ears-" + DateTime.Now.ToString("yyyyMMdd") + ".log");
-
- File.AppendAllText(path,
- Environment.NewLine +
- "=== session start " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") +
- " (" + Environment.OSVersion.Platform + ") ===" + Environment.NewLine,
- Encoding.UTF8);
-
- return path;
- }
-}
\ No newline at end of file
diff --git a/program/MainWindow.cs b/program/MainWindow.cs
deleted file mode 100644
index b7828c5..0000000
--- a/program/MainWindow.cs
+++ /dev/null
@@ -1,432 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using Gtk;
-using UI = Gtk.Builder.ObjectAttribute;
-using System.Runtime.InteropServices;
-
-public class MainWindow : Window
-{
-
- // ---- stack + pages (new all-in-one glade) ----
- [UI] private Notebook RootNoteBook = null;
-
- 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;
- [UI] private Button Option2Button = null;
- [UI] private Button Option3Button = null;
- // [UI] private Switch LanguageSwitch = null;
- [UI] private ToggleButton LanguageToggle = null;
- [UI] private Label LanguageLabel = null;
- [UI] private Button CloseButton = null;
- [UI] private Button SettingButton = null;
- [UI] private Image ConnectionIcon = null; // was GtkIconView in the old glade
-
- [UI] private Label ConnectionAlertLabel = null;
-
- private uint _retryTimerId;
- private bool _probing;
- private const uint RetryMs = 1000;
-
- private CssProvider _scaleCssProvider;
- // private CssProvider _languageCssProvider;
- private int _lastAppliedWidth = -1;
-
- private const int BaseWidth = 1920;
- private const int BaseHeight = 1080;
-
- // private const string EngCssPath = "lang_eng.css";
- // private const string JpnCssPath = "lang_jpn.css";
-
- // private Dictionary _engText;
- // private Dictionary _jpnText;
- // private Dictionary _buttonsById;
- private bool _connected;
-
- private readonly ArduinoConnection _arduino = new ArduinoConnection();
- internal SettingsWindow _settingsWindow;
- internal Language _Language;
-
- internal SoundWindow _soundWindow;
- internal MapWindow _mapWindow;
-
- public MainWindow() : this(CreateBuilder()) { }
-
- private static Builder CreateBuilder()
- {
- var builder = new Builder();
- builder.AddFromFile("homepageallv7.glade");
- return builder;
- }
-
- private MainWindow(Builder builder) : base(builder.GetObject("Root").Handle)
- {
- builder.Autoconnect(this);
-
- _scaleCssProvider = new CssProvider();
- // _languageCssProvider = new CssProvider();
-
- StyleContext.AddProviderForScreen(Gdk.Screen.Default, _scaleCssProvider, 600);
- // StyleContext.AddProviderForScreen(Gdk.Screen.Default, _Language._languageCssProvider, 600);
-
- // // make it full screen
- // this.Fullscreen();
- //use the linux computer resolution
- this.SetDefaultSize(1280, 800);
- this.SetPosition(WindowPosition.Center);
-
- // RootStack.TransitionType = StackTransitionType.None;
- // RootStack.TransitionType = StackTransitionType.SlideLeftRight;
- // RootStack.TransitionDuration = 150;
-
- Option1Button.Clicked += OnNormalSessionClicked;
- Option2Button.Clicked += OnPositionRecordingClicked;
- Option3Button.Clicked += OnExamModeClicked;
- CloseButton.Clicked += OnCloseClicked;
- SettingButton.Clicked += OnSettingClicked;
- // LanguageSwitch.AddNotification("active", OnLanguageSwitchNotify);
- LanguageToggle.Toggled += OnLanguageToggled;
-
- DeleteEvent += OnDeleteEvent;
- SizeAllocated += OnWindowSizeAllocated;
-
- _arduino.ConnectionChanged += OnArduinoConnectionChanged;
-
-
- // // _buttonsById = new Dictionary
- // // {
- // // { "Option1Button", Option1Button },
- // // { "Option2Button", Option2Button },
- // // { "Option3Button", Option3Button },
- // // { "CloseButton", CloseButton },
- // // { "SettingButton", SettingButton },
- // // };
-
- // // CSS ID selectors (#Option1Button etc.) need the widget Name set explicitly
- // foreach (var kvp in _buttonsById)
- // kvp.Value.Name = kvp.Key;
-
- // // _engText = LoadButtonText("info_eng.csv");
- // // _jpnText = LoadButtonText("info_jpn.csv");
-
- // _Language = new Language(_buttonsById,LanguageLabel);
-
- // // _Language.ApplyLanguage(LanguageSwitch.Active);
-
- _Language = new Language(LanguageToggle.Active);
- _Language.Register("Option1Button", Option1Button);
- _Language.Register("Option2Button", Option2Button);
- _Language.Register("Option3Button", Option3Button);
- _Language.Register("CloseButton", CloseButton);
- _Language.Register("SettingButton", SettingButton);
- _Language.Register("LanguageLabel", LanguageLabel);
- _Language.Register("LanguageToggle", LanguageToggle);
- _Language.RegisterDynamic("ConnectionIcon", ConnectionIcon);
- ConnectionAlertLabel.Name = "ConnectionAlertLabel";
- _Language.Register("ConnectionAlertLabel", ConnectionAlertLabel);
-
- _settingsWindow = new SettingsWindow(builder, _arduino, _Language);
- _settingsWindow.BackRequested += OnSettingsClosed;
-
- _mapWindow = new MapWindow(builder, _arduino, _Language);
- _mapWindow.BackRequested += OnMapClosed;
-
- _Language.Changed += (s, e) => OnArduinoConnectionChanged(_connected);
- _Language.Apply(LanguageToggle.Active);
-
- // // single source of truth: the toggle drives both pages
- // ApplyLanguageEverywhere(LanguageToggle.Active);
-
- RootNoteBook.ShowTabs = false;
- RootNoteBook.ShowBorder = false;
- RootNoteBook.CurrentPage = PageSplash;
- _soundWindow = new SoundWindow(builder);
- _soundWindow.BackRequested += (s, e) => RootNoteBook.CurrentPage = PageSplash;
- _soundWindow.CaseSelected += OnCaseSelected;
-
-
- // OnArduinoConnectionChanged(false);
-
- StartAutoDetectOnLoad();
-
- if (AppConfig.DebugMode)
- this.Title = (this.Title ?? "App") + " [DEBUG MODE]";
- }
-
-
-
- // ---------- settings panel ----------
-
- private void OnSettingClicked(object sender, EventArgs e)
- {
- // if (_settingsWindow == null)
- // {
- // // _settingsWindow = new SettingsWindow(_arduino);
- // // _settingsWindow.BackRequested += OnSettingsClosed;
- // // _settingsWindow.ShowAll(); // build it fullscreen once, up front
- // // _settingsWindow.Fullscreen();
- // RootNoteBook.CurrentPage = PageSettings;
- // }
-
-
- // _settingsWindow.ShowAll();
- // _settingsWindow.Present();
- // this.Hide();
- RootNoteBook.CurrentPage = PageSettings;
- }
-
- private void OnSettingsClosed(object sender, EventArgs e)
- {
- // this.ShowAll();
- // this.Present();
- // _settingsWindow.Hide();
- RootNoteBook.CurrentPage = PageSplash;
- }
-
- private async void StartAutoDetectOnLoad()
- {
- // Lock down splash options until we know the device state.
- OnArduinoConnectionChanged(false);
-
- string found = await _arduino.DetectPortAsync();
-
- if (found != null)
- _arduino.Connect(found); // fires ConnectionChanged(true) -> unlocks options
- else
- OnArduinoConnectionChanged(false); // still no device -> stays locked unless debug
- }
-
- private void OnArduinoConnectionChanged(bool connected)
- {
- _connected = connected;
-
- // Debug mode bypasses the "device required" restriction entirely.
- bool allowFullAccess = connected || AppConfig.DebugMode;
-
- Option1Button.Sensitive = allowFullAccess;
- Option2Button.Sensitive = allowFullAccess;
- Option3Button.Sensitive = allowFullAccess;
- // SettingButton.Sensitive = true; // always reachable, device or not
-
- if (ConnectionAlertLabel != null)
- {
- ConnectionAlertLabel.Visible = !connected;
- var ctx = ConnectionAlertLabel.StyleContext;
- if (connected) ctx.RemoveClass("alert");
- else ctx.AddClass("alert");
- }
-
- if (connected || AppConfig.DebugMode) StopAutoRetry();
- else StartAutoRetry();
-
- if (ConnectionIcon == null) return;
-
- ConnectionIcon.SetFromIconName(
- connected ? "network-transmit-receive" : "network-offline",
- IconSize.Dnd);
-
- ConnectionIcon.TooltipText = connected
- ? _Language.Format("ConnectionIcon.Connected", _arduino.PortName)
- : _Language["ConnectionIcon.Disconnected"];
- }
-
- private void StartAutoRetry()
- {
- if (_retryTimerId != 0) return;
- _retryTimerId = GLib.Timeout.Add(RetryMs, OnRetryTick);
- }
-
- private void StopAutoRetry()
- {
- if (_retryTimerId != 0)
- {
- GLib.Source.Remove(_retryTimerId);
- _retryTimerId = 0;
- }
- }
-
- private bool OnRetryTick()
- {
- if (_arduino.IsOpen) { _retryTimerId = 0; return false; }
- if (_probing) return true; // scan still running — don't stack them
-
- _probing = true;
- _arduino.DetectPortAsync().ContinueWith(t =>
- {
- string port = t.IsFaulted ? null : t.Result;
- // DetectPortAsync runs off the UI thread; Connect() touches GLib.
- Gtk.Application.Invoke((s, e) =>
- {
- _probing = false;
- if (port != null && !_arduino.IsOpen)
- _arduino.Connect(port); // fires ConnectionChanged(true)
- });
- });
- return true;
- }
-
- // ---------- localisation ----------
-
- // private Dictionary LoadButtonText(string path)
- // {
- // var result = new Dictionary();
-
- // if (!File.Exists(path))
- // {
- // Console.WriteLine($"Warning: text file not found: {path}");
- // return result;
- // }
-
- // var lines = File.ReadAllLines(path);
- // for (int i = 1; i < lines.Length; i++) // skip header row
- // {
- // var line = lines[i].Trim();
- // if (string.IsNullOrEmpty(line)) continue;
-
- // var parts = line.Split(new[] { ',' }, 2);
- // if (parts.Length < 2) continue;
-
- // result[parts[0]] = parts[1];
- // }
-
- // return result;
- // }
-
- // private void ApplyLanguage(bool isEnglish)
- // {
- // var text = isEnglish ? _engText : _jpnText;
- // string cssPath = isEnglish ? EngCssPath : JpnCssPath;
-
- // foreach (var kvp in _buttonsById)
- // {
- // if (text.TryGetValue(kvp.Key, out var buttonText))
- // kvp.Value.Label = buttonText;
- // }
-
- // if (File.Exists(cssPath))
- // _languageCssProvider.LoadFromPath(cssPath);
- // else
- // Console.WriteLine($"Warning: CSS file not found: {cssPath}");
-
- // LanguageLabel.Text = isEnglish ? "あ" : "A";
- // }
-
- // ---------- scaling ----------
-
- private void OnWindowSizeAllocated(object o, SizeAllocatedArgs args)
- {
- int width = args.Allocation.Width;
- int height = args.Allocation.Height;
-
- if (width == _lastAppliedWidth) return;
- _lastAppliedWidth = width;
-
- double scaleX = (double)width / BaseWidth;
- double scaleY = (double)height / BaseHeight;
-
- ApplyScale(Math.Min(scaleX, scaleY));
- }
-
- private void ApplyScale(double scale)
- {
- int fontSize = Math.Max(10, (int)(20 * scale));
- int buttonPadV = Math.Max(4, (int)(12 * scale));
- int buttonPadH = Math.Max(8, (int)(24 * scale));
- int switchWidth = Math.Max(30, (int)(40 * scale));
- int switchHeight= Math.Max(18, (int)(24 * scale));
-
- // NOTE: integers + InvariantCulture. Interpolating a double here emitted
- // "40,5px" under ja_JP / de_DE locales and GTK silently dropped the rule.
- string css = string.Format(CultureInfo.InvariantCulture, @"
- grid, label, button, switch {{
- font-size: {0}px;
- }}
- button {{
- padding: {1}px {2}px;
- }}
- switch {{
- min-width: {3}px;
- min-height: {4}px;
- }}
- ", fontSize, buttonPadV, buttonPadH, switchWidth, switchHeight);
-
- _scaleCssProvider.LoadFromData(css);
- }
-
- // ---------- lifecycle ----------
-
- private void OnDeleteEvent(object sender, DeleteEventArgs a)
- {
- if (_mapWindow != null) _mapWindow.StopSession();
- Application.Quit();
- a.RetVal = true;
- // Shutdown();
- // a.RetVal = true;
- }
-
- private void OnCloseClicked(object sender, EventArgs e)
- {
- Shutdown();
- }
-
- private void Shutdown()
- {
- _arduino.Disconnect();
- _arduino.Dispose();
- // _settingsWindow?.Destroy();
- Application.Quit();
- }
-
- private void OnNormalSessionClicked(object sender, EventArgs e)
- {
- Console.WriteLine("Normal Session selected");
- RootNoteBook.CurrentPage = PageSound;
- _soundWindow.Reset();
- }
-
- private void OnPositionRecordingClicked(object sender, EventArgs e)
- {
- Console.WriteLine("Position Recording Mode selected");
- }
-
- private void OnExamModeClicked(object sender, EventArgs e)
- {
- Console.WriteLine("Exam Mode selected");
- }
-
- private void OnLanguageToggled(object sender, EventArgs e)
- {
- _Language.Apply(LanguageToggle.Active);
- }
-
- // private void ApplyLanguageEverywhere(bool isEnglish)
- // {
- // Language.LanguageSwitchValue = isEnglish;
- // _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;
- _mapWindow.ShowCase(def, string.Join(" : ", def.TreePath));
- 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
deleted file mode 100644
index af71404..0000000
--- a/program/MapWindow.cs
+++ /dev/null
@@ -1,1337 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using Gtk;
-using UI = Gtk.Builder.ObjectAttribute;
-
-public class MapWindow : IDisposable
-{
- private const float FRONT_Z = 1.0f;
- private const float BACK_Z = -1.0f;
- private const float RIGHT_Z = 0.5f;
- private const float LEFT_Z = -0.5f;
- private const float Z_TOLERANCE = 0.1f;
-
- // ══════════════════════════════════════════════════════════════════
- // TUNABLES — per (jacket size, view). Absolute values, subtracted.
- // Paste a CalibrationWindow dump into the tuner to overwrite these,
- // then "Dump seed" to bake the result back into SeedDefaults().
- // ══════════════════════════════════════════════════════════════════
-
- public class Tune
- {
- public double OffsetX = 25.0; // circle cluster shift (SUBTRACTED)
- public double OffsetY = 50.0;
- public double SizeScale = 1.0; // circle diameter multiplier
- public double Cluster = 1.0; // 1.0 = no clustering
- public double MapZoom = 1.0; // see MapFit mode
- public double MapX = 0.0; // map nudge, body pixels
- public double MapY = 0.0;
- public double MapAlpha = 0.7;
- public double FitMargin = 40.0; // Form2: panel − 40 (front: −100)
- // cursor calibration
- public double MarginX = 0.0;
- public double MarginY = 0.0;
-
- public Tune Clone() { return (Tune)MemberwiseClone(); }
- }
-
- /// How the MAP is sized over the body.
- /// Native546 — Form2 behaviour: 546x546 drawn at native size, centred.
- /// MapZoom multiplies that. MapZoom 1.0 == today's output.
- /// FitBody — CalibrationWindow behaviour: raw map scaled to fit the
- /// body, then x MapZoom. Use this to verify a dump.
- public enum MapFit { Native546, FitBody }
-
- private readonly Dictionary _tunes = new Dictionary();
- private MapFit _mapFit = MapFit.FitBody;
-
- private string TuneKey { get { return JacketSize + ":" + _view; } }
-
- private Tune T
- {
- get
- {
- string k = TuneKey;
- if (!_tunes.ContainsKey(k)) _tunes[k] = new Tune();
- return _tunes[k];
- }
- }
-
- private void SeedDefaults()
- {
- // ── L ────────────────────────────────────────────────────────
- Seed("L:front", 9.0, 34.0, 2.75, 1.10, 95, 1.060, -4.0, -14.0, 0.70);
- Seed("L:back", -2.0, 10.0, 1.95, 0.90, 40, 1.090, 2.0, -7.0, 0.70);
- Seed("L:right", 16.0, -106.0, 1.00, 0.50, 40, 1.010, -32.0, 71.0, 0.70);
- Seed("L:left", -14.0, -110.0, 0.80, 0.40, 15, 1.050, 0.0, 5.0, 0.70);
-
- // ── XL ── (converted from the old XL_* deltas: L_offset − delta)
- Seed("XL:front", 7.0, 17.0, 2.30, 1.05, 50, 1.060, -4.0, -14.0, 0.70);
- Seed("XL:back", 5.0, 10.0, 1.95, 0.90, 40, 1.090, 2.0, -7.0, 0.70);
- Seed("XL:right", 10.0, -108.0, 0.80, 0.40, 40, 1.010, -32.0, 71.0, 0.70);
- Seed("XL:left", -4.0, -101.0, 0.80, 0.40, 40, 1.050, 0.0, 5.0, 0.70);
- }
-
- private void Seed(string key, double ox, double oy, double size, double cluster,
- double fitMargin, double zoom, double mx, double my, double alpha)
- {
- _tunes[key] = new Tune
- {
- OffsetX = ox, OffsetY = oy, SizeScale = size, Cluster = cluster,
- FitMargin = fitMargin, MapZoom = zoom, MapX = mx, MapY = my, MapAlpha = alpha
- };
- }
-
- private const int CircleDiameter = 50;
- private const int MapSize = 546; // Form2: new Bitmap(mapImage, 546, 546)
-
- // ── glade widgets ──
- [UI] private DrawingArea MapArea = null;
- [UI] private Label MapTitleLabel = null;
- [UI] private Label MapCaseLabel = null;
- [UI] private Button MapBackButton = null;
- [UI] private RadioButton BodySkeletonRadio = null;
- [UI] private RadioButton BodyJacketRadio = null;
- [UI] private ComboBoxText JacketSizeCombo = null;
- [UI] private RadioButton ViewFrontRadio = null;
- [UI] private RadioButton ViewBackRadio = null;
- [UI] private RadioButton ViewRightRadio = null;
- [UI] private RadioButton ViewLeftRadio = null;
- [UI] private CheckButton HideCircleCheck = null;
-
- // NEW — needed only so the map-page CSS can reach them.
- [UI] private Button SoundTestButton = null;
- [UI] private CheckButton BlackAudioCheck = null;
- [UI] private Label JacketSizeLabel = null;
-
- private readonly Language _lang;
- private readonly SoundLooper _sound = new SoundLooper();
-
- // ── state ──
- private Gdk.Pixbuf _body; // background, native size
- private Gdk.Pixbuf _map; // 546x546, black keyed — audio reference
- private Gdk.Pixbuf _mapRaw; // keyed, ORIGINAL size — used by FitBody mode
- private CaseDefinition _case;
- private string _view = "front";
- private readonly ArduinoConnection _arduino;
-
- // ── live Arduino position ──
- private float _currentX, _currentY, _currentZ;
- private bool _hasPosition;
-
- private static readonly Regex FullRx = new Regex(
- @"(\d+(?:\.\d+)?)\(([+-]?\d+(?:\.\d+)?),([+-]?\d+(?:\.\d+)?),([+-]?\d+(?:\.\d+)?)\)",
- RegexOptions.Compiled);
- private static readonly Regex AvgRx = new Regex(
- @"Avg(\d+(?:\.\d+)?)\(([+-]?\d+(?:\.\d+)?),([+-]?\d+(?:\.\d+)?),([+-]?\d+(?:\.\d+)?)\)",
- RegexOptions.Compiled);
- private string _bodyPath = "(none)";
- private string _mapPath = "(none)";
-
- public event EventHandler BackRequested;
-
- private JacketData _jacket;
- private List _highlighted = new List();
- private static bool _listed;
-
- // ── tuner state ──
- private Window _tuner;
- private TextView _tunerOut;
- private Label _tunerInfo, _tunerKey;
- private readonly Dictionary _spins = new Dictionary();
- private CheckButton _tShowBody, _tShowMap, _tShowCircles, _tGuides;
- private ComboBoxText _tFitCombo;
- private bool _building;
- private bool _keysHooked;
-
- private bool ShowBody { get { return _tShowBody == null || _tShowBody.Active; } }
- private bool ShowMap { get { return _tShowMap == null || _tShowMap.Active; } }
- private bool ShowCircles { get { return (_tShowCircles == null || _tShowCircles.Active)
- && !HideCircleCheck.Active; } }
- private bool ShowGuides { get { return _tGuides != null && _tGuides.Active; } }
-
- // ══════════════════════════════════════════════════════════════════
-
- public MapWindow(Builder builder, ArduinoConnection arduino, Language language)
- {
- builder.Autoconnect(this);
- _arduino = arduino;
-
- SeedDefaults();
- LoadCalibFile(false); // calib.txt overrides seeds if present
-
- _jacket = JacketData.Load();
- Logger.Write("jacket", "folder: " + _jacket.Folder + " loaded=" + _jacket.Loaded);
-
- if (MapArea == null || MapTitleLabel == null || MapBackButton == null)
- throw new InvalidOperationException(
- "Glade id mismatch — MapArea=" + (MapArea != null) +
- " MapTitleLabel=" + (MapTitleLabel != null) +
- " MapBackButton=" + (MapBackButton != null));
-
- _lang = language;
-
- // ── styling hooks ─────────────────────────────────────────────
- // GTK3's Builder does NOT copy the glade id into the CSS name, so
- // #Id selectors match nothing unless we assign Name ourselves. The
- // shared "mappage" class then lets touch_friendly.css restyle this
- // page without touching the splash / sound / settings pages.
- Widget[] mapWidgets = {
- MapArea, MapTitleLabel, MapCaseLabel, MapBackButton,
- BodySkeletonRadio, BodyJacketRadio, JacketSizeCombo, JacketSizeLabel,
- ViewFrontRadio, ViewBackRadio, ViewRightRadio, ViewLeftRadio,
- HideCircleCheck, BlackAudioCheck, SoundTestButton
- };
- foreach (var w in mapWidgets)
- if (w != null) w.StyleContext.AddClass("mappage");
-
- SetName(MapArea, "MapArea");
- SetName(MapTitleLabel, "MapTitleLabel");
- SetName(MapCaseLabel, "MapCaseLabel");
- SetName(BodySkeletonRadio, "BodySkeletonRadio");
- SetName(BodyJacketRadio, "BodyJacketRadio");
- SetName(JacketSizeCombo, "JacketSizeCombo");
- SetName(JacketSizeLabel, "JacketSizeLabel");
- SetName(ViewFrontRadio, "ViewFrontRadio");
- SetName(ViewBackRadio, "ViewBackRadio");
- SetName(ViewRightRadio, "ViewRightRadio");
- SetName(ViewLeftRadio, "ViewLeftRadio");
- SetName(HideCircleCheck, "HideCircleCheck");
- SetName(BlackAudioCheck, "BlackAudioCheck");
- SetName(SoundTestButton, "SoundTestButton");
-
- // Register() assigns Name itself and pulls the label from the CSVs.
- _lang.Register("MapBackButton", MapBackButton);
-
- // ── signals ───────────────────────────────────────────────────
- MapArea.Drawn += OnMapDrawn;
- MapBackButton.Clicked += OnBackClicked;
-
- BodySkeletonRadio.Toggled += (s, e) => { if (BodySkeletonRadio.Active) ReloadBody(); };
- BodyJacketRadio.Toggled += (s, e) => { if (BodyJacketRadio.Active) ReloadBody(); };
- JacketSizeCombo.Changed += (s, e) => { ReloadBody(); SyncSpins(); MapArea.QueueDraw(); };
-
- HookView(ViewFrontRadio, "front");
- HookView(ViewBackRadio, "back");
- HookView(ViewRightRadio, "right");
- HookView(ViewLeftRadio, "left");
-
- HideCircleCheck.Toggled += (s, e) => MapArea.QueueDraw();
-
- // Ctrl+T anywhere, or Ctrl+double-click on the map, opens the tuner.
- MapArea.Realized += (s, e) => HookKeys();
- MapArea.AddEvents((int)Gdk.EventMask.ButtonPressMask);
- MapArea.ButtonPressEvent += (o, a) =>
- {
- if (a.Event.Type == Gdk.EventType.TwoButtonPress &&
- (a.Event.State & Gdk.ModifierType.ControlMask) != 0)
- ToggleTuner();
- };
-
- _sound.Log += msg => Logger.Write("sound", msg);
- // NEW — live position feed
- _arduino.LineReceived += OnArduinoLine;
- _arduino.ConnectionChanged += OnArduinoConnectionChanged;
- }
-
- /// Null-safe: SoundTestButton / BlackAudioCheck / JacketSizeLabel are
- /// optional — an older glade without them must still load.
- private static void SetName(Widget w, string name)
- {
- if (w != null) w.Name = name;
- }
-
- private void HookKeys()
- {
- if (_keysHooked) return;
- var top = MapArea.Toplevel as Window;
- if (top == null) return;
- _keysHooked = true;
-
- top.KeyPressEvent += (o, a) =>
- {
- if ((a.Event.State & Gdk.ModifierType.ControlMask) == 0) return;
- if (a.Event.Key == Gdk.Key.t || a.Event.Key == Gdk.Key.T)
- ToggleTuner();
- };
- }
-
- private void HookView(RadioButton rb, string view)
- {
- if (rb == null) return;
- rb.Toggled += (s, e) => { if (rb.Active) SetView(view); };
- }
-
- // ══════════════════════════════════════════════════════════════════
- // ARDUINO POSITION
- // ══════════════════════════════════════════════════════════════════
-
- private void OnArduinoLine(string line)
- {
- var fullMatches = FullRx.Matches(line);
- var avgMatch = AvgRx.Match(line);
-
- float avgChipNumber = -1f;
- float x = 0, y = 0, z = 0;
- bool havePosition = false;
-
- _highlighted.Clear();
-
- Logger.Write("chip", "raw='" + line + "' fullMatches=" + fullMatches.Count + // NEW
- " avgMatch=" + avgMatch.Success);
-
- if (avgMatch.Success)
- {
- avgChipNumber = ParseF(avgMatch.Groups[1].Value);
- x = ParseF(avgMatch.Groups[2].Value);
- y = ParseF(avgMatch.Groups[3].Value);
- z = ParseF(avgMatch.Groups[4].Value);
- havePosition = true;
- }
-
- if (fullMatches.Count > 0)
- {
- foreach (Match m in fullMatches)
- {
- float chipNumber = ParseF(m.Groups[1].Value);
- if (Math.Abs(chipNumber - avgChipNumber) < 0.001f) continue; // that's the Avg entry itself
- _highlighted.Add(chipNumber); // feeds the existing yellow-fill in DrawOneCircle
- }
-
- if (!havePosition)
- {
- var first = fullMatches[0];
- x = ParseF(first.Groups[2].Value);
- y = ParseF(first.Groups[3].Value);
- z = ParseF(first.Groups[4].Value);
- havePosition = true;
- }
- }
-
- if (!havePosition) return; // status/mode line, not a coordinate line
-
- _currentX = x;
- _currentY = y;
- _currentZ = z;
- _hasPosition = _highlighted.Count > 0; // NEW — marker only shows when a chip is actually on the target
-
- string newView = DetermineViewFromZ(z);
- if (newView != _view)
- SwitchViewFromArduino(newView);
-
- MapArea.QueueDraw();
- }
-
- private static float ParseF(string s)
- {
- return float.Parse(s, CultureInfo.InvariantCulture);
- }
-
- private void OnArduinoConnectionChanged(bool connected)
- {
- if (!connected)
- {
- _hasPosition = false;
- MapArea.QueueDraw();
- }
- }
-
- /// Maps the live Arduino (x,y) into BODY PIXELS using the same
- /// bounding-box scale/offset DrawCircles uses, so the cursor tracks
- /// the same coordinate frame as the numbered circles.
- private bool TryGetCursorBodyPoint(out double bx, out double by)
- {
- bx = by = 0;
- if (!_hasPosition || _jacket == null || _body == null) return false;
-
- var circles = _jacket.Get(JacketSize, _view);
- if (circles.Count == 0) return false;
-
- var t = T;
- int panelW = _body.Width;
- int panelH = _body.Height;
-
- float minX = float.MaxValue, maxX = float.MinValue;
- float minY = float.MaxValue, maxY = float.MinValue;
- foreach (var c in circles)
- {
- if (c.X < minX) minX = c.X;
- if (c.X > maxX) maxX = c.X;
- if (c.Y < minY) minY = c.Y;
- if (c.Y > maxY) maxY = c.Y;
- }
-
- double availW = panelW - t.FitMargin, availH = panelH - t.FitMargin;
- const double circleRadius = 2.1 / 2;
- double dataW = (maxX - minX) + circleRadius * 2;
- double dataH = (maxY - minY) + circleRadius * 2;
- if (dataW <= 0 || dataH <= 0) return false;
-
- double scale = Math.Min(availW / dataW, availH / dataH) * 0.9;
-
- double offsetX = (panelW - (maxX - minX) * scale) / 2 - minX * scale;
- double offsetY = (panelH - (maxY - minY) * scale) / 2 - minY * scale;
-
- // Form2: raw Arduino units run ~50x smaller than CSV/circle units.
- bx = (_currentX * 50 + t.MarginX) * scale + offsetX;
- by = (_currentY * 50 + t.MarginY) * scale + offsetY;
-
- if (JacketSize == "XL" && (_view == "front" || _view == "right" || _view == "left"))
- {
- double centerX = panelW / 2.0;
- double offsetFromCenter = bx - centerX;
- bx = centerX - offsetFromCenter;
- }
-
- return true;
- }
-
- private void DrawCursor(Cairo.Context cr, double x, double y)
- {
- const double size = 14;
-
- cr.NewPath();
- // cr.SetSourceRGBA(1.0, 0.0, 0.0, 0.9); //red not that visible
- cr.SetSourceRGBA(1.0, 0.85, 0.0, 0.95); //yellow
- cr.LineWidth = 2.5;
- cr.MoveTo(x - size, y);
- cr.LineTo(x + size, y);
- cr.MoveTo(x, y - size);
- cr.LineTo(x, y + size);
- cr.Stroke();
-
- cr.NewPath();
- cr.Arc(x, y, 5, 0, 2 * Math.PI);
- cr.Stroke();
- }
-
- private void DrawCoordHud(Cairo.Context cr, int w, int h)
- {
- string text = string.Format(CultureInfo.InvariantCulture,
- "X={0:F2} Y={1:F2} Z={2:F2} view={3}", _currentX, _currentY, _currentZ, _view);
-
- cr.SelectFontFace("Monospace", Cairo.FontSlant.Normal, Cairo.FontWeight.Bold);
- cr.SetFontSize(13);
- var ext = cr.TextExtents(text);
-
- double pad = 6, bx = 8, by = 8;
- double bw = ext.Width + pad * 2, bh = ext.Height + pad * 2;
-
- cr.NewPath();
- cr.Rectangle(bx, by, bw, bh);
- cr.SetSourceRGBA(0, 0, 0, 0.55);
- cr.Fill();
-
- cr.MoveTo(bx + pad - ext.XBearing, by + pad + ext.Height);
- cr.SetSourceRGBA(1, 1, 1, 1);
- cr.ShowText(text);
- }
-
- // ── public API ─────────────────────────────────────────────────────
-
- public void ShowCase(CaseDefinition def, string title)
- {
- _case = def;
- _view = "front";
- if (ViewFrontRadio != null) ViewFrontRadio.Active = true;
-
- MapTitleLabel.Text = title ?? "";
- MapCaseLabel.Text = title ?? "-";
-
- DisposePixbufs();
- _body = LoadBody();
- _map = LoadMap(MapPathForView());
-
- MapArea.QueueDraw();
- UpdateTunerInfo();
- _sound.Start(SoundWindow.AppFile(def.SoundPath));
-
- Logger.Write("case", "open: " + title + " | map=" + MapPathForView());
- }
-
- public void StopSession()
- {
- _sound.Stop();
- Logger.Write("case", "closed");
- }
-
- /// Raw MAP pixbuf — Form2's originalMapForAudio. Never composited.
- public Gdk.Pixbuf AudioReference { get { return _map; } }
-
- /// Body-pixel point -> pixel index inside the MAP that is drawn there.
- /// Stays correct in both fit modes, so audio sampling tracks the display.
- public bool BodyToMapPixel(double bx, double by, out int px, out int py)
- {
- px = py = 0;
- double mx, my, mw, mh;
- if (!MapRect(out mx, out my, out mw, out mh)) return false;
- if (mw <= 0 || mh <= 0) return false;
-
- double u = (bx - mx) / mw, v = (by - my) / mh;
- if (u < 0 || u >= 1 || v < 0 || v >= 1) return false;
-
- Gdk.Pixbuf src = (_mapFit == MapFit.FitBody && _mapRaw != null) ? _mapRaw : _map;
- if (src == null) return false;
-
- px = (int)(u * src.Width);
- py = (int)(v * src.Height);
- return true;
- }
-
- // ── the paint ──────────────────────────────────────────────────────
-
- /// Where the MAP lands, in BODY PIXELS. Single source of truth for both
- /// the draw and BodyToMapPixel.
- private bool MapRect(out double mx, out double my, out double mw, out double mh)
- {
- mx = my = mw = mh = 0;
- if (_body == null) return false;
-
- var t = T;
-
- if (_mapFit == MapFit.FitBody)
- {
- if (_mapRaw == null) return false;
- double ms = Math.Min((double)_body.Width / _mapRaw.Width,
- (double)_body.Height / _mapRaw.Height) * t.MapZoom;
- mw = _mapRaw.Width * ms;
- mh = _mapRaw.Height * ms;
- }
- else
- {
- if (_map == null) return false;
- mw = _map.Width * t.MapZoom;
- mh = _map.Height * t.MapZoom;
- }
-
- mx = (_body.Width - mw) / 2.0 + t.MapX;
- my = (_body.Height - mh) / 2.0 + t.MapY;
- return true;
- }
-
- private void OnMapDrawn(object o, DrawnArgs args)
- {
- var cr = args.Cr;
- int w = MapArea.AllocatedWidth;
- int h = MapArea.AllocatedHeight;
-
- cr.SetSourceRGB(1, 1, 1);
- cr.Rectangle(0, 0, w, h);
- cr.Fill();
-
- if (_body == null) { args.RetVal = true; return; }
-
- // ONE scale factor, derived from the body — this is what keeps the
- // overlay registered. Zoom-to-fit, same as PictureBoxSizeMode.Zoom.
- double s = Math.Min((double)w / _body.Width, (double)h / _body.Height);
- double ox = (w - _body.Width * s) / 2.0;
- double oy = (h - _body.Height * s) / 2.0;
-
- cr.Save();
- cr.Translate(ox, oy);
- cr.Scale(s, s); // everything below is in BODY PIXELS
-
- if (ShowBody)
- {
- Gdk.CairoHelper.SetSourcePixbuf(cr, _body, 0, 0);
- cr.Paint();
- }
-
- double mx, my, mw, mh;
- bool haveMap = MapRect(out mx, out my, out mw, out mh);
-
- if (haveMap && ShowMap)
- {
- Gdk.Pixbuf src = (_mapFit == MapFit.FitBody && _mapRaw != null) ? _mapRaw : _map;
- double sx = mw / src.Width, sy = mh / src.Height;
-
- cr.Save();
- cr.Translate(mx, my);
- cr.Scale(sx, sy);
- Gdk.CairoHelper.SetSourcePixbuf(cr, src, 0, 0);
- cr.PaintWithAlpha(T.MapAlpha);
- cr.Restore();
- }
-
- if (ShowCircles)
- DrawCircles(cr); // body-pixel coords; transform does the rest
-
- double cx, cy;
- if (TryGetCursorBodyPoint(out cx, out cy))
- DrawCursor(cr, cx, cy);
-
- if (ShowGuides)
- {
- if (haveMap)
- {
- cr.NewPath();
- cr.SetSourceRGBA(1, 0, 0, 0.6);
- cr.LineWidth = 1.5;
- cr.Rectangle(mx, my, mw, mh);
- cr.Stroke();
- }
-
- cr.NewPath();
- cr.SetSourceRGBA(0, 0, 1, 0.35);
- cr.LineWidth = 1.0;
- cr.MoveTo(_body.Width / 2.0, 0);
- cr.LineTo(_body.Width / 2.0, _body.Height);
- cr.MoveTo(0, _body.Height / 2.0);
- cr.LineTo(_body.Width, _body.Height / 2.0);
- cr.Stroke();
- }
-
- cr.Restore();
- // NEW — screen-space readout, unaffected by the body scale/translate above
- if (_hasPosition)
- DrawCoordHud(cr, w, h); //show the coordinate on top of the image
-
- args.RetVal = true;
- }
-
- private void DrawCircles(Cairo.Context cr)
- {
- if (_jacket == null || _body == null) return;
-
- var circles = _jacket.Get(JacketSize, _view);
- if (circles.Count == 0) return;
-
- var t = T;
-
- // "panel" is the body pixbuf — Form2's gridPictureBox was 546x546
- int panelW = _body.Width;
- int panelH = _body.Height;
-
- float minX = float.MaxValue, maxX = float.MinValue;
- float minY = float.MaxValue, maxY = float.MinValue;
- foreach (var c in circles)
- {
- if (c.X < minX) minX = c.X;
- if (c.X > maxX) maxX = c.X;
- if (c.Y < minY) minY = c.Y;
- if (c.Y > maxY) maxY = c.Y;
- }
-
- double availW = panelW - t.FitMargin, availH = panelH - t.FitMargin;
- const double circleRadius = 2.1 / 2;
- double dataW = (maxX - minX) + circleRadius * 2;
- double dataH = (maxY - minY) + circleRadius * 2;
- if (dataW <= 0 || dataH <= 0) return;
-
- double scale = Math.Min(availW / dataW, availH / dataH) * 0.9;
-
- double offsetX = (panelW - (maxX - minX) * scale) / 2 - minX * scale;
- double offsetY = (panelH - (maxY - minY) * scale) / 2 - minY * scale;
-
- // cluster centroid, in already-offset space
- double avgX = 0, avgY = 0;
- foreach (var c in circles)
- {
- avgX += c.X * scale + offsetX - t.OffsetX;
- avgY += c.Y * scale + offsetY - t.OffsetY;
- }
- avgX /= circles.Count;
- avgY /= circles.Count;
-
- cr.SelectFontFace("Sans", Cairo.FontSlant.Normal, Cairo.FontWeight.Bold);
-
- foreach (var c in circles)
- DrawOneCircle(cr, c, t, scale, offsetX, offsetY, avgX, avgY);
- }
-
- private void DrawOneCircle(Cairo.Context cr, Circle c, Tune t,
- double scale, double ox, double oy,
- double avgX, double avgY)
- {
- double x = c.X * scale + ox - t.OffsetX;
- double y = c.Y * scale + oy - t.OffsetY;
-
- if (Math.Abs(t.Cluster - 1.0) > 0.001)
- {
- x = avgX + (x - avgX) * t.Cluster;
- y = avgY + (y - avgY) * t.Cluster;
- }
-
- double d = CircleDiameter * scale * t.SizeScale;
- double r = d / 2;
-
- bool hot = false;
- foreach (float n in _highlighted)
- if (Math.Abs(c.Number - n) < 0.001f) { hot = true; break; }
-
- cr.NewPath();
- cr.Arc(x, y, r, 0, 2 * Math.PI);
-
- if (hot)
- {
- cr.SetSourceRGBA(1.0, 0.922, 0.231, 1.0); // Color.FromArgb(255,235,59)
- cr.FillPreserve();
- }
-
- cr.SetSourceRGBA(0.5, 0.5, 0.5, 1.0); // BorderColor = Gray
- cr.LineWidth = Math.Max(1.0, scale);
- cr.Stroke();
-
- string text = (c.Number % 1 == 0)
- ? ((int)c.Number).ToString()
- : c.Number.ToString("F1", CultureInfo.InvariantCulture);
-
- cr.SetFontSize(Math.Max(6.0, d / (c.Inner ? 3 : 4)));
- var ext = cr.TextExtents(text);
- cr.MoveTo(x - ext.Width / 2 - ext.XBearing, y + ext.Height / 2);
- cr.SetSourceRGBA(0.2, 0.2, 0.2, 1.0); // TextColor
- cr.ShowText(text);
- cr.NewPath();
- }
-
- // ══════════════════════════════════════════════════════════════════
- // TUNER
- // ══════════════════════════════════════════════════════════════════
-
- public void ToggleTuner()
- {
- if (_tuner != null) { _tuner.Destroy(); _tuner = null; return; }
- BuildTuner();
- }
-
- private void BuildTuner()
- {
- _tuner = new Window("MapWindow tuner — Ctrl+T to close");
- _tuner.SetDefaultSize(430, 760);
- _tuner.DeleteEvent += (o, a) => { _tuner = null; };
-
- var scroll = new ScrolledWindow();
- var col = new Box(Orientation.Vertical, 4);
- col.MarginLeft = col.MarginRight = col.MarginTop = col.MarginBottom = 8;
-
- _tunerKey = new Label(); _tunerKey.Xalign = 0;
- col.PackStart(_tunerKey, false, false, 0);
-
- _tunerInfo = new Label(); _tunerInfo.Xalign = 0;
- _tunerInfo.Selectable = true;
- col.PackStart(_tunerInfo, false, false, 0);
-
- col.PackStart(Header("Sound map"), false, false, 0);
-
- _tFitCombo = new ComboBoxText();
- _tFitCombo.AppendText("Native546 (Form2)");
- _tFitCombo.AppendText("FitBody (calibrator)");
- _tFitCombo.Active = (_mapFit == MapFit.FitBody) ? 1 : 0;
- _tFitCombo.Changed += (s, e) =>
- {
- if (_building) return;
- _mapFit = _tFitCombo.Active == 1 ? MapFit.FitBody : MapFit.Native546;
- UpdateTunerInfo();
- MapArea.QueueDraw();
- };
- col.PackStart(Row("Map fit mode", _tFitCombo), false, false, 0);
-
- AddSpin(col, "MapZoom", "Zoom", 0.10, 3.00, 0.01, 2);
- AddSpin(col, "MapX", "Offset X (px)", -600, 600, 1, 0);
- AddSpin(col, "MapY", "Offset Y (px)", -600, 600, 1, 0);
- AddSpin(col, "MapAlpha", "Opacity", 0.00, 1.00, 0.05, 2);
-
- col.PackStart(Header("Circle cluster"), false, false, 0);
- AddSpin(col, "OffsetX", "Offset X (px)", -600, 600, 1, 0);
- AddSpin(col, "OffsetY", "Offset Y (px)", -600, 600, 1, 0);
- AddSpin(col, "SizeScale", "Circle size", 0.05, 4.00, 0.05, 2);
- AddSpin(col, "Cluster", "Cluster factor", 0.05, 2.00, 0.05, 2);
- AddSpin(col, "FitMargin", "Fit margin (px)", 0, 400, 5, 0);
-
- col.PackStart(Header("Cursor (Arduino)"), false, false, 0);
- AddSpin(col, "MarginX", "Margin X (raw units)", -2000, 2000, 1, 0);
- AddSpin(col, "MarginY", "Margin Y (raw units)", -2000, 2000, 1, 0);
-
- col.PackStart(Header("Display"), false, false, 0);
- _tShowBody = Chk(col, "Show body", true);
- _tShowMap = Chk(col, "Show map", true);
- _tShowCircles = Chk(col, "Show circles", true);
- _tGuides = Chk(col, "Show guides", false);
-
- col.PackStart(Header("Images"), false, false, 0);
- var imgRow = new Box(Orientation.Horizontal, 4);
- imgRow.PackStart(Btn("Reload images", () =>
- {
- ReloadBody();
- if (_map != null) { _map.Dispose(); _map = null; }
- if (_mapRaw != null) { _mapRaw.Dispose(); _mapRaw = null; }
- _map = LoadMap(MapPathForView());
- UpdateTunerInfo();
- MapArea.QueueDraw();
- }), true, true, 0);
- imgRow.PackStart(Btn("Cycle view", () =>
- {
- string[] v = { "front", "back", "right", "left" };
- int i = Array.IndexOf(v, _view);
- string next = v[(i + 1) % v.Length];
- RadioButton rb = next == "back" ? ViewBackRadio
- : next == "right" ? ViewRightRadio
- : next == "left" ? ViewLeftRadio : ViewFrontRadio;
- if (rb != null) rb.Active = true; else SetView(next);
- }), true, true, 0);
- col.PackStart(imgRow, false, false, 0);
-
- col.PackStart(Header("Constants"), false, false, 0);
-
- var r1 = new Box(Orientation.Horizontal, 4);
- r1.PackStart(Btn("Apply pasted C#", ApplyPastedCSharp), true, true, 0);
- r1.PackStart(Btn("Dump C# (calib)", () => Emit(DumpCalibStyle())), true, true, 0);
- col.PackStart(r1, false, false, 0);
-
- var r2 = new Box(Orientation.Horizontal, 4);
- r2.PackStart(Btn("Dump seed", () => Emit(DumpSeed())), true, true, 0);
- r2.PackStart(Btn("Reset view", () =>
- {
- _tunes.Remove(TuneKey);
- var fresh = new Dictionary(_tunes);
- SeedDefaults();
- foreach (var kv in fresh) if (kv.Key != TuneKey) _tunes[kv.Key] = kv.Value;
- SyncSpins(); MapArea.QueueDraw();
- }), true, true, 0);
- col.PackStart(r2, false, false, 0);
-
- var r3 = new Box(Orientation.Horizontal, 4);
- r3.PackStart(Btn("Load calib.txt", () => { LoadCalibFile(true); SyncSpins(); MapArea.QueueDraw(); }), true, true, 0);
- r3.PackStart(Btn("Save calib.txt", SaveCalibFile), true, true, 0);
- col.PackStart(r3, false, false, 0);
-
- _tunerOut = new TextView();
- _tunerOut.OverrideFont(Pango.FontDescription.FromString("Monospace 9"));
- _tunerOut.WrapMode = WrapMode.None;
- var outScroll = new ScrolledWindow();
- outScroll.HeightRequest = 220;
- outScroll.Add(_tunerOut);
- col.PackStart(outScroll, true, true, 0);
-
- col.PackStart(new Label(
- "Paste a CalibrationWindow dump above, then Apply pasted C#.") { Xalign = 0 },
- false, false, 0);
-
- scroll.Add(col);
- _tuner.Add(scroll);
- _tuner.ShowAll();
-
- SyncSpins();
- UpdateTunerInfo();
- }
-
- // ── tuner widget helpers ──
-
- private Label Header(string text)
- {
- var l = new Label();
- l.Markup = "" + text + "";
- l.Xalign = 0;
- l.MarginTop = 8;
- return l;
- }
-
- private Box Row(string label, Widget w)
- {
- var box = new Box(Orientation.Horizontal, 6);
- var l = new Label(label);
- l.Xalign = 0;
- l.WidthRequest = 130;
- box.PackStart(l, false, false, 0);
- box.PackStart(w, true, true, 0);
- return box;
- }
-
- private Button Btn(string label, System.Action onClick)
- {
- var b = new Button(label);
- b.Clicked += (s, e) => onClick();
- return b;
- }
-
- private CheckButton Chk(Box parent, string label, bool active)
- {
- var c = new CheckButton(label);
- c.Active = active;
- c.Toggled += (s, e) => MapArea.QueueDraw();
- parent.PackStart(c, false, false, 0);
- return c;
- }
-
- private void AddSpin(Box parent, string field, string label,
- double min, double max, double step, uint digits)
- {
- var sb = new SpinButton(min, max, step);
- sb.Digits = digits;
- sb.Numeric = true;
- sb.ValueChanged += (s, e) =>
- {
- if (_building) return;
- SetField(field, sb.Value);
- UpdateTunerInfo();
- MapArea.QueueDraw();
- };
- _spins[field] = sb;
- parent.PackStart(Row(label, sb), false, false, 0);
- }
-
- private void SetField(string field, double v)
- {
- var t = T;
- switch (field)
- {
- case "OffsetX": t.OffsetX = v; break;
- case "OffsetY": t.OffsetY = v; break;
- case "SizeScale": t.SizeScale = v; break;
- case "Cluster": t.Cluster = v; break;
- case "MapZoom": t.MapZoom = v; break;
- case "MapX": t.MapX = v; break;
- case "MapY": t.MapY = v; break;
- case "MapAlpha": t.MapAlpha = v; break;
- case "FitMargin": t.FitMargin = v; break;
- case "MarginX": t.MarginX = v; break;
- case "MarginY": t.MarginY = v; break;
- }
- }
-
- private double GetField(string field)
- {
- var t = T;
- switch (field)
- {
- case "OffsetX": return t.OffsetX;
- case "OffsetY": return t.OffsetY;
- case "SizeScale": return t.SizeScale;
- case "Cluster": return t.Cluster;
- case "MapZoom": return t.MapZoom;
- case "MapX": return t.MapX;
- case "MapY": return t.MapY;
- case "MapAlpha": return t.MapAlpha;
- case "FitMargin": return t.FitMargin;
- case "MarginX": return t.MarginX;
- case "MarginY": return t.MarginY;
- }
- return 0;
- }
-
- private void SyncSpins()
- {
- if (_tuner == null) return;
- _building = true;
- foreach (var kv in _spins) kv.Value.Value = GetField(kv.Key);
- if (_tunerKey != null) _tunerKey.Markup = "Editing: " + TuneKey + "";
- _building = false;
- }
-
- private void UpdateTunerInfo()
- {
- if (_tunerInfo == null) return;
-
- var sb = new StringBuilder();
- sb.Append("body: ").Append(_bodyPath);
- if (_body != null) sb.Append(" [").Append(_body.Width).Append("x").Append(_body.Height).Append("]");
- else sb.Append(" [MISSING]");
- sb.AppendLine();
-
- sb.Append("map: ").Append(_mapPath);
- if (_mapRaw != null) sb.Append(" raw[").Append(_mapRaw.Width).Append("x").Append(_mapRaw.Height).Append("]");
- if (_map != null) sb.Append(" used[").Append(_map.Width).Append("x").Append(_map.Height).Append("]");
- else sb.Append(" [MISSING]");
- sb.AppendLine();
-
- double mx, my, mw, mh;
- if (MapRect(out mx, out my, out mw, out mh))
- sb.AppendFormat(CultureInfo.InvariantCulture,
- "rect: x={0:F1} y={1:F1} w={2:F1} h={3:F1}\n", mx, my, mw, mh);
-
- if (_mapFit == MapFit.FitBody)
- sb.AppendLine("NOTE: FitBody changes display geometry; AudioReference\n" +
- " is still the 546 pixbuf — sample via BodyToMapPixel.");
-
- _tunerInfo.Text = sb.ToString();
- }
-
- private void Emit(string msg)
- {
- if (_tunerOut == null) { Logger.Write("tune", msg); return; }
- _tunerOut.Buffer.Text = msg + "\n\n" + _tunerOut.Buffer.Text;
- }
-
- // ── C# constant round-trip ──
-
- private static readonly Regex ConstRx = new Regex(
- @"(?XL_)?(?FRONT|BACK|RIGHT|LEFT)_" +
- @"(?OFFSET_X|OFFSET_Y|SIZE|CLUSTER|FIT_MARGIN|MAP_ZOOM|MAP_X|MAP_Y|MAP_ALPHA)" +
- @"\s*=\s*(?-?[0-9]*\.?[0-9]+)",
- RegexOptions.IgnoreCase | RegexOptions.Compiled);
-
- /// Parses a CalibrationWindow "Dump" block pasted into the text view.
- private void ApplyPastedCSharp()
- {
- if (_tunerOut == null) return;
- string text = _tunerOut.Buffer.Text;
-
- var inv = CultureInfo.InvariantCulture;
- int applied = 0;
- var touched = new List();
-
- foreach (Match m in ConstRx.Matches(text))
- {
- string size = m.Groups["xl"].Success ? "XL" : "L";
- string view = m.Groups["view"].Value.ToLower();
- string key = size + ":" + view;
-
- double v;
- if (!double.TryParse(m.Groups["val"].Value, NumberStyles.Float, inv, out v)) continue;
-
- if (!_tunes.ContainsKey(key)) _tunes[key] = new Tune();
- var t = _tunes[key];
-
- switch (m.Groups["field"].Value.ToUpper())
- {
- case "OFFSET_X": t.OffsetX = v; break;
- case "OFFSET_Y": t.OffsetY = v; break;
- case "SIZE": t.SizeScale = v; break;
- case "CLUSTER": t.Cluster = v; break;
- case "FIT_MARGIN": t.FitMargin = v; break;
- case "MAP_ZOOM": t.MapZoom = v; break;
- case "MAP_X": t.MapX = v; break;
- case "MAP_Y": t.MapY = v; break;
- case "MAP_ALPHA": t.MapAlpha = v; break;
- default: continue;
- }
- applied++;
- if (!touched.Contains(key)) touched.Add(key);
- }
-
- SyncSpins();
- UpdateTunerInfo();
- MapArea.QueueDraw();
-
- if (applied == 0)
- Emit("no constants recognised — expected lines like\n" +
- " private const float FRONT_OFFSET_X = 25.0f;");
- else
- Emit("applied " + applied + " values to: " + string.Join(", ", touched.ToArray()) +
- "\nIf MAP_ZOOM came from the calibrator, switch fit mode to FitBody.");
- }
-
- /// Same format CalibrationWindow emits — round-trips through Apply.
- private string DumpCalibStyle()
- {
- var inv = CultureInfo.InvariantCulture;
- var sb = new StringBuilder();
- sb.AppendLine("// MapWindow tuner " + DateTime.Now.ToString("yyyy-MM-dd HH:mm") +
- " fit=" + _mapFit);
-
- foreach (string size in new[] { "L", "XL" })
- {
- sb.AppendLine("// ---- " + size + " ----");
- foreach (string view in new[] { "front", "back", "right", "left" })
- {
- string k = size + ":" + view;
- if (!_tunes.ContainsKey(k)) continue;
- var t = _tunes[k];
- string p = (size == "XL" ? "XL_" : "") + view.ToUpper();
-
- sb.AppendLine("private const float " + p + "_OFFSET_X = " + t.OffsetX.ToString("F1", inv) + "f;");
- sb.AppendLine("private const float " + p + "_OFFSET_Y = " + t.OffsetY.ToString("F1", inv) + "f;");
- sb.AppendLine("private const float " + p + "_SIZE = " + t.SizeScale.ToString("F2", inv) + "f;");
- sb.AppendLine("private const float " + p + "_CLUSTER = " + t.Cluster.ToString("F2", inv) + "f;");
- sb.AppendLine("private const float " + p + "_FIT_MARGIN = " + t.FitMargin.ToString("F0", inv) + "f;");
- sb.AppendLine("private const double " + p + "_MAP_ZOOM = " + t.MapZoom.ToString("F3", inv) + ";");
- sb.AppendLine("private const double " + p + "_MAP_X = " + t.MapX.ToString("F1", inv) + ";");
- sb.AppendLine("private const double " + p + "_MAP_Y = " + t.MapY.ToString("F1", inv) + ";");
- sb.AppendLine("private const double " + p + "_MAP_ALPHA = " + t.MapAlpha.ToString("F2", inv) + ";");
- sb.AppendLine();
- }
- }
- return sb.ToString();
- }
-
- /// Paste this straight over SeedDefaults() to bake values into the build.
- private string DumpSeed()
- {
- var inv = CultureInfo.InvariantCulture;
- var sb = new StringBuilder();
- sb.AppendLine("private void SeedDefaults()");
- sb.AppendLine("{");
- foreach (string size in new[] { "L", "XL" })
- foreach (string view in new[] { "front", "back", "right", "left" })
- {
- string k = size + ":" + view;
- if (!_tunes.ContainsKey(k)) continue;
- var t = _tunes[k];
- sb.AppendFormat(inv,
- " Seed(\"{0}\", {1:F1}, {2:F1}, {3:F2}, {4:F2}, {5:F0}, {6:F3}, {7:F1}, {8:F1}, {9:F2});\n",
- k, t.OffsetX, t.OffsetY, t.SizeScale, t.Cluster,
- t.FitMargin, t.MapZoom, t.MapX, t.MapY, t.MapAlpha);
- }
- sb.AppendLine("}");
- sb.AppendLine("// _mapFit = MapFit." + _mapFit + ";");
- return sb.ToString();
- }
-
- // ── calib.txt (same tab format CalibrationWindow writes) ──
-
- private static string CalibPath { get { return SoundWindow.AppFile("calib.txt"); } }
-
- private void LoadCalibFile(bool verbose)
- {
- string p = CalibPath;
- if (!File.Exists(p))
- {
- if (verbose) Emit("calib.txt not found at " + p);
- return;
- }
-
- var inv = CultureInfo.InvariantCulture;
- int n = 0;
- foreach (string line in File.ReadAllLines(p))
- {
- if (line.StartsWith("#") || line.Trim().Length == 0) continue;
- string[] f = line.Split('\t');
- if (f.Length < 10) continue;
-
- try
- {
- var t = new Tune();
- t.OffsetX = double.Parse(f[1], inv);
- t.OffsetY = double.Parse(f[2], inv);
- t.SizeScale = double.Parse(f[3], inv);
- t.Cluster = double.Parse(f[4], inv);
- t.MapZoom = double.Parse(f[5], inv);
- t.MapX = double.Parse(f[6], inv);
- t.MapY = double.Parse(f[7], inv);
- t.MapAlpha = double.Parse(f[8], inv);
- t.FitMargin = double.Parse(f[9], inv);
- _tunes[f[0]] = t;
- n++;
- }
- catch { }
- }
-
- Logger.Write("tune", "loaded " + n + " entries from " + p);
- if (verbose) Emit("loaded " + n + " entries from " + p +
- "\ncalib.txt zoom values assume FitBody mode.");
- }
-
- private void SaveCalibFile()
- {
- var inv = CultureInfo.InvariantCulture;
- var sb = new StringBuilder();
- sb.AppendLine("# size:view offsetX offsetY sizeScale cluster mapZoom mapX mapY mapAlpha fitMargin");
- foreach (var kv in _tunes)
- {
- var t = kv.Value;
- sb.AppendLine(string.Join("\t", new[]
- {
- kv.Key,
- t.OffsetX.ToString(inv), t.OffsetY.ToString(inv),
- t.SizeScale.ToString(inv), t.Cluster.ToString(inv),
- t.MapZoom.ToString(inv), t.MapX.ToString(inv),
- t.MapY.ToString(inv), t.MapAlpha.ToString(inv),
- t.FitMargin.ToString(inv)
- }));
- }
-
- try
- {
- File.WriteAllText(CalibPath, sb.ToString());
- Emit("saved " + CalibPath);
- }
- catch (Exception ex) { Emit("save failed: " + ex.Message); }
- }
-
- // ── loading ────────────────────────────────────────────────────────
-
- private void SetView(string view)
- {
- if (_view == view) return;
- _view = view;
-
- ReloadBody();
-
- if (_map != null) { _map.Dispose(); _map = null; }
- if (_mapRaw != null) { _mapRaw.Dispose(); _mapRaw = null; }
- _map = LoadMap(MapPathForView());
-
- SyncSpins();
- UpdateTunerInfo();
- MapArea.QueueDraw();
- Logger.Write("view", "-> " + view);
- }
-
- private void ReloadBody()
- {
- if (_body != null) { _body.Dispose(); _body = null; }
- _body = LoadBody();
- UpdateTunerInfo();
- MapArea.QueueDraw();
- }
-
- private Gdk.Pixbuf LoadBody()
- {
- string stem = BodyStem();
- string full = SoundWindow.AppFile(Path.Combine("map", stem + ".png"));
- _bodyPath = "map/" + stem + ".png";
-
- if (!File.Exists(full))
- {
- Logger.Write("body", "not found: map/" + stem + ".png");
- ListFolderOnce("map");
- return null;
- }
-
- return Load(full);
- }
-
- private static void ListFolderOnce(string rel)
- {
- if (_listed) return;
- _listed = true;
-
- string dir = SoundWindow.AppFile(rel);
- if (!Directory.Exists(dir)) { Logger.Write("body", "folder missing: " + dir); return; }
-
- foreach (string f in Directory.GetFiles(dir))
- Logger.Write("body", "found: " + Path.GetFileName(f));
- }
-
- private string BodyStem()
- {
- bool skeleton = (BodySkeletonRadio != null && BodySkeletonRadio.Active);
-
- if (skeleton)
- {
- switch (_view)
- {
- case "back": return "jacketBackBody";
- case "right": return "jacketRightBody";
- case "left": return "jacketLeftBody";
- default: return "jacketFrontBody";
- }
- }
-
- switch (_view)
- {
- case "back": return "jacketBack";
- case "right": return "jacketRight";
- case "left": return "jacketLeft";
- default: return "jacketFront";
- }
- }
-
- private string MapPathForView()
- {
- if (_case == null) return null;
-
- string rel;
- switch (_view)
- {
- case "back": rel = _case.MapBack; break;
- case "right": rel = _case.MapRight; break;
- case "left": rel = _case.MapLeft; break;
- default: rel = _case.MapFront; break;
- }
-
- // Form2 falls back to a mirrored front map when there's no dedicated back.
- if (string.IsNullOrEmpty(rel)) rel = _case.MapFront;
-
- return string.IsNullOrEmpty(rel) ? null : SoundWindow.AppFile(rel);
- }
-
- private static Gdk.Pixbuf Load(string path)
- {
- if (string.IsNullOrEmpty(path) || !File.Exists(path))
- {
- Logger.Write("map", "image not found: " + path);
- return null;
- }
-
- try { return new Gdk.Pixbuf(path); }
- catch (Exception ex)
- {
- Logger.Write("map", "load failed " + path + ": " + ex.Message);
- return null;
- }
- }
-
- private Gdk.Pixbuf LoadMap(string path)
- {
- _mapPath = path ?? "(none)";
-
- var raw = Load(path);
- if (raw == null) return null;
-
- // Key out black FIRST, then scale. Scaling first leaves near-black
- // edge pixels (1,0,2) that AddAlpha won't match — that's the dark halo.
- var keyed = raw.AddAlpha(true, 0, 0, 0);
- raw.Dispose();
-
- // Keep the keyed original for FitBody mode — the calibrator never
- // resized to 546, so its zoom numbers refer to THIS pixbuf.
- _mapRaw = keyed.Copy();
-
- var sized = keyed.ScaleSimple(MapSize, MapSize, Gdk.InterpType.Bilinear);
- keyed.Dispose();
- return sized;
- }
-
- // ── plumbing ───────────────────────────────────────────────────────
-
- private void OnBackClicked(object sender, EventArgs e)
- {
- StopSession();
- var h = BackRequested;
- if (h != null) h(this, EventArgs.Empty);
- }
-
- private void DisposePixbufs()
- {
- if (_body != null) { _body.Dispose(); _body = null; }
- if (_map != null) { _map.Dispose(); _map = null; }
- if (_mapRaw != null) { _mapRaw.Dispose(); _mapRaw = null; }
- }
-
- public void Dispose()
- {
- _arduino.LineReceived -= OnArduinoLine;
- _arduino.ConnectionChanged -= OnArduinoConnectionChanged;
- _sound.Dispose();
- DisposePixbufs();
- if (_tuner != null) { _tuner.Destroy(); _tuner = null; }
- }
-
- private string JacketSize
- {
- get { return (JacketSizeCombo != null ? JacketSizeCombo.ActiveText : null) ?? "L"; }
- }
-
- private string DetermineViewFromZ(float z)
- {
- float frontDiff = Math.Abs(z - FRONT_Z);
- float backDiff = Math.Abs(z - BACK_Z);
- float rightDiff = Math.Abs(z - RIGHT_Z);
- float leftDiff = Math.Abs(z - LEFT_Z);
-
- float minDiff = Math.Min(Math.Min(frontDiff, backDiff), Math.Min(rightDiff, leftDiff));
- if (minDiff > Z_TOLERANCE) return _view; // no clean match — stay put
-
- if (minDiff == frontDiff) return "front";
- if (minDiff == backDiff) return "back";
- if (minDiff == rightDiff) return "right";
- return "left";
- }
-
- /// Drives the same radio buttons a manual click would, so the UI and
- /// _view stay in sync (SetView no-ops if it's already the active view).
- private void SwitchViewFromArduino(string view)
- {
- RadioButton rb = view == "back" ? ViewBackRadio
- : view == "right" ? ViewRightRadio
- : view == "left" ? ViewLeftRadio
- : ViewFrontRadio;
- if (rb != null) rb.Active = true;
- else SetView(view);
- }
-}
\ No newline at end of file
diff --git a/program/SettingWindow.cs b/program/SettingWindow.cs
deleted file mode 100644
index f091c74..0000000
--- a/program/SettingWindow.cs
+++ /dev/null
@@ -1,217 +0,0 @@
-using System;
-using Gtk;
-using System.Collections.Generic;
-using UI = Gtk.Builder.ObjectAttribute;
-
-public class SettingsWindow
-{
- [UI] private Label ComPortLabel = null;
- [UI] private ComboBoxText ComPortComboBox = null;
- [UI] private Button ConnectButton = null;
- [UI] private Button AutoConnectButton = null;
- [UI] private Button RefreshButton = null;
- [UI] private Button SettingBackButton = null;
- [UI] private Label StatusLabel = null;
- [UI] private Label LogLabel = null;
- [UI] private TextView LogtextBox = null;
-
- // private readonly Dictionary _buttonsById;
- // private readonly Dictionary _labelsById;
- // internal Language _Language;
- private readonly Language _lang;
- private bool _connected;
-
- private const int MaxLogLines = 500;
-
- private readonly ArduinoConnection _arduino;
- private string[] _ports = new string[0];
-
- /// Raised after the window hides itself, so the caller can re-show the main window.
- public event EventHandler BackRequested;
-
-
- public SettingsWindow(Builder builder, ArduinoConnection arduino, Language language)
- {
- builder.Autoconnect(this);
- _arduino = arduino;
- _lang = language;
-
- // Title = "Settings";
- // ComPortLabel.Text = "COM Port";
- // LogLabel.Text = "Log";
- LogtextBox.Editable = false;
- LogtextBox.WrapMode = WrapMode.WordChar;
-
- RefreshButton.Clicked += (s, e) => RefreshPorts();
- ConnectButton.Clicked += OnConnectClicked;
- AutoConnectButton.Clicked += OnAutoConnectClicked;
- SettingBackButton.Clicked += OnBackClicked;
-
- // DeleteEvent += (o, a) => { a.RetVal = true; OnBackClicked(o, EventArgs.Empty); };
-
- _arduino.Log += AppendLog;
- _arduino.LineReceived += OnLineReceived;
- _arduino.ConnectionChanged += OnConnectionChanged;
-
- _lang.Register("ComPortLabel", ComPortLabel);
- _lang.Register("LogLabel", LogLabel);
- _lang.Register("AutoConnectButton", AutoConnectButton);
- _lang.Register("RefreshButton", RefreshButton);
- _lang.Register("SettingBackButton", SettingBackButton);
- _lang.RegisterDynamic("ConnectButton", ConnectButton); // set in OnConnectionChanged
- _lang.RegisterDynamic("StatusLabel", StatusLabel);
- _lang.Changed += (s, e) => OnConnectionChanged(_connected);
-
- // _buttonsById = new Dictionary
- // {
- // { "ConnectButton", ConnectButton },
- // { "AutoConnectButton", AutoConnectButton },
- // { "RefreshButton", RefreshButton },
- // { "BackButton", BackButton },
- // };
-
- // // CSS ID selectors (#Option1Button etc.) need the widget Name set explicitly
- // foreach (var kvp in _buttonsById)
- // kvp.Value.Name = kvp.Key;
-
- // _Language = new Language(_buttonsById);
- // // _Language.ApplyLanguages(Language.LanguageSwitchValue);
-
- RefreshPorts();
- OnConnectionChanged(_arduino.IsOpen);
- }
-
- // ---------- UI actions ----------
-
- private void RefreshPorts()
- {
- ComPortComboBox.RemoveAll();
- _ports = ArduinoConnection.ListPorts();
-
- foreach (string p in _ports)
- ComPortComboBox.AppendText(p);
-
- if (_ports.Length > 0)
- {
- ComPortComboBox.Active = 0;
- AppendLog(_lang.Format("Log.PortsFound", _ports.Length));
- }
- else
- {
- AppendLog(_lang["Log.NoPorts"]);
- }
- }
-
- private void OnConnectClicked(object sender, EventArgs e)
- {
- if (_arduino.IsOpen)
- {
- _arduino.Disconnect();
- return;
- }
-
- string port = ComPortComboBox.ActiveText;
- if (string.IsNullOrEmpty(port))
- {
- AppendLog(_lang["Log.SelectPort"]);
- return;
- }
- _arduino.Connect(port);
- }
-
- private async void OnAutoConnectClicked(object sender, EventArgs e)
- {
- AutoConnectButton.Sensitive = false;
- ConnectButton.Sensitive = false;
- AppendLog(_lang["Log.Scanning"]);
-
- string found = await _arduino.DetectPortAsync();
-
- if (found != null)
- {
- RefreshPorts();
- SelectPort(found);
- _arduino.Connect(found);
- }
- else
- {
- AppendLog(_lang["Log.DeviceNotFound"]);
- }
-
- AutoConnectButton.Sensitive = true;
- ConnectButton.Sensitive = true;
- }
-
- private void OnBackClicked(object sender, EventArgs e)
- {
- // Hide();
- BackRequested?.Invoke(this, EventArgs.Empty);
- }
-
- private void SelectPort(string portName)
- {
- for (int i = 0; i < _ports.Length; i++)
- {
- if (_ports[i] == portName)
- {
- ComPortComboBox.Active = i;
- return;
- }
- }
- }
-
- // ---------- device events ----------
-
- private void OnConnectionChanged(bool connected)
- {
- _connected = connected;
-
- ConnectButton.Label = connected
- ? _lang["ConnectButton.Disconnect"]
- : _lang["ConnectButton.Connect"];
-
- StatusLabel.Text = connected
- ? _lang.Format("StatusLabel.Connected", _arduino.PortName)
- : _lang["StatusLabel.Disconnected"];
-
- ComPortComboBox.Sensitive = !connected;
- RefreshButton.Sensitive = !connected;
-
- StyleContext ctx = StatusLabel.StyleContext;
- ctx.RemoveClass(connected ? "status-off" : "status-on");
- ctx.AddClass(connected ? "status-on" : "status-off");
- }
-
- private void OnLineReceived(string line)
- {
- // Same filtering Form2.cs applied before parsing.
- if (line.StartsWith("Send Status:")) return;
- if (line.StartsWith("\u2713") || line.StartsWith("\u2717")) return;
-
- if (line.Contains("ESPNOW_CONNECTED")) { AppendLog(_lang["Log.EspNowConnected"]); return; }
- if (line.Contains("ESPNOW_DISCONNECTED")) { AppendLog(_lang["Log.EspNowDisconnected"]); return; }
-
- AppendLog(line);
- }
-
- // ---------- log ----------
-
- public void AppendLog(string message)
- {
- TextBuffer buf = LogtextBox.Buffer;
-
- TextIter end = buf.EndIter;
- buf.Insert(ref end, $"[{DateTime.Now:HH:mm:ss.fff}] {message}\n");
-
- if (buf.LineCount > MaxLogLines)
- {
- TextIter start = buf.StartIter;
- TextIter cut = buf.GetIterAtLine(buf.LineCount - MaxLogLines);
- buf.Delete(ref start, ref cut);
- }
-
- TextMark mark = buf.CreateMark(null, buf.EndIter, false);
- LogtextBox.ScrollToMark(mark, 0, false, 0, 0);
- buf.DeleteMark(mark);
- }
-}
\ No newline at end of file
diff --git a/program/SoundLooper.cs b/program/SoundLooper.cs
deleted file mode 100644
index 311d4b6..0000000
--- a/program/SoundLooper.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-using System.Media;
-using System.Threading;
-
-public class SoundLooper : IDisposable
-{
- // ── unix backend ──
- private Process _proc;
- private Thread _thread;
- private volatile bool _running;
- private string _path;
-
- // ── windows backend ──
- private SoundPlayer _player;
-
- public event Action Log;
-
- public bool IsPlaying { get { return _running; } }
-
- private static bool IsWindows
- {
- get
- {
- var p = Environment.OSVersion.Platform;
- return p != PlatformID.Unix && p != PlatformID.MacOSX;
- }
- }
-
- public void Start(string wavPath)
- {
- Stop();
-
- if (string.IsNullOrEmpty(wavPath) || !File.Exists(wavPath))
- {
- Emit("Sound file not found: " + wavPath);
- return;
- }
-
- _path = wavPath;
-
- if (IsWindows) StartWindows();
- else StartUnix();
- }
-
- public void Stop()
- {
- if (!_running) return;
- _running = false;
-
- if (_player != null)
- {
- try { _player.Stop(); } catch { }
- try { _player.Dispose(); } catch { }
- _player = null;
- }
-
- 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");
- }
-
- // ── windows: SoundPlayer loops natively, no thread needed ──────────
-
- private void StartWindows()
- {
- try
- {
- _player = new SoundPlayer(_path);
- _player.Load(); // throws here if the WAV isn't plain PCM
- _player.PlayLooping(); // gapless, runs until Stop()
- _running = true;
- Emit("Looping started (SoundPlayer): " + _path);
- }
- catch (Exception ex)
- {
- Emit("SoundPlayer failed for " + _path + ": " + ex.Message);
- _player = null;
- _running = false;
- }
- }
-
- // ── unix: respawn a CLI player each pass ───────────────────────────
-
- private void StartUnix()
- {
- _running = true;
- _thread = new Thread(LoopWorker);
- _thread.IsBackground = true;
- _thread.Start();
- Emit("Looping started (" + PlayerCommand() + "): " + _path);
- }
-
- 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_cmd;
- private static string PlayerCommand()
- {
- if (_player_cmd != null) return _player_cmd;
- _player_cmd = Exists("paplay") ? "paplay" : "aplay";
- return _player_cmd;
- }
-
- 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
deleted file mode 100644
index a8162be..0000000
--- a/program/SoundWindow.cs
+++ /dev/null
@@ -1,198 +0,0 @@
-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 _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;
-
- 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 labels, Action 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);
- }
- }
-}
\ No newline at end of file
diff --git a/program/WavePlayer.cs b/program/WavePlayer.cs
deleted file mode 100644
index 00cd07a..0000000
--- a/program/WavePlayer.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.IO;
-
-public static class WavePlayer
-{
- private static Process _current;
-
- private static bool IsUnix
- {
- get
- {
- int p = (int)Environment.OSVersion.Platform;
- return p == 4 || p == 6 || p == 128;
- }
- }
-
- public static void Play(string absolutePath)
- {
- Stop();
-
- if (!IsUnix)
- {
- try
- {
- var sp = new System.Media.SoundPlayer(absolutePath);
- sp.Play();
- }
- catch (Exception ex) { Console.WriteLine("Playback failed: " + ex.Message); }
- return;
- }
-
- foreach (string player in new[] { "paplay", "aplay" })
- {
- try
- {
- var psi = new ProcessStartInfo(player, "\"" + absolutePath + "\"")
- {
- UseShellExecute = false,
- RedirectStandardError = true
- };
- _current = Process.Start(psi);
- return;
- }
- catch { /* not installed, try the next one */ }
- }
-
- Console.WriteLine("No audio player found — install pulseaudio-utils or alsa-utils");
- }
-
- public static void Stop()
- {
- try
- {
- if (_current != null && !_current.HasExited) _current.Kill();
- }
- catch { }
- _current = null;
- }
-}
\ No newline at end of file
diff --git a/program/homepage.glade b/program/homepage.glade
deleted file mode 100644
index 7c21a1d..0000000
--- a/program/homepage.glade
+++ /dev/null
@@ -1,380 +0,0 @@
-
-
-
-
-
-
- False
- 1280
- 800
-
-
-
- True
- False
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- False
- EARS
- 1280
- 800
-
-
-
- True
- False
- 3
- 5
-
-
- Normal Session
- True
- True
- True
- 50
- 50
- True
- True
-
-
- 1
- 1
-
-
-
-
- Position Recording Mode
- True
- True
- True
- 50
- 50
- True
- True
-
-
- 1
- 2
-
-
-
-
- Exam Mode
- True
- True
- True
- 50
- 50
- True
- True
-
-
- 1
- 3
-
-
-
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/program/homepage.glade~ b/program/homepage.glade~
deleted file mode 100644
index fade189..0000000
--- a/program/homepage.glade~
+++ /dev/null
@@ -1,347 +0,0 @@
-
-
-
-
-
- False
- 1280
- 800
-
-
-
- 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
-
-
-
-
-
-
-
-
-
- False
-
-
-
-
-
- False
- EARS
- 1280
- 800
-
-
-
- True
- False
- 3
- 5
-
-
- Normal Session
- True
- True
- True
- 50
- 50
- True
- True
-
-
- 1
- 1
-
-
-
-
- Position Recording Mode
- True
- True
- True
- 50
- 50
- True
- True
-
-
- 1
- 2
-
-
-
-
- Exam Mode
- True
- True
- True
- 50
- 50
- True
- True
-
-
- 1
- 3
-
-
-
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/program/homepageall.glade b/program/homepageall.glade
deleted file mode 100644
index 03f30c2..0000000
--- a/program/homepageall.glade
+++ /dev/null
@@ -1,487 +0,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
-
-
-
-
-
-
-
-
-
- False
- 1280
- 800
-
-
- True
- False
- vertical
-
-
- True
- False
- label
-
-
-
-
-
- False
- True
- 0
-
-
-
-
-
- True
- False
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- False
- True
- 1
-
-
-
-
-
- True
- False
-
-
- Back
- True
- True
- True
- end
- True
- right
-
-
- 2
- 0
-
-
-
-
-
-
-
-
-
-
- False
- True
- 2
-
-
-
-
-
-
diff --git a/program/homepageallv3.glade b/program/homepageallv3.glade
deleted file mode 100644
index 284cfee..0000000
--- a/program/homepageallv3.glade
+++ /dev/null
@@ -1,445 +0,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/homepageallv4.glade b/program/homepageallv4.glade
deleted file mode 100644
index 6f3fd9c..0000000
--- a/program/homepageallv4.glade
+++ /dev/null
@@ -1,472 +0,0 @@
-
-
-
-
-
- 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
deleted file mode 100644
index dd9fd65..0000000
--- a/program/homepageallv5.glade
+++ /dev/null
@@ -1,499 +0,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
-
-
-
-
- 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
-
-
-
-
-
-
-
-
-
diff --git a/program/homepageallv6.glade b/program/homepageallv6.glade
deleted file mode 100644
index 094dffa..0000000
--- a/program/homepageallv6.glade
+++ /dev/null
@@ -1,831 +0,0 @@
-
-
-
-
-
- False
- 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
- True
- fill
- fill
-
-
- 4
- 4
-
-
-
-
- Settings
- True
- True
- True
- True
- True
- fill
- fill
-
-
- 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
- 12
-
-
- 340
- True
- True
- never
-
-
- True
- False
-
-
- True
- False
- 12
- 12
- 12
- 12
- vertical
- 10
-
-
- True
- False
- 0
- in
-
-
- True
- False
- 8
- 8
- 6
- 6
- -
- True
- 0
-
-
-
-
- True
- False
- 選択中の症例
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 0
- in
-
-
- True
- False
- 8
- 8
- 6
- 6
- vertical
-
-
- スケルトン
- True
- True
- False
- True
- True
-
-
- False
- True
- 0
-
-
-
-
- ジャケット
- True
- True
- False
- True
- BodySkeletonRadio
-
-
- False
- True
- 1
-
-
-
-
-
-
- True
- False
- 体の画像選択
-
-
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- 8
-
-
- True
- False
- サイズ:
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 0
-
- - L
- - XL
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 2
-
-
-
-
- True
- False
- 0
- in
-
-
- True
- False
- 8
- 8
- 6
- 6
- True
-
-
- 前
- True
- True
- False
- True
- False
-
-
- True
- True
- 0
-
-
-
-
- 後
- True
- True
- False
- False
- ViewFrontRadio
-
-
- True
- True
- 1
-
-
-
-
- 右
- True
- True
- False
- False
- ViewFrontRadio
-
-
- True
- True
- 2
-
-
-
-
- 左
- True
- True
- False
- False
- ViewFrontRadio
-
-
- True
- True
- 3
-
-
-
-
-
-
- True
- False
- 視点
-
-
-
-
- False
- True
- 3
-
-
-
-
- 番号を隠す
- True
- True
- False
- True
- True
-
-
- False
- True
- 4
-
-
-
-
- 黒画素でも再生
- True
- True
- False
- True
-
-
- False
- True
- 5
-
-
-
-
- 再生テスト
- True
- True
- True
-
-
- False
- True
- 6
-
-
-
-
- True
- False
- True
- vertical
- 8
-
-
- True
- True
- 7
-
-
-
-
- 戻る
- True
- True
- True
- end
-
-
- False
- True
- 8
-
-
-
-
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 12
- 12
- 12
- True
- vertical
- 6
-
-
- True
- False
- label
-
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- True
- True
-
-
- True
- True
- 1
-
-
-
-
- True
- True
- 1
-
-
-
-
- 3
-
-
-
-
-
-
-
-
-
diff --git a/program/homepageallv7.glade b/program/homepageallv7.glade
deleted file mode 100644
index 0d81a6d..0000000
--- a/program/homepageallv7.glade
+++ /dev/null
@@ -1,819 +0,0 @@
-
-
-
-
-
- False
- False
-
-
- True
- True
- False
- False
-
-
-
- True
- False
- 20
- 5
- 20
-
-
-
- 0
- 0
- 2
-
-
-
-
- Normal Session
- True
- True
- True
- 50
- 20
- True
- True
-
-
- 0
- 1
-
-
-
-
- Position Recording Mode
- True
- True
- True
- 50
- 20
- True
- True
-
-
- 0
- 2
-
-
-
-
- Exam Mode
- True
- True
- True
- 50
- 20
- True
- True
-
-
- 0
- 3
-
-
-
-
- Settings
- True
- True
- True
- center
- center
- True
- True
- 20
- 40
-
-
- 1
- 2
-
-
-
-
- Close
- True
- True
- True
- center
- center
- True
- True
- 20
- 40
-
-
- 1
- 3
-
-
-
-
- True
- False
- center
- center
- True
- Problem connecting with device
-
-
- 0
- 4
- 2
-
-
-
-
-
-
-
- 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
-
-
-
-
- 戻る
- 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
-
-
-
-
- 戻る
- True
- True
- True
- end
- True
- right
-
-
- False
- True
- 4
-
-
-
-
- 2
-
-
-
-
- True
- False
- 12
-
-
- 340
- True
- True
- never
-
-
- True
- False
-
-
- True
- False
- 12
- 12
- 12
- 12
- vertical
- 10
-
-
- True
- False
- 0
- in
-
-
- True
- False
- 8
- 8
- 6
- 6
- -
- True
- 0
-
-
-
-
- True
- False
- 選択中の症例
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 0
- in
-
-
- True
- False
- 8
- 8
- 6
- 6
- vertical
-
-
- スケルトン
- True
- True
- False
- True
- True
-
-
- False
- True
- 0
-
-
-
-
- ジャケット
- True
- True
- False
- True
- BodySkeletonRadio
-
-
- False
- True
- 1
-
-
-
-
-
-
- True
- False
- 体の画像選択
-
-
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- 8
-
-
- True
- False
- サイズ:
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 0
-
- - L
- - XL
-
-
-
- False
- True
- 1
-
-
-
-
- False
- True
- 2
-
-
-
-
- True
- False
- 0
- in
-
-
- True
- False
- 8
- 8
- 6
- 6
- True
-
-
- 前
- True
- True
- False
- True
- False
-
-
- True
- True
- 0
-
-
-
-
- 後
- True
- True
- False
- False
- ViewFrontRadio
-
-
- True
- True
- 1
-
-
-
-
- 右
- True
- True
- False
- False
- ViewFrontRadio
-
-
- True
- True
- 2
-
-
-
-
- 左
- True
- True
- False
- False
- ViewFrontRadio
-
-
- True
- True
- 3
-
-
-
-
-
-
- True
- False
- 視点
-
-
-
-
- False
- True
- 3
-
-
-
-
- 番号を隠す
- True
- True
- False
- True
- True
-
-
- False
- True
- 4
-
-
-
-
- 黒画素でも再生
- True
- True
- False
- True
-
-
- False
- True
- 5
-
-
-
-
- 再生テスト
- True
- True
- True
-
-
- False
- True
- 6
-
-
-
-
- True
- False
- True
- vertical
- 8
-
-
- True
- True
- 7
-
-
-
-
- 戻る
- True
- True
- True
- end
-
-
- False
- True
- 8
-
-
-
-
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- 12
- 12
- 12
- True
- vertical
- 6
-
-
- True
- False
- label
-
-
-
-
-
- False
- True
- 0
-
-
-
-
- True
- False
- True
- True
-
-
- True
- True
- 1
-
-
-
-
- True
- True
- 1
-
-
-
-
- 3
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/program/info_eng.csv b/program/info_eng.csv
deleted file mode 100644
index 02440d3..0000000
--- a/program/info_eng.csv
+++ /dev/null
@@ -1,29 +0,0 @@
-key,text
-Option1Button,Normal Session
-Option2Button,Position Recording Mode
-Option3Button,Exam Mode
-SettingButton,Settings
-CloseButton,Close
-LanguageLabel,Language
-LanguageToggle,あ
-ComPortLabel,COM Port
-LogLabel,Log
-AutoConnectButton,Auto Connect
-RefreshButton,Refresh Ports
-SettingBackButton,Back
-ConnectButton.Connect,Connect
-ConnectButton.Disconnect,Disconnect
-StatusLabel.Connected,Connected ({0})
-StatusLabel.Disconnected,Disconnected
-ConnectionIcon.Connected,Connected to {0}
-ConnectionIcon.Disconnected,Device not connected
-Log.PortsFound,{0} port(s) found
-Log.NoPorts,No serial ports found
-Log.SelectPort,Select a COM port first
-Log.Scanning,Scanning ports for device...
-Log.DeviceNotFound,Device not found — use manual connection
-Log.EspNowConnected,[ESP-NOW] Peer connected
-Log.EspNowDisconnected,[ESP-NOW] Peer disconnected
-SoundBackButton,Back
-MapBackButton,Back
-ConnectionAlertLabel,Problem connecting with device
\ No newline at end of file
diff --git a/program/info_jpn.csv b/program/info_jpn.csv
deleted file mode 100644
index 23de4cb..0000000
--- a/program/info_jpn.csv
+++ /dev/null
@@ -1,29 +0,0 @@
-key,text
-Option1Button,通常セッション
-Option2Button,位置記録モード
-Option3Button,試験モード
-SettingButton,設定
-CloseButton,終了
-LanguageLabel,言語
-LanguageToggle,A
-ComPortLabel,COMポート
-LogLabel,ログ
-AutoConnectButton,自動接続
-RefreshButton,ポート更新
-SettingBackButton,戻る
-ConnectButton.Connect,接続
-ConnectButton.Disconnect,切断
-StatusLabel.Connected,接続済み ({0})
-StatusLabel.Disconnected,未接続
-ConnectionIcon.Connected,{0} に接続しました
-ConnectionIcon.Disconnected,デバイスが接続されていません
-Log.PortsFound,ポートが {0} 個見つかりました
-Log.NoPorts,シリアルポートが見つかりません
-Log.SelectPort,COMポートを選択してください
-Log.Scanning,デバイスを検索中...
-Log.DeviceNotFound,デバイスが見つかりません — 手動で接続してください
-Log.EspNowConnected,[ESP-NOW] 相手が接続しました
-Log.EspNowDisconnected,[ESP-NOW] 相手が切断しました
-SoundBackButton,戻る
-MapBackButton,戻る
-ConnectionAlertLabel,デバイスに接続できません
\ No newline at end of file
diff --git a/program/lang_eng.css b/program/lang_eng.css
deleted file mode 100644
index 220280a..0000000
--- a/program/lang_eng.css
+++ /dev/null
@@ -1,6 +0,0 @@
-#Option1Button, #Option2Button, #Option3Button, #CloseButton {
- font-family: 'Sans';
-}
-
-#StatusLabel.status-on { color: #2e7d32; font-weight: bold; }
-#StatusLabel.status-off { color: #c62828; font-weight: bold; }
\ No newline at end of file
diff --git a/program/lang_jpn.css b/program/lang_jpn.css
deleted file mode 100644
index dcc6859..0000000
--- a/program/lang_jpn.css
+++ /dev/null
@@ -1,6 +0,0 @@
-#Option1Button, #Option2Button, #Option3Button, #CloseButton {
- font-family: 'Noto Sans JP';
-}
-
-#StatusLabel.status-on { color: #2e7d32; font-weight: bold; }
-#StatusLabel.status-off { color: #c62828; font-weight: bold; }
\ No newline at end of file
diff --git a/program/program.cs b/program/program.cs
deleted file mode 100644
index f9db539..0000000
--- a/program/program.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using Gtk;
-
-class Program
-{
- [STAThread]
- static void Main(string[] args)
- {
- Application.Init();
-
- var app = new MainWindow();
- app.ShowAll();
-
- Application.Run();
- }
-}
\ No newline at end of file
diff --git a/program/style.css b/program/style.css
deleted file mode 100644
index e69de29..0000000
--- a/program/style.css
+++ /dev/null
diff --git a/program/touch_friendly.css b/program/touch_friendly.css
deleted file mode 100644
index 207a7d8..0000000
--- a/program/touch_friendly.css
+++ /dev/null
@@ -1,211 +0,0 @@
-/* touch_friendly.css — EARS look, authored at 1280x800 */
-
-window {
- background-image: url("back.png");
- background-size: cover;
- background-position: center;
-}
-
-notebook, notebook > stack, notebook header,
-grid, box, viewport, scrolledwindow, frame {
- background-color: transparent;
- background-image: none;
-}
-scrolledwindow, #SoundScroller { border: none; box-shadow: none; }
-
-/* NOTE: scoped to standalone labels only — a bare `label {}` rule would
- repaint every button's text and override the button colors below. */
-#StatusLabel, #ComPortLabel, #LogLabel, #LanguageLabel,
-checkbutton, radiobutton, combobox {
- font-family: 'Noto Sans JP', 'IPAPGothic', 'Noto Sans', sans-serif;
- color: #1e1e1e;
- font-size: 14px;
-}
-
-/* ---------- shared button look ---------- */
-button, button label {
- font-family: 'Noto Sans JP', 'IPAPGothic', 'Noto Sans', sans-serif;
- font-weight: 700;
- color: #ffffff;
-}
-button {
- border-radius: 0;
- border: 2px solid rgba(0,0,0,0.45);
- background-image: none;
- box-shadow: none;
- text-shadow: none;
- padding: 0;
- min-height: 0;
- min-width: 0;
-}
-
-/* ---------- navy title banner ---------- */
-#ConditionNameLabel {
- font-family: 'Noto Sans JP', 'IPAPGothic', sans-serif;
- font-size: 46px;
- font-weight: 700;
- color: #ffffff;
- background-color: #0B3B6B;
- padding: 10px 26px;
- margin: 14px 40px 24px 40px;
-}
-
-/* ---------- splash / main menu ---------- */
-#Option1Button, #Option2Button, #Option3Button {
- min-height: 95px;
- background-color: #6A991A;
-}
-#Option1Button label, #Option2Button label, #Option3Button label { font-size: 50px; }
-#Option1Button:hover, #Option2Button:hover, #Option3Button:hover { background-color: #7BAE22; }
-#Option1Button:active, #Option2Button:active, #Option3Button:active { background-color: #5A8315; }
-
-#SettingButton { min-height: 95px;min-width: 200px; background-color: #F97F00; }
-#SettingButton label { font-size: 34px; }
-#SettingButton:hover { background-color: #FF9020; }
-#SettingButton:active { background-color: #D96C00; }
-
-#CloseButton { min-height: 95px;min-width: 200px; background-color: #808080; }
-#CloseButton label { font-size: 34px; color: #ffffff; }
-#CloseButton:hover { background-color: #909090; }
-#CloseButton:active { background-color: #6E6E6E; }
-
-#LanguageToggle { min-height: 46px; min-width: 80px; background-color: #ffffff; }
-#LanguageToggle label { font-size: 20px; color: #1e1e1e; }
-#LanguageToggle:checked { background-color: #0B3B6B; }
-#LanguageToggle:checked label { color: #ffffff; }
-
-/* ---------- back buttons ---------- */
-#SoundBackButton, #MapBackButton, #SettingBackButton {
- min-width: 100px;
- min-height: 58px;
- background-color: #F0F0F0;
- border: 2px solid #B5B5B5;
-}
-#SoundBackButton label, #MapBackButton label, #SettingBackButton label {
- font-size: 20px;
- color: #111111;
-}
-#SoundBackButton:hover, #MapBackButton:hover, #SettingBackButton:hover {
- background-color: #E2E2E2;
-}
-
-/* ---------- dynamic sound-list buttons ---------- */
-.sound-clip { min-height: 74px; background-color: #3592C8; }
-.sound-clip label { font-size: 20px; color: #ffffff; }
-.sound-clip.heart { background-color: #CE3733; }
-.sound-clip.lung { background-color: #3592C8; }
-
-.sound-clip.compact { min-height: 52px; }
-.sound-clip.compact label { font-size: 19px; }
-
-.sound-clip.top { min-height: 112px; }
-.sound-clip.top label { font-size: 34px; }
-
-/* ---------- settings page ---------- */
-#ConnectButton, #AutoConnectButton, #RefreshButton {
- min-height: 46px; min-width: 150px; background-color: #0B3B6B;
-}
-#ConnectButton label, #AutoConnectButton label, #RefreshButton label { font-size: 19px; }
-#ComPortComboBox { min-height: 40px; font-size: 17px; }
-#StatusLabel.status-on { color: #2e7d32; font-weight: bold; font-size: 17px; }
-#StatusLabel.status-off { color: #c62828; font-weight: bold; font-size: 17px; }
-
-/* ══════════════════════════════════════════════════════════════════
- MAP PAGE ONLY — scoped under #MapBox, so nothing here can leak to
- the splash, sound or settings pages. Sizes target the `label` node
- directly because _scaleCssProvider emits a plain `label {}` rule
- that beats any inherited value.
- ══════════════════════════════════════════════════════════════════ */
-
-/* ── map page: composite widgets whose inner button node has no class ── */
-
-/* GtkRadioButton with draw-indicator=False renders as a button node that
- is a CHILD of the widget carrying .mappage, so button.mappage misses it. */
-#ViewFrontRadio, #ViewBackRadio, #ViewRightRadio, #ViewLeftRadio,
-#ViewFrontRadio button, #ViewBackRadio button,
-#ViewRightRadio button, #ViewLeftRadio button {
- color: #111111;
- background-color: #F0F0F0;
- background-image: none;
- border: 2px solid #B5B5B5;
- border-radius: 0;
- min-height: 46px;
-}
-#ViewFrontRadio label, #ViewBackRadio label,
-#ViewRightRadio label, #ViewLeftRadio label {
- color: #111111;
- font-size: 18px;
- font-weight: 700;
-}
-#ViewFrontRadio:checked, #ViewBackRadio:checked,
-#ViewRightRadio:checked, #ViewLeftRadio:checked,
-#ViewFrontRadio button:checked, #ViewBackRadio button:checked,
-#ViewRightRadio button:checked, #ViewLeftRadio button:checked {
- background-color: #2e7d32;
- border-color: #1f5c23;
-}
-#ViewFrontRadio:checked label, #ViewBackRadio:checked label,
-#ViewRightRadio:checked label, #ViewLeftRadio:checked label {
- color: #ffffff;
-}
-
-/* 再生テスト — accent fill, white text is correct here */
-#SoundTestButton {
- background-color: #0B3B6B;
- background-image: none;
- border: 2px solid #072a4d;
- border-radius: 0;
- min-height: 46px;
-}
-#SoundTestButton label { color: #ffffff; font-size: 18px; font-weight: 700; }
-#SoundTestButton:hover { background-color: #14538F; }
-
-/* サイズ combo — the visible box is a button node inside the combobox */
-#JacketSizeCombo,
-#JacketSizeCombo button {
- color: #111111;
- background-color: #ffffff;
- background-image: none;
- border: 1px solid #B5B5B5;
- border-radius: 0;
- min-height: 36px;
- min-width: 90px;
-}
-#JacketSizeCombo label,
-#JacketSizeCombo button label,
-#JacketSizeCombo cellview {
- color: #111111;
- font-size: 16px;
- font-weight: 400;
-}
-#JacketSizeCombo arrow { color: #111111; min-width: 16px; min-height: 16px; }
-
-/* the popup list is a separate toplevel — style it too or it inherits white */
-#JacketSizeCombo window.popup,
-#JacketSizeCombo window.popup list,
-#JacketSizeCombo window.popup row label {
- color: #111111;
- background-color: #ffffff;
- font-size: 16px;
-}
-window.popup list row label { color: #111111; }
-
-/* inactive buttons */
-/* splash: modes unavailable until the device connects */
-#Option1Button:disabled, #Option2Button:disabled, #Option3Button:disabled {
- background-color: #A8AFA0;
- border-color: rgba(0,0,0,0.25);
-}
-#Option1Button:disabled label,
-#Option2Button:disabled label,
-#Option3Button:disabled label { color: #EFEFEF; }
-
-#ConnectionAlertLabel {
- font-family: 'Noto Sans JP', 'IPAPGothic', sans-serif;
- font-size: 22px;
- font-weight: 700;
- color: #ffffff;
- background-color: #C62828;
- padding: 10px 24px;
- margin: 8px 40px;
-}
\ No newline at end of file