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 - 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 - 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 - - - True - False - horizontal - 12 - end - center - True - False - 40 - 8 - - - True - False - center - Language - - - False - False - 0 - - - - - A / あ - True - True - True - False - center - - - False - False - 1 - - - - - True - False - network-offline - 6 - center - - - False - False - 2 - - - - - 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