Add custom icon and tray-minimize behavior (like VersaGUI's TrayApp)
Minimizing or closing hides the window instead of leaving a taskbar entry; only "Beenden" in the tray right-click menu really exits. Tray callbacks route through a queue instead of touching Tk directly from pystray's thread (same pattern as the earlier serial-thread fix). Also fixes the build script: PyInstaller failed on the network share while copying Tcl/Tk tzdata (path-length issue), so building now happens in a local temp copy instead of directly on Z:. Adds an info button that explains the MCP server's available tools.
This commit is contained in:
parent
e456af7c19
commit
4ac29a3e5c
6 changed files with 208 additions and 25 deletions
61
AGENTS.md
61
AGENTS.md
|
|
@ -116,30 +116,63 @@ zuverlässig zur echten Python313-Installation auf.
|
||||||
|
|
||||||
## Gepackte .exe (PyInstaller) — kein Terminal-Flash, kein Python-Setup nötig
|
## Gepackte .exe (PyInstaller) — kein Terminal-Flash, kein Python-Setup nötig
|
||||||
|
|
||||||
`build_and_deploy.ps1` baut `desktop_viewer.py` mit PyInstaller
|
`build_and_deploy.ps1` kopiert den Quellcode zuerst nach `%TEMP%`, baut dort
|
||||||
(`--windowed --onedir`) und kopiert das Ergebnis lokal nach
|
mit PyInstaller (`--windowed --onedir --icon icon.ico`), und kopiert das
|
||||||
`%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`. Desktop-Verknüpfung
|
Ergebnis nach `%LOCALAPPDATA%\VersaPadViewer\VersaPadViewer.exe`.
|
||||||
"VersaPad Viewer (Fenster)" zeigt dorthin.
|
Desktop-Verknüpfung "VersaPad Viewer (Fenster)" zeigt dorthin (inkl. eigenem
|
||||||
|
Icon, `IconLocation` zeigt auf die `.exe` selbst statt auf `pythonw.exe`).
|
||||||
|
|
||||||
**`--onedir`, nicht `--onefile`** — Single-File-PyInstaller-Bundles lösen
|
**`--onedir`, nicht `--onefile`** — Single-File-PyInstaller-Bundles lösen
|
||||||
öfter AV-Fehlalarme aus (sehen strukturell wie ein Packer aus); `--onedir`
|
öfter AV-Fehlalarme aus (sehen strukturell wie ein Packer aus); `--onedir`
|
||||||
(Ordner mit `.exe` + `_internal`) ist unauffälliger. Trotzdem unsigniert,
|
(Ordner mit `.exe` + `_internal`) ist unauffälliger. Trotzdem unsigniert,
|
||||||
also grundsätzlich nie über Smart App Control/AV in Stein gemeißelt sicher.
|
also grundsätzlich nie über Smart App Control/AV in Stein gemeißelt sicher.
|
||||||
|
|
||||||
**Kritischer Stolperstein:** Die gebaute `.exe` läuft NICHT vom Netzlaufwerk
|
**Zwei Netzlaufwerk-Stolpersteine, deshalb baut UND deployt alles lokal:**
|
||||||
aus (`Z:\Git\...`, SMB-Share auf cj-ki) — Windows blockiert das Nachladen
|
1. Laufzeit: die gebaute `.exe` läuft NICHT vom Netzlaufwerk aus (`Z:\Git\...`,
|
||||||
der `_internal`-DLLs von einem Netzwerkpfad, der Prozess startet dann
|
SMB-Share auf cj-ki) — Windows blockiert das Nachladen der
|
||||||
einfach nie, ganz ohne Fehlermeldung/Crash-Log/AV-Benachrichtigung (sah
|
`_internal`-DLLs von einem Netzwerkpfad, der Prozess startet dann einfach
|
||||||
zuerst wie ein AV-Block aus, war aber keiner — sauber verifiziert indem
|
nie, ganz ohne Fehlermeldung/Crash-Log/AV-Benachrichtigung (sah zuerst wie
|
||||||
dieselbe `.exe` 1:1 lokal kopiert sofort lief). Bauen darf auf Z: passieren,
|
ein AV-Block aus, war aber keiner — verifiziert indem dieselbe `.exe` 1:1
|
||||||
das fertige Bundle muss vor dem Start nach C: kopiert werden — genau das
|
lokal kopiert sofort lief).
|
||||||
macht `build_and_deploy.ps1`.
|
2. Buildzeit: PyInstaller scheitert beim Kopieren der Tcl/Tk-`tzdata`
|
||||||
|
(viele tief verschachtelte, lang benannte Zeitzonen-Ordner) auf dem
|
||||||
|
SMB-Share mit `FileNotFoundError: [WinError 3]` — Pfadlängen-Problem,
|
||||||
|
kombiniert mit dem eh schon langen Projektpfad (`Z:\Git\Versa Board\
|
||||||
|
versapad-viewer\dist\...`).
|
||||||
|
|
||||||
|
Deshalb kopiert `build_and_deploy.ps1` den Quellcode zuerst nach `%TEMP%`
|
||||||
|
und baut NUR dort — Z: wird nur zum Lesen der `.py`-Dateien angefasst.
|
||||||
|
|
||||||
Bei erneutem Bauen: `.\build_and_deploy.ps1` in PowerShell ausführen
|
Bei erneutem Bauen: `.\build_and_deploy.ps1` in PowerShell ausführen
|
||||||
(lokal, nicht über die sandboxed Bash-Tool-Umgebung — die hat eine eigene
|
(lokal, nicht über die sandboxed Bash-Tool-Umgebung — die hat eine eigene
|
||||||
Dateisystem-Sicht, die nicht zuverlässig mit dem echten Windows-Dateisystem
|
Dateisystem-Sicht, die nicht zuverlässig mit dem echten Windows-Dateisystem
|
||||||
übereinstimmt; Kopieroperationen für Dinge, die der User/andere Prozesse
|
übereinstimmt; siehe Sessionlog: ein `cp -r` über Bash landete an einem Ort,
|
||||||
sehen sollen, gehören in PowerShell, nicht in Bash).
|
den PowerShell/der reale Explorer nicht sah. Kopieroperationen für Dinge,
|
||||||
|
die der User/andere Prozesse sehen sollen, gehören in PowerShell, nicht in
|
||||||
|
Bash).
|
||||||
|
|
||||||
|
## Icon + Tray (kein Taskleisten-Eintrag beim Minimieren, wie VersaGUI)
|
||||||
|
|
||||||
|
- `icon.png`/`icon.ico` — generiert via Pillow (dunkles abgerundetes Quadrat,
|
||||||
|
blauer Block, 2x2-Punktraster; flaches Design, keine Farbverläufe/
|
||||||
|
Anti-Aliasing-Details, siehe Speicher-Notiz zu Tkinter-Flat-Design).
|
||||||
|
`.ico` fürs Fenster-/Taskleisten-/Exe-Icon, `.png` fürs Tray-Icon (pystray
|
||||||
|
braucht ein PIL-Image, kein Multi-Res-`.ico`).
|
||||||
|
- `resource_path()` in `desktop_viewer.py` findet Assets sowohl im
|
||||||
|
Quellordner als auch im PyInstaller-Bundle (`sys._MEIPASS`).
|
||||||
|
- Minimieren (`<Unmap>` mit `state()=='iconic'`) ruft `withdraw()` statt
|
||||||
|
normal zu minimieren → kein Taskleisten-Eintrag, nur noch Tray-Icon.
|
||||||
|
Schließen (X) macht dasselbe (`_hide_to_tray`, nicht `_quit`) — echtes
|
||||||
|
Beenden nur über "Beenden" im Tray-Rechtsklickmenü (mirrort VersaGUIs
|
||||||
|
`TrayApp`-Menü: Öffnen/Beenden). Tray-Icon läuft über `pystray.Icon.
|
||||||
|
run_detached()` in eigenem Thread — Klicks im Tray-Menü legen nur ein
|
||||||
|
Ereignis in `self._tray_queue`, das der Main-Thread per `after()`-Polling
|
||||||
|
abholt (dieselbe Queue-statt-Direktzugriff-Regel wie beim Serial-Thread,
|
||||||
|
siehe oben — Tk-Aufrufe aus einem Fremdthread haben schon mal einen
|
||||||
|
stillen Absturz verursacht).
|
||||||
|
- Neue Abhängigkeiten: `pystray`, `pillow` (zusätzlich zu `pyserial`).
|
||||||
|
- Info-Button (ⓘ oben rechts im Header) öffnet einen Dialog mit der
|
||||||
|
MCP-Server-Doku (`MCP_INFO_TEXT`-Konstante) — rein informativ, keine Logik.
|
||||||
|
|
||||||
## Dateien/Pfade (hardcoded, hal9001-spezifisch)
|
## Dateien/Pfade (hardcoded, hal9001-spezifisch)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ a = Analysis(
|
||||||
['desktop_viewer.py'],
|
['desktop_viewer.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[],
|
datas=[('icon.png', '.'), ('icon.ico', '.')],
|
||||||
hiddenimports=[],
|
hiddenimports=[],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
|
|
@ -32,6 +32,7 @@ exe = EXE(
|
||||||
target_arch=None,
|
target_arch=None,
|
||||||
codesign_identity=None,
|
codesign_identity=None,
|
||||||
entitlements_file=None,
|
entitlements_file=None,
|
||||||
|
icon=['icon.ico'],
|
||||||
)
|
)
|
||||||
coll = COLLECT(
|
coll = COLLECT(
|
||||||
exe,
|
exe,
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,38 @@
|
||||||
# Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis
|
# Baut VersaPadViewer.exe (PyInstaller --onedir) und kopiert das Ergebnis
|
||||||
# nach lokal (C:\Users\<user>\AppData\Local\VersaPadViewer).
|
# nach lokal (C:\Users\<user>\AppData\Local\VersaPadViewer).
|
||||||
#
|
#
|
||||||
# WICHTIG: Muss lokal laufen, NICHT direkt vom Netzlaufwerk (Z:\Git\...) --
|
# WICHTIG: Sowohl Bauen als auch Laufen muessen lokal passieren, NICHT auf
|
||||||
# Windows blockiert das Nachladen der PyInstaller-_internal-DLLs von einem
|
# dem Netzlaufwerk (Z:\Git\...):
|
||||||
# Netzwerkpfad (SMB-Share) ohne jede Fehlermeldung, der Prozess startet
|
# - Laufzeit: Windows blockiert das Nachladen der PyInstaller-_internal-
|
||||||
# einfach nie. Deshalb bauen wir hier (Quelle darf auf Z: liegen) und
|
# DLLs von einem Netzwerkpfad (SMB-Share) ohne jede Fehlermeldung, der
|
||||||
# kopieren das fertige Bundle danach nach C:.
|
# Prozess startet einfach nie.
|
||||||
|
# - Buildzeit: PyInstaller scheitert beim Kopieren von Tcl/Tk-tzdata (viele
|
||||||
|
# tief verschachtelte, lang benannte Zeitzonen-Ordner) mit
|
||||||
|
# "FileNotFoundError: [WinError 3]" -- Pfadlaengen-Problem auf dem
|
||||||
|
# SMB-Share, kombiniert mit dem eh schon langen Projektpfad.
|
||||||
|
#
|
||||||
|
# Deshalb: Quellcode zuerst nach lokal (%TEMP%) kopieren, dort bauen, danach
|
||||||
|
# das fertige Bundle nach %LOCALAPPDATA% kopieren. Z: wird nur zum Lesen der
|
||||||
|
# Quelldateien angefasst.
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$projectDir = $PSScriptRoot
|
$projectDir = $PSScriptRoot
|
||||||
|
$buildSrc = "$env:TEMP\versapad_build_src"
|
||||||
$localDir = "$env:LOCALAPPDATA\VersaPadViewer"
|
$localDir = "$env:LOCALAPPDATA\VersaPadViewer"
|
||||||
|
|
||||||
Push-Location $projectDir
|
if (Test-Path $buildSrc) {
|
||||||
|
Remove-Item $buildSrc -Recurse -Force
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Path $buildSrc | Out-Null
|
||||||
|
Copy-Item "$projectDir\*.py" -Destination $buildSrc
|
||||||
|
Copy-Item "$projectDir\icon.ico" -Destination $buildSrc
|
||||||
|
Copy-Item "$projectDir\icon.png" -Destination $buildSrc
|
||||||
|
|
||||||
|
Push-Location $buildSrc
|
||||||
try {
|
try {
|
||||||
py -m PyInstaller --windowed --onedir --name VersaPadViewer desktop_viewer.py --noconfirm
|
py -m PyInstaller --windowed --onedir --name VersaPadViewer `
|
||||||
|
--icon icon.ico --add-data "icon.png;." --add-data "icon.ico;." `
|
||||||
|
desktop_viewer.py --noconfirm
|
||||||
} finally {
|
} finally {
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +40,7 @@ try {
|
||||||
if (Test-Path $localDir) {
|
if (Test-Path $localDir) {
|
||||||
Remove-Item $localDir -Recurse -Force
|
Remove-Item $localDir -Recurse -Force
|
||||||
}
|
}
|
||||||
Copy-Item "$projectDir\dist\VersaPadViewer" -Destination $localDir -Recurse
|
Copy-Item "$buildSrc\dist\VersaPadViewer" -Destination $localDir -Recurse
|
||||||
|
Remove-Item $buildSrc -Recurse -Force
|
||||||
|
|
||||||
Write-Host "Fertig: $localDir\VersaPadViewer.exe"
|
Write-Host "Fertig: $localDir\VersaPadViewer.exe"
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,28 @@ Start: python desktop_viewer.py
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import filedialog, messagebox, simpledialog
|
from tkinter import filedialog, messagebox, simpledialog
|
||||||
|
|
||||||
|
import pystray
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
import action_dialog
|
import action_dialog
|
||||||
import versapad_combined as vcomb
|
import versapad_combined as vcomb
|
||||||
import versapad_data as vp
|
import versapad_data as vp
|
||||||
import versapad_protocol as vproto
|
import versapad_protocol as vproto
|
||||||
import versapad_serial as vs
|
import versapad_serial as vs
|
||||||
|
|
||||||
|
|
||||||
|
def resource_path(name):
|
||||||
|
"""Findet Assets (z.B. icon.png) sowohl im Quellordner als auch im
|
||||||
|
PyInstaller-Bundle (sys._MEIPASS zur Laufzeit gesetzt)."""
|
||||||
|
base = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
return os.path.join(base, name)
|
||||||
|
|
||||||
BG = "#14161b"
|
BG = "#14161b"
|
||||||
CARD_BG = "#1e2129"
|
CARD_BG = "#1e2129"
|
||||||
CARD_BORDER = "#2a2d37"
|
CARD_BORDER = "#2a2d37"
|
||||||
|
|
@ -57,6 +68,42 @@ SERIAL_STATUS_TEXT = {
|
||||||
"too_large": "Datenblock zu groß",
|
"too_large": "Datenblock zu groß",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MCP_INFO_TEXT = """Registrierung (einmalig, schon erledigt):
|
||||||
|
claude mcp add -s user versapad -- <Python313> versapad_mcp_server.py
|
||||||
|
landet in ~/.claude.json, gilt fuer alle Claude-Code-Sessions.
|
||||||
|
|
||||||
|
Lesen:
|
||||||
|
list_profiles() Profile + lokale Namen
|
||||||
|
get_profile(profile) alle 20 Buttons + 4 Encoder, lesbar beschriftet
|
||||||
|
get_macro(slot) Tastenfolge eines Makro-Slots (0-31)
|
||||||
|
get_board_status() ist das Board erreichbar, welches Profil aktiv
|
||||||
|
|
||||||
|
Buttons setzen (profile 0-2, index 0-19):
|
||||||
|
set_button_key(profile, index, key, modifiers)
|
||||||
|
set_button_consumer(profile, index, consumer)
|
||||||
|
set_button_macro(profile, index, slot)
|
||||||
|
set_button_profile_switch(profile, index, target)
|
||||||
|
set_button_none(profile, index)
|
||||||
|
set_button_led(profile, index, r, g, b, anim, period_ms)
|
||||||
|
|
||||||
|
Encoder setzen (index 0-3, field 'sw'/'cw'/'ccw'):
|
||||||
|
set_encoder_key / _consumer / _macro / _profile_switch / _none(...)
|
||||||
|
|
||||||
|
Makro:
|
||||||
|
set_macro(slot, steps) steps=[{"key":"Z","modifiers":["Strg"]}, ...]
|
||||||
|
|
||||||
|
Profilname (nur lokal, nicht aufs Board):
|
||||||
|
rename_profile(profile, name)
|
||||||
|
|
||||||
|
Speichern/Laden:
|
||||||
|
save_local() / load_local() <-> versapad_config_all.json
|
||||||
|
load_from_board() / write_to_board() <-> echtes Board per Serial
|
||||||
|
|
||||||
|
Wichtig: set_*-Aufrufe aendern nur den In-Memory-State. Erst
|
||||||
|
save_local() oder write_to_board() macht die Aenderung dauerhaft.
|
||||||
|
Board-Zugriff braucht den COM-Port exklusiv (nicht gleichzeitig mit
|
||||||
|
VersaGUI oder Live-Sync/Programmiermodus hier in der GUI)."""
|
||||||
|
|
||||||
|
|
||||||
class VersaPadViewer(tk.Tk):
|
class VersaPadViewer(tk.Tk):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -75,6 +122,21 @@ class VersaPadViewer(tk.Tk):
|
||||||
self._serial_results = queue.Queue()
|
self._serial_results = queue.Queue()
|
||||||
self._serial_thread = threading.Thread(target=self._serial_loop, daemon=True)
|
self._serial_thread = threading.Thread(target=self._serial_loop, daemon=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.iconbitmap(resource_path("icon.ico"))
|
||||||
|
except tk.TclError:
|
||||||
|
pass # z.B. kein .ico verfuegbar -- kein Beinbruch, nur Fenster-Icon fehlt dann
|
||||||
|
|
||||||
|
self._tray_queue = queue.Queue()
|
||||||
|
self._tray_icon = pystray.Icon(
|
||||||
|
"versapad", Image.open(resource_path("icon.png")), "VersaPad Steuermatrix",
|
||||||
|
menu=pystray.Menu(
|
||||||
|
pystray.MenuItem("Öffnen", lambda: self._tray_queue.put("show"), default=True),
|
||||||
|
pystray.MenuItem("Beenden", lambda: self._tray_queue.put("quit")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self._tray_icon.run_detached()
|
||||||
|
|
||||||
header = tk.Frame(self, bg=BG)
|
header = tk.Frame(self, bg=BG)
|
||||||
header.pack(fill="x", padx=20, pady=(18, 4))
|
header.pack(fill="x", padx=20, pady=(18, 4))
|
||||||
tk.Label(header, text="VersaPad Steuermatrix", bg=BG, fg=TEXT,
|
tk.Label(header, text="VersaPad Steuermatrix", bg=BG, fg=TEXT,
|
||||||
|
|
@ -83,6 +145,13 @@ class VersaPadViewer(tk.Tk):
|
||||||
fg=TEXT_DIM, font=("Segoe UI", 9))
|
fg=TEXT_DIM, font=("Segoe UI", 9))
|
||||||
self.header_sub.pack(anchor="w")
|
self.header_sub.pack(anchor="w")
|
||||||
|
|
||||||
|
info_btn = tk.Label(header, text="ⓘ", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 14),
|
||||||
|
cursor="hand2")
|
||||||
|
info_btn.place(relx=1.0, x=0, y=-4, anchor="ne")
|
||||||
|
info_btn.bind("<Button-1>", lambda e: self._show_mcp_info())
|
||||||
|
info_btn.bind("<Enter>", lambda e: info_btn.configure(fg=ACCENT))
|
||||||
|
info_btn.bind("<Leave>", lambda e: info_btn.configure(fg=TEXT_DIM))
|
||||||
|
|
||||||
self.tabs = tk.Frame(self, bg=BG)
|
self.tabs = tk.Frame(self, bg=BG)
|
||||||
self.tabs.pack(fill="x", padx=20, pady=(12, 10))
|
self.tabs.pack(fill="x", padx=20, pady=(12, 10))
|
||||||
self.tab_buttons = {}
|
self.tab_buttons = {}
|
||||||
|
|
@ -135,12 +204,14 @@ class VersaPadViewer(tk.Tk):
|
||||||
self.footer = tk.Label(self, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8))
|
self.footer = tk.Label(self, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8))
|
||||||
self.footer.pack(anchor="w", padx=20, pady=(20, 10))
|
self.footer.pack(anchor="w", padx=20, pady=(20, 10))
|
||||||
|
|
||||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
self.protocol("WM_DELETE_WINDOW", self._hide_to_tray)
|
||||||
|
self.bind("<Unmap>", self._on_unmap)
|
||||||
self._serial_thread.start()
|
self._serial_thread.start()
|
||||||
|
|
||||||
self.set_profile(0)
|
self.set_profile(0)
|
||||||
self.after(POLL_MS, self._poll)
|
self.after(POLL_MS, self._poll)
|
||||||
self.after(200, self._drain_serial_queue)
|
self.after(200, self._drain_serial_queue)
|
||||||
|
self.after(200, self._drain_tray_queue)
|
||||||
|
|
||||||
# ── Profil-Auswahl ─────────────────────────────────────────────────────
|
# ── Profil-Auswahl ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -168,6 +239,30 @@ class VersaPadViewer(tk.Tk):
|
||||||
self.combined["profile_names"][profile] = name
|
self.combined["profile_names"][profile] = name
|
||||||
self._update_tab_labels()
|
self._update_tab_labels()
|
||||||
|
|
||||||
|
def _show_mcp_info(self):
|
||||||
|
win = tk.Toplevel(self)
|
||||||
|
win.title("MCP-Server: versapad")
|
||||||
|
win.configure(bg=BG)
|
||||||
|
win.geometry("480x520")
|
||||||
|
win.transient(self)
|
||||||
|
|
||||||
|
tk.Label(win, text="MCP-Server \"versapad\"", bg=BG, fg=TEXT,
|
||||||
|
font=("Segoe UI", 13, "bold")).pack(anchor="w", padx=16, pady=(16, 4))
|
||||||
|
tk.Label(win, text="Lässt eine KI (z.B. Claude) die VersaPad-Config direkt per\n"
|
||||||
|
"Tool-Aufruf lesen/bearbeiten -- ohne JSON von Hand zu schreiben\n"
|
||||||
|
"oder Klicks in dieser GUI.",
|
||||||
|
bg=BG, fg=TEXT_DIM, font=("Segoe UI", 9), justify="left").pack(anchor="w", padx=16)
|
||||||
|
|
||||||
|
text = tk.Text(win, bg=CARD_BG, fg=TEXT, font=("Consolas", 9), wrap="word",
|
||||||
|
relief="flat", padx=12, pady=10, bd=0, highlightthickness=0)
|
||||||
|
text.pack(fill="both", expand=True, padx=16, pady=12)
|
||||||
|
text.insert("1.0", MCP_INFO_TEXT)
|
||||||
|
text.configure(state="disabled")
|
||||||
|
|
||||||
|
tk.Button(win, text="Schließen", command=win.destroy, bg=CARD_BG, fg=TEXT,
|
||||||
|
activebackground=ACCENT, activeforeground="#fff", relief="flat",
|
||||||
|
padx=14, pady=4).pack(pady=(0, 16))
|
||||||
|
|
||||||
# ── Config-Datei-Polling (nur im Nicht-Edit-Modus relevant) ────────────
|
# ── Config-Datei-Polling (nur im Nicht-Edit-Modus relevant) ────────────
|
||||||
|
|
||||||
def _poll(self):
|
def _poll(self):
|
||||||
|
|
@ -224,9 +319,43 @@ class VersaPadViewer(tk.Tk):
|
||||||
text = SERIAL_STATUS_TEXT.get(error, error or "Fehler")
|
text = SERIAL_STATUS_TEXT.get(error, error or "Fehler")
|
||||||
self.sync_status.configure(text=text, fg=WARN_RED)
|
self.sync_status.configure(text=text, fg=WARN_RED)
|
||||||
|
|
||||||
def _on_close(self):
|
# ── Tray-Icon (kein Taskleisten-Eintrag beim Minimieren, wie VersaGUI) ──
|
||||||
|
|
||||||
|
def _on_unmap(self, event):
|
||||||
|
"""Minimieren faengt Windows normalerweise als Taskleisten-Icon ab --
|
||||||
|
wir wollen stattdessen: Fenster komplett weg, nur noch Tray-Icon."""
|
||||||
|
if event.widget is self and self.state() == "iconic":
|
||||||
|
self._hide_to_tray()
|
||||||
|
|
||||||
|
def _hide_to_tray(self):
|
||||||
|
self.withdraw()
|
||||||
|
|
||||||
|
def _show_from_tray(self):
|
||||||
|
self.deiconify()
|
||||||
|
self.state("normal")
|
||||||
|
self.lift()
|
||||||
|
self.focus_force()
|
||||||
|
|
||||||
|
def _drain_tray_queue(self):
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
action = self._tray_queue.get_nowait()
|
||||||
|
if action == "show":
|
||||||
|
self._show_from_tray()
|
||||||
|
elif action == "quit":
|
||||||
|
self._quit()
|
||||||
|
except queue.Empty:
|
||||||
|
pass
|
||||||
|
if not self._closing:
|
||||||
|
self.after(200, self._drain_tray_queue)
|
||||||
|
|
||||||
|
def _quit(self):
|
||||||
self._closing = True
|
self._closing = True
|
||||||
self._link.close()
|
self._link.close()
|
||||||
|
try:
|
||||||
|
self._tray_icon.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
# ── Programmiermodus ──────────────────────────────────────────
|
# ── Programmiermodus ──────────────────────────────────────────
|
||||||
|
|
|
||||||
BIN
icon.ico
Normal file
BIN
icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
icon.png
Normal file
BIN
icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Loading…
Add table
Add a link
Reference in a new issue