rs2322tcp/internal/gui/app.go
2026-08-11 10:59:46 +02:00

245 lines
6.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* ============================================================================
* Projekt.....: rs2322tcp
* Datei.......: internal/gui/app.go
* Copyright (C) 2026 Dieter Lang
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Beschreibung:
* Grundgerüst der grafischen Benutzeroberfläche des rs2322tcp-Clients.
*
* Die GUI verwendet Fyne und stellt die Benutzeroberfläche für den
* technischen rs2322tcp-Client bereit.
*
* Die eigentliche Kommunikation mit dem Server sowie die Verwaltung der
* virtuellen seriellen Ports bleiben vollständig in internal/client.
*
* Der technische Client wird im Hintergrund gestartet, damit das Fyne-
* Fenster unmittelbar angezeigt werden kann. Dadurch bleibt die GUI auch
* dann bedienbar, wenn der Server nicht erreichbar ist oder der Aufbau
* der Verbindung längere Zeit benötigt.
*
* Aktualisierungen von Fyne-Widgets aus einer Hintergrund-Goroutine werden
* über fyne.Do() auf den Fyne-GUI-Thread übertragen.
*
* Die GUI stellt außerdem die Funktion "Server neu verbinden" bereit.
* Dabei wird die bestehende technische Client-Anwendung beendet und
* anschließend vollständig neu gestartet. Dadurch wird die aktuelle
* client.json erneut geladen.
* ============================================================================
*/
package gui
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"git.lang-dieter.de/rs2322tcp/internal/client"
)
// App represents the graphical rs2322tcp client application.
//
// App owns the Fyne window and the technical client application. The GUI
// does not access the Runtime or the Client directly. All technical client
// operations are performed through client.Application.
type App struct {
fyneApp fyne.App
window fyne.Window
clientApplication *client.Application
statusLabel *widget.Label
errorLabel *widget.Label
reconnectButton *widget.Button
}
// NewApp creates the graphical rs2322tcp client application.
//
// No server connection is established by NewApp. The GUI can therefore be
// displayed immediately after this function returns.
func NewApp(
fyneApp fyne.App,
configFile string,
) (*App, error) {
if fyneApp == nil {
return nil, fmt.Errorf("fyne application is nil")
}
application, err := client.NewApplication(configFile)
if err != nil {
return nil, err
}
window := fyneApp.NewWindow("rs2322tcp Client")
window.Resize(fyne.NewSize(600, 400))
app := &App{
fyneApp: fyneApp,
window: window,
clientApplication: application,
statusLabel: widget.NewLabel("Server: nicht verbunden"),
errorLabel: widget.NewLabel(""),
}
app.reconnectButton = widget.NewButton(
"Server neu verbinden",
app.reconnect,
)
app.buildContent()
return app, nil
}
// buildContent creates the initial main-window content.
//
// The layout is deliberately simple. More detailed status information and
// the device-assignment dialog will be added in later development steps.
func (a *App) buildContent() {
title := widget.NewLabel("rs2322tcp Client")
content := container.NewVBox(
title,
a.statusLabel,
a.errorLabel,
a.reconnectButton,
)
a.window.SetContent(content)
}
// Start starts the technical client application in the background.
//
// The Fyne window is deliberately not blocked by the network connection.
// The status shown by the GUI is updated through fyne.Do(), because the
// technical client runs outside the Fyne GUI thread.
//
// While the initial connection is being established, the reconnect button
// is disabled. This prevents multiple concurrent connection attempts.
func (a *App) Start() {
if a == nil {
return
}
a.statusLabel.SetText("Server: Verbindung wird aufgebaut ...")
a.errorLabel.SetText("")
a.reconnectButton.Disable()
go func() {
err := a.clientApplication.Start()
if err != nil {
fyne.Do(func() {
a.statusLabel.SetText("Server: nicht verbunden")
a.errorLabel.SetText(
fmt.Sprintf("Fehler: %v", err),
)
a.reconnectButton.Enable()
})
return
}
devices := a.clientApplication.Devices()
fyne.Do(func() {
a.statusLabel.SetText(
fmt.Sprintf(
"Server: verbunden %d Geräte verfügbar",
len(devices),
),
)
a.errorLabel.SetText("")
a.reconnectButton.Enable()
})
}()
}
// reconnect starts a complete reconnect of the technical client.
//
// The reconnect operation is deliberately handled by
// client.Application.Reconnect(). The GUI therefore does not need to know
// how the technical client closes connections, reloads the configuration,
// connects to the server or starts the Runtime.
//
// While reconnecting, the button is disabled so that only one reconnect
// operation can be active at a time.
func (a *App) reconnect() {
if a == nil {
return
}
a.statusLabel.SetText("Server: Verbindung wird neu aufgebaut ...")
a.errorLabel.SetText("")
a.reconnectButton.Disable()
go func() {
err := a.clientApplication.Reconnect()
if err != nil {
fyne.Do(func() {
a.statusLabel.SetText("Server: nicht verbunden")
a.errorLabel.SetText(
fmt.Sprintf("Fehler: %v", err),
)
a.reconnectButton.Enable()
})
return
}
devices := a.clientApplication.Devices()
fyne.Do(func() {
a.statusLabel.SetText(
fmt.Sprintf(
"Server: verbunden %d Geräte verfügbar",
len(devices),
),
)
a.errorLabel.SetText("")
a.reconnectButton.Enable()
})
}()
}
// Close closes the technical client application.
//
// The Runtime and the server connection are closed before the GUI window
// itself is closed.
func (a *App) Close() {
if a == nil {
return
}
_ = a.clientApplication.Close()
}
// ShowAndRun displays the main window and starts the Fyne event loop.
//
// This method blocks until the GUI application terminates.
func (a *App) ShowAndRun() {
if a == nil {
return
}
a.window.ShowAndRun()
}
// Window returns the main application window.
//
// The method is provided for the program entry point and for later GUI
// initialization that needs access to the window.
func (a *App) Window() fyne.Window {
if a == nil {
return nil
}
return a.window
}