From a74841b048016e59a09b024c161e720ff61bec52 Mon Sep 17 00:00:00 2001 From: Roman Boykov Date: Sun, 22 Mar 2026 14:27:14 +0300 Subject: [PATCH] Redo main cycle --- appWindow.go | 69 +++++++--- config/config.go | 45 +++++- debug/breakpoint/breakpoint.go | 4 +- debug/breakpoint/breakpoint_test.go | 3 +- debug/listener/listener.go | 10 ++ main.go | 205 ++++++++++++++++++---------- nanotime/nanotime.go | 18 +++ okean240/computer.go | 66 +++++++-- okean240/fdc/fdc.go | 36 ++--- okemu.yml | 26 ++-- settings.go | 91 ++++++++++++ 11 files changed, 434 insertions(+), 139 deletions(-) create mode 100644 nanotime/nanotime.go create mode 100644 settings.go diff --git a/appWindow.go b/appWindow.go index 63ff6dd..a38a7f7 100644 --- a/appWindow.go +++ b/appWindow.go @@ -3,6 +3,7 @@ package main import ( "fmt" "image/color" + "okemu/config" "okemu/okean240" "okemu/okean240/fdc" @@ -17,7 +18,7 @@ import ( "fyne.io/fyne/v2/widget" ) -func mainWindow(computer *okean240.ComputerType) (*fyne.Window, *canvas.Raster, *widget.Label) { +func mainWindow(computer *okean240.ComputerType, config *config.OkEmuConfig) (*fyne.Window, *canvas.Raster, *widget.Label) { emulatorApp := app.New() w := emulatorApp.NewWindow("Океан 240.2") w.Canvas().SetOnTypedKey( @@ -52,7 +53,7 @@ func mainWindow(computer *okean240.ComputerType) (*fyne.Window, *canvas.Raster, w.Resize(fyne.NewSize(600, 600)) vBox := container.NewVBox( - newToolbar(computer, w), + newToolbar(computer, w, emulatorApp, config), centerRaster, label, ) @@ -62,45 +63,75 @@ func mainWindow(computer *okean240.ComputerType) (*fyne.Window, *canvas.Raster, return &w, raster, label } -func newToolbar(c *okean240.ComputerType, w fyne.Window) *fyne.Container { +func newToolbar(c *okean240.ComputerType, w fyne.Window, a fyne.App, config *config.OkEmuConfig) *fyne.Container { hBox := container.NewHBox() for d := 0; d < fdc.TotalDrives; d++ { hBox.Add(widget.NewLabel(string(rune(66+d)) + ":")) hBox.Add(widget.NewToolbar( widget.NewToolbarAction(theme.DocumentSaveIcon(), func() { - err := c.SaveFloppy(fdc.FloppyB) + err := c.SaveFloppy(byte(d)) if err != nil { dialog.ShowError(err, w) } }), //widget.NewToolbarSpacer(), widget.NewToolbarAction(theme.FolderOpenIcon(), func() { - err := c.SaveFloppy(fdc.FloppyC) + err := c.LoadFloppy(byte(d)) if err != nil { dialog.ShowError(err, w) } }), )) + hBox.Add(widget.NewSeparator()) } + + hBox.Add(widget.NewButtonWithIcon("1", theme.DownloadIcon(), func() { + c.SetRamBytes(ramBytes1) + })) hBox.Add(widget.NewSeparator()) - hBox.Add(widget.NewButtonWithIcon("Ctrl+C", theme.LogoutIcon(), func() { + hBox.Add(widget.NewButtonWithIcon("2", theme.DownloadIcon(), func() { + c.SetRamBytes(ramBytes2) + })) + hBox.Add(widget.NewSeparator()) + hBox.Add(widget.NewButtonWithIcon("^C", theme.MediaStopIcon(), func() { c.PutCtrlKey(0x03) })) hBox.Add(widget.NewSeparator()) - bNorm := widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() { - fullSpeed.Store(false) - c.SetCPUFrequency(2_500_000) - //bNorm.Disable() + cbFreq := widget.NewCheck("Fmax", func(checked bool) { + fullSpeed.Store(checked) + if checked { + c.SetCPUFrequency(25_000_000) + } else { + c.SetCPUFrequency(2_500_000) + } + }) + //bNorm := widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() { + // fullSpeed.Store(false) + // c.SetCPUFrequency(2_500_000) + // //bNorm.Disable() + // + //}) + //bFast := widget.NewButtonWithIcon("", theme.MediaFastForwardIcon(), func() { + // fullSpeed.Store(true) + // c.SetCPUFrequency(50_000_000) + // bNorm.Enable() + // //bFast.Disable() + //}) - }) - bFast := widget.NewButtonWithIcon("", theme.MediaFastForwardIcon(), func() { - fullSpeed.Store(true) - c.SetCPUFrequency(50_000_000) - bNorm.Enable() - //bFast.Disable() - }) - hBox.Add(bNorm) - hBox.Add(bFast) + hBox.Add(cbFreq) + //hBox.Add(bFast) + hBox.Add(widget.NewSeparator()) + hBox.Add(widget.NewButtonWithIcon("", theme.SettingsIcon(), func() { + cfg := config.Clone() + d := dialog.NewCustomConfirm("OK-Emu settings", "Save", "Cancel", settingsDialog(cfg), func(b bool) { + if b { + cfg.Save() + } + }, w) + d.Resize(fyne.NewSize(450, 360)) + //w.SetContent(settings.NewSettings().LoadAppearanceScreen(w)) + d.Show() + })) hBox.Add(layout.NewSpacer()) hBox.Add(widget.NewButtonWithIcon("Reset", theme.MediaReplayIcon(), func() { needReset = true diff --git a/config/config.go b/config/config.go index ce2bf5a..4c5d16c 100644 --- a/config/config.go +++ b/config/config.go @@ -10,21 +10,21 @@ import ( const defaultMonitorFile = "rom/MON_v5.bin" const defaultCPMFile = "rom/CPM_v5.bin" const DefaultDebufPort = 10000 +const confFile = "okemu.yml" type OkEmuConfig struct { LogFile string `yaml:"logFile"` LogLevel string `yaml:"logLevel"` MonitorFile string `yaml:"monitorFile"` CPMFile string `yaml:"cpmFile"` - FDC FDCConfig `yaml:"fdc"` + FDC []FDCConfig `yaml:"fdc"` Debugger DebuggerConfig `yaml:"debugger"` } type FDCConfig struct { - AutoLoadB bool `yaml:"autoLoadB"` - AutoLoadC bool `yaml:"autoLoadC"` - FloppyB string `yaml:"floppyB"` - FloppyC string `yaml:"floppyC"` + AutoLoad bool `yaml:"autoLoad"` + AutoSave bool `yaml:"autoSave"` + FloppyFile string `yaml:"floppyFile"` } type DebuggerConfig struct { @@ -54,7 +54,6 @@ func LoadConfig() { // usage() //} // confFile := args[2] - confFile := "okemu.yml" conf := OkEmuConfig{} data, err := os.ReadFile(confFile) @@ -96,3 +95,37 @@ func setDefaultConf(conf *OkEmuConfig) { conf.Debugger.Port = DefaultDebufPort } } + +func (c *OkEmuConfig) Clone() *OkEmuConfig { + + fds := make([]FDCConfig, 2) + for n, fd := range c.FDC { + fds[n].FloppyFile = fd.FloppyFile + fds[n].AutoLoad = fd.AutoLoad + fds[n].AutoSave = fd.AutoSave + } + + return &OkEmuConfig{ + LogFile: c.LogFile, + LogLevel: c.LogLevel, + MonitorFile: c.MonitorFile, + CPMFile: c.CPMFile, + FDC: fds, + Debugger: DebuggerConfig{ + Enabled: c.Debugger.Enabled, + Host: c.Debugger.Host, + Port: c.Debugger.Port, + }, + } +} + +func (c *OkEmuConfig) Save() { + data, err := yaml.Marshal(c) + if err != nil { + log.Errorf("config error: %v", err) + } + err = os.WriteFile(confFile, data, 0600) + if err != nil { + log.Errorf("save config file: %v", err) + } +} diff --git a/debug/breakpoint/breakpoint.go b/debug/breakpoint/breakpoint.go index e9d92ea..11b7562 100644 --- a/debug/breakpoint/breakpoint.go +++ b/debug/breakpoint/breakpoint.go @@ -34,7 +34,7 @@ var orMatch = regexp.MustCompile(`\s+OR\s+`) // var xorMatch = regexp.MustCompile(`\s+XOR\s+`) var hexHMatch = regexp.MustCompile(`[[:xdigit:]]+H`) -var eqMatch = regexp.MustCompile(`[^=><]=[^=]`) +var eqMatch = regexp.MustCompile(`[^=>", "!=") return ex } diff --git a/debug/breakpoint/breakpoint_test.go b/debug/breakpoint/breakpoint_test.go index 19d1081..32227bc 100644 --- a/debug/breakpoint/breakpoint_test.go +++ b/debug/breakpoint/breakpoint_test.go @@ -5,13 +5,14 @@ import ( "testing" ) -const expr1 = "PC=00100h && SP>=256" +const expr1 = "PC=00100h and (NOT(B > 10)) and a != 5" const expr2 = "PC=00115h" const expr3 = "SP>=1332" var ctx = map[string]interface{}{ "PC": 0x100, "A": 0x55, + "B": 5, "SP": 0x200, } diff --git a/debug/listener/listener.go b/debug/listener/listener.go index 2b72970..0c04994 100644 --- a/debug/listener/listener.go +++ b/debug/listener/listener.go @@ -241,6 +241,8 @@ func HandleCommand(str string, writer *bufio.Writer) bool { quit = true case "snapshot-save": writeResponseMessage(writer, snapshotSave(params)) + case "snapshot-load": + writeResponseMessage(writer, snapshotLoad(params)) case "set-breakpointaction": // now do nothing writeResponseMessage(writer, "") @@ -665,3 +667,11 @@ func snapshotSave(params string) string { } return "" } + +func snapshotLoad(params string) string { + e := computer.LoadSnapshot(strings.TrimSpace(params)) + if e != nil { + return fmt.Sprintf("Error load snapshot: %s", e) + } + return "" +} diff --git a/main.go b/main.go index 1ae364a..f5dd0ec 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,16 @@ package main import ( + "context" _ "embed" "fmt" + "math" "okemu/config" "okemu/debug" "okemu/debug/listener" "okemu/logger" + "okemu/nanotime" "okemu/okean240" - "okemu/okean240/fdc" "okemu/z80/dis" "sync/atomic" "time" @@ -16,27 +18,33 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/widget" + //log "github.com/sirupsen/logrus" ) var Version = "v1.0.0" var BuildTime = "2026-03-01" -//go:embed hex/m80.hex -var serialBytes []byte +////go:embed hex/m80.hex +//var serialBytes []byte -//go:embed bin/main.com +//go:embed bin/2048.com var ramBytes1 []byte -//go:embed bin/PLOT.BAS +//go:embed bin/JACK.COM var ramBytes2 []byte var needReset = false + var fullSpeed atomic.Bool func main() { fmt.Printf("Starting Ocean-240.2 emulator %s build at %s\n", Version, BuildTime) + // Create a context that can be cancelled + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // base log init logger.InitLogging() @@ -51,91 +59,148 @@ func main() { debugger := debug.NewDebugger() computer := okean240.NewComputer(conf, debugger) - computer.SetSerialBytes(serialBytes) + //computer.SetSerialBytes(serialBytes) - if conf.FDC.AutoLoadB { - err := computer.LoadFloppy(fdc.FloppyB) - if err != nil { - // show message - } - } - if conf.FDC.AutoLoadC { - err := computer.LoadFloppy(fdc.FloppyC) - if err != nil { - // show message - } - } + computer.AutoLoadFloppy() disasm := dis.NewDisassembler(computer) - w, raster, label := mainWindow(computer) + w, raster, label := mainWindow(computer, conf) - go emulator(computer) - go screen(computer, raster, label) + go emulator(ctx, computer) + // go timerClock(ctx, computer) + go screen(ctx, computer, raster, label) if conf.Debugger.Enabled { go listener.SetupTcpHandler(conf, debugger, disasm, computer) } (*w).ShowAndRun() + computer.AutoSaveFloppy() } -func screen(computer *okean240.ComputerType, raster *canvas.Raster, label *widget.Label) { +func screen(ctx context.Context, computer *okean240.ComputerType, raster *canvas.Raster, label *widget.Label) { ticker := time.NewTicker(20 * time.Millisecond) + defer ticker.Stop() + frame := 0 var pre uint64 = 0 - var freq uint64 = 0 + var preTim uint64 = 0 + var freq float64 = 0 + var freqTim float64 = 0 + timeStart := time.Now() - for range ticker.C { - frame++ - // redraw screen here - fyne.Do(func() { - // status for every 50 frames - if frame%50 == 0 { - freq = computer.Cycles() - pre - pre = computer.Cycles() - label.SetText(formatLabel(computer, freq)) - } - raster.Refresh() - }) - } -} + for { + select { + case <-ticker.C: + frame++ + // redraw screen here + fyne.Do(func() { + // status for every 50 frames + if frame%50 == 0 { + timeElapsed := time.Since(timeStart) + period := float64(timeElapsed.Nanoseconds()) / 1_000_000.0 -func formatLabel(computer *okean240.ComputerType, freq uint64) string { - return fmt.Sprintf("Screen size: %dx%d | F: %d | Debugger: %s", computer.ScreenWidth(), computer.ScreenHeight(), freq, computer.DebuggerState()) -} + freq = math.Round(float64(computer.Cycles()-pre)/period) / 1000.0 + freqTim = math.Round(float64(timerTicks.Load()-preTim)/period) / 1000.0 + label.SetText(formatLabel(computer, freq, freqTim)) -const ticksPerTact uint64 = 4 + // adjust cpu clock + if freq > 2.55 && cpuClkPeriod.Load() < defaultCpuClkPeriod+40 { + cpuClkPeriod.Add(1) + } else if freq < 2.45 && cpuClkPeriod.Load() > defaultCpuClkPeriod-40 { + cpuClkPeriod.Add(-1) + } + // adjust timer clock + if freqTim > 1.53 && timerClkPeriod.Load() < defaultTimerClkPeriod+20 { + timerClkPeriod.Add(1) + } else if freqTim < 1.47 && timerClkPeriod.Load() > defaultTimerClkPeriod-20 { + timerClkPeriod.Add(-1) + } -func emulator(computer *okean240.ComputerType) { - ticker := time.NewTicker(66 * time.Nanosecond) - var ticks uint64 = 0 - var nextClock = ticks + ticksPerTact - //var ticksCPU = 3 - for range ticker.C { - ticks++ - if ticks%10 == 0 { - // 1.5 MHz - computer.TimerClk() - } - var bp uint16 = 0 - var typ byte = 0 - if fullSpeed.Load() { - _, bp, typ = computer.Do() - } else { - if ticks >= nextClock { - var t uint32 - t, bp, typ = computer.Do() - nextClock = ticks + uint64(t)*ticksPerTact - } - } - // Breakpoint hit - if bp > 0 || typ != 0 { - listener.BreakpointHit(bp, typ) - } - if needReset { - computer.Reset() - needReset = false + //log.Debugf("Cpu clk period: %d, Timer clock period: %d, period: %1.3f", cpuClkPeriod.Load(), timerClkPeriod.Load(), period) + pre = computer.Cycles() + preTim = timerTicks.Load() + timeStart = time.Now() + } + raster.Refresh() + }) + case <-ctx.Done(): + return } } } + +func formatLabel(computer *okean240.ComputerType, freq float64, freqTim float64) string { + return fmt.Sprintf("Screen size: %dx%d | Fcpu: %1.2fMHz | Ftmr: %1.2fMHz | Debugger: %s", computer.ScreenWidth(), computer.ScreenHeight(), freq, freqTim, computer.DebuggerState()) +} + +var timerTicks atomic.Uint64 + +const defaultTimerClkPeriod = 564 // = 1_000_000_000 / 1_607_900 // period in nanos for 1.5MHz frequency +const defaultCpuClkPeriod = 221 // = 1_000_000_000 / 2_770_000 // period in nanos for 2.5MHz frequency + +var timerClkPeriod atomic.Int64 // = 1_000_000_000 / 1_607_900 // period in nanos for 1.5MHz frequency +var cpuClkPeriod atomic.Int64 // = 1_000_000_000 / 2_770_000 // period in nanos for 2.5MHz frequency + +func emulator(ctx context.Context, computer *okean240.ComputerType) { + ticker := time.NewTicker(133 * time.Nanosecond) + defer ticker.Stop() + + cpuClkPeriod.Store(defaultCpuClkPeriod) + timerClkPeriod.Store(defaultTimerClkPeriod) + + cpuTicks := uint64(0) + nextTick := uint64(0) + + cpuTStart := nanotime.Now() + tmrTStart := cpuTStart + + var bp uint16 + var bpType byte + + for { + select { + case <-ticker.C: + tmrElapsed := nanotime.Since(tmrTStart) + // TIMER CLK + if tmrElapsed.Nanoseconds() >= timerClkPeriod.Load() { + computer.TimerClk() + timerTicks.Add(1) + tmrTStart = nanotime.Now() + } + + // CPU + cpuElapsed := nanotime.Since(cpuTStart) + if cpuElapsed.Nanoseconds() >= cpuClkPeriod.Load() { + cpuTicks++ + bp = 0 + bpType = 0 + + if fullSpeed.Load() { + // Max frequency + _, bp, bpType = computer.Do() + } else { + // 2.5MHz frequency + if cpuTicks >= nextTick { + var t uint32 + t, bp, bpType = computer.Do() + nextTick += uint64(t) + } + } + // Breakpoint hit + if bp > 0 || bpType != 0 { + listener.BreakpointHit(bp, bpType) + } + if needReset { + computer.Reset() + needReset = false + } + cpuTStart = nanotime.Now() + } + case <-ctx.Done(): + return + } + } + +} diff --git a/nanotime/nanotime.go b/nanotime/nanotime.go new file mode 100644 index 0000000..ce0de8a --- /dev/null +++ b/nanotime/nanotime.go @@ -0,0 +1,18 @@ +package nanotime + +import ( + "time" + _ "unsafe" // required to use //go:linkname +) + +//go:noescape +//go:linkname nanotime runtime.nanotime +func nanotime() int64 + +func Now() uint64 { + return uint64(nanotime()) +} + +func Since(t uint64) time.Duration { + return time.Duration(Now() - t) +} diff --git a/okean240/computer.go b/okean240/computer.go index 47d4a13..b605e69 100644 --- a/okean240/computer.go +++ b/okean240/computer.go @@ -14,6 +14,7 @@ import ( "okemu/okean240/usart" "okemu/z80" "os" + "strconv" //"okemu/z80" "okemu/z80/c99" @@ -48,6 +49,7 @@ type ComputerType struct { cpuFrequency uint32 // debugger *debug.Debugger + config *config.OkEmuConfig } type Snapshot struct { @@ -98,6 +100,7 @@ func (c *ComputerType) MemWrite(addr uint16, val byte) { // NewComputer Builds new computer func NewComputer(cfg *config.OkEmuConfig, deb *debug.Debugger) *ComputerType { c := ComputerType{} + c.config = cfg c.memory = Memory{} c.memory.Init(cfg.MonitorFile, cfg.CPMFile) @@ -379,17 +382,6 @@ func (c *ComputerType) memoryAsHexStr() string { } func (c *ComputerType) SaveSnapshot(fn string) error { - // create snapshot file - file, err := os.Create(fn) - if err != nil { - return err - } - defer func() { - err := file.Close() - if err != nil { - log.Error(err) - } - }() // take snapshot s := Snapshot{ CPU: c.cpu.GetState(), @@ -401,9 +393,59 @@ func (c *ComputerType) SaveSnapshot(fn string) error { return err } // and save - err = binary.Write(file, binary.LittleEndian, b) + err = os.WriteFile(fn, b, 0644) if err != nil { return err } return nil } + +func (c *ComputerType) LoadSnapshot(fn string) error { + // read snapshot file + b, err := os.ReadFile(fn) + if err != nil { + return err + } + // unmarshal from JSON + var result Snapshot + err = json.Unmarshal(b, &result) + if err != nil { + return err + } + c.cpu.SetState(result.CPU) + return c.restoreMemoryFromHex(result.Memory) +} + +func (c *ComputerType) restoreMemoryFromHex(memory string) error { + for addr := 0; addr <= 65535; addr++ { + b, e := strconv.ParseUint(memory[addr*2:addr*2+2], 16, 8) + if e != nil { + log.Error(e) + return e + } + c.memory.MemWrite(uint16(addr), byte(b)) + } + return nil +} + +func (c *ComputerType) AutoSaveFloppy() { + for drv := byte(0); drv < fdc.TotalDrives; drv++ { + if c.config.FDC[drv].AutoSave { + e := c.fdc.SaveFloppy(drv) + if e != nil { + log.Error(e) + } + } + } +} + +func (c *ComputerType) AutoLoadFloppy() { + for drv := byte(0); drv < fdc.TotalDrives; drv++ { + if c.config.FDC[drv].AutoLoad { + e := c.fdc.LoadFloppy(drv) + if e != nil { + log.Error(e) + } + } + } +} diff --git a/okean240/fdc/fdc.go b/okean240/fdc/fdc.go index 18e3325..a0cc9e0 100644 --- a/okean240/fdc/fdc.go +++ b/okean240/fdc/fdc.go @@ -88,8 +88,8 @@ type FloppyDriveController struct { //curSector *SectorType bytePtr uint16 trackBuffer []byte - floppyFile []string - config *config.OkEmuConfig + // floppyFile []string + config *config.OkEmuConfig } type FloppyDriveControllerInterface interface { @@ -332,14 +332,14 @@ func (f *FloppyDriveController) Drq() byte { func (f *FloppyDriveController) LoadFloppy(drive byte) error { if drive < TotalDrives { - return loadFloppy(&f.sectors[drive], f.floppyFile[drive]) + return loadFloppy(&f.sectors[drive], f.config.FDC[drive].FloppyFile) } return errors.New("DriveNo " + strconv.Itoa(int(drive)) + " out of range") } func (f *FloppyDriveController) SaveFloppy(drive byte) error { if drive < TotalDrives { - return saveFloppy(&f.sectors[drive], f.floppyFile[drive]) + return saveFloppy(&f.sectors[drive], f.config.FDC[drive].FloppyFile) } return errors.New("DriveNo " + strconv.Itoa(int(drive)) + " out of range") } @@ -353,20 +353,22 @@ func NewFDC(conf *config.OkEmuConfig) *FloppyDriveController { sec[d][i] = bytes.Repeat([]byte{0xe5}, SectorSize) } } + return &FloppyDriveController{ - sideNo: 0, - ddEn: 0, - init: 0, - drive: 0, - mot1: 0, - mot0: 0, - intRq: 0, - motSt: 0, - drq: 0, - lastCmd: 0xff, - sectors: sec, - bytePtr: 0xffff, - floppyFile: []string{conf.FDC.FloppyB, conf.FDC.FloppyC}, + sideNo: 0, + ddEn: 0, + init: 0, + drive: 0, + mot1: 0, + mot0: 0, + intRq: 0, + motSt: 0, + drq: 0, + lastCmd: 0xff, + sectors: sec, + bytePtr: 0xffff, + //floppyConf: conf.FDC, + config: conf, } } diff --git a/okemu.yml b/okemu.yml index 5487ce8..c536925 100644 --- a/okemu.yml +++ b/okemu.yml @@ -1,15 +1,15 @@ -logFile: "okemu.log" -logLevel: "info" - -monitorFile: "rom/MON_r8_9c6c6546.bin" -cpmFile: "rom/CPM_r8_bc0695e4.bin" - +logFile: okemu.log +logLevel: info +monitorFile: rom/MON_r8_9c6c6546.bin +cpmFile: rom/CPM_r8_bc0695e4.bin fdc: - autoLoadB: true - floppyB: "floppy/floppyB.okd" - autoLoadC: true - floppyC: "floppy/floppyC.okd" - + - autoLoad: true + autoSave: true + floppyFile: floppy/floppyB.okd + - autoLoad: true + autoSave: true + floppyFile: floppy/floppyC.okd debugger: - enabled: true - port: 10001 + enabled: true + host: localhost + port: 10001 diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..e172060 --- /dev/null +++ b/settings.go @@ -0,0 +1,91 @@ +package main + +import ( + "okemu/config" + "okemu/okean240/fdc" + "strconv" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/data/validation" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + log "github.com/sirupsen/logrus" +) + +func settingsDialog(config *config.OkEmuConfig) fyne.CanvasObject { + + debugger := widget.NewForm( + widget.NewFormItem("Remote debug", debugCheckBox(config)), + widget.NewFormItem("Host", hostEntry(config)), + widget.NewFormItem("Port", portEntry(config))) + + cont := container.NewAppTabs( + container.NewTabItemWithIcon("Debug", theme.MediaPlayIcon(), widget.NewCard("", "", debugger)), + ) + for drv := byte(0); drv < fdc.TotalDrives; drv++ { + cont.Append( + container.NewTabItemWithIcon("Drive "+string(rune(66+drv))+":", theme.DocumentSaveIcon(), widget.NewCard("Floppy 720k", "", diskForm(config, drv))), + ) + } + return cont +} + +func portEntry(cfg *config.OkEmuConfig) *widget.Entry { + dbgPort := widget.NewEntry() + dbgPort.SetText(strconv.Itoa(cfg.Debugger.Port)) + dbgPort.Validator = validation.NewRegexp(`^[0-9]+$`, "port can only contain numbers") + dbgPort.OnSubmitted = func(s string) { + p, e := strconv.Atoi(s) + if e != nil { + log.Warn("Illegal port number: " + s) + } else { + cfg.Debugger.Port = p + } + } + return dbgPort +} + +func hostEntry(cfg *config.OkEmuConfig) *widget.Entry { + entry := widget.NewEntry() + entry.SetText(cfg.Debugger.Host) + entry.Validator = validation.NewRegexp(`^[A-Za-z0-9_-]+$`, "hostname can only contain letters, numbers, '_', and '-'") + entry.OnSubmitted = func(s string) { + cfg.Debugger.Host = s + } + return entry +} + +func debugCheckBox(cfg *config.OkEmuConfig) *widget.Check { + // Debug Enabled + check := widget.NewCheck("Enable", func(checked bool) { + cfg.Debugger.Enabled = checked + }) + check.Checked = cfg.Debugger.Enabled + return check +} + +func diskForm(cfg *config.OkEmuConfig, drv byte) *widget.Form { + dskAutoLoad := widget.NewCheck("AutoLoad", func(checked bool) { + cfg.FDC[drv].AutoLoad = checked + }) + dskAutoLoad.Checked = cfg.FDC[drv].AutoLoad + + dskAutoSave := widget.NewCheck("AutoSave", func(checked bool) { + cfg.FDC[drv].AutoSave = checked + }) + + dskAutoSave.Checked = cfg.FDC[drv].AutoSave + + dskFileName := widget.NewEntry() + dskFileName.SetText(cfg.FDC[drv].FloppyFile) + dskFileName.OnSubmitted = func(s string) { + cfg.FDC[drv].FloppyFile = s + } + + return widget.NewForm( + widget.NewFormItem("AutoLoad", dskAutoLoad), + widget.NewFormItem("AutoSave", dskAutoSave), + widget.NewFormItem("File", dskFileName), + ) +}