mirror of
https://github.com/ivanizag/izapple2.git
synced 2026-10-22 00:02:27 +00:00
Share the frontend code that does not depend on the toolkit
The frontends were copies of each other. The help was the same text three times, the chain that picks the screen to show was the same branches three times, and the SDL2 and SDL3 keyboards, joysticks and mice were the same files twice with the constants of each toolkit. frontend/shared holds them now: the view with what is on screen and the help, the keyboard with the codes of the Apple II keyboard and the commands of the emulator, the paddles with the four axis and the three buttons, the mouse, and the mixer with every sound source of the machine already attached. The frontends keep what belongs to their toolkit: the translation of their keys to shared.Key, the clipboard, the instance ids of the SDL3 joysticks and the file system Ebitengine hands the dropped files in. The drop targets take the drag over the window too, so a frontend only tells which drive the pointer is on. One implementation aligns what the frontends did differently, following a2sdl where they disagreed: - Ctrl with a letter was broken on a2ebiten and a2wasm. The key was already zero based and 97 was subtracted from it again, so Ctrl-A sent 160 and not 1. - The libretro core gets Ctrl-Left, the Delete of the Base64A. - a2sdl3 marks the drive under the pointer when the areas are shown with F8. - a2sdl finds the apple keys by scancode, so they work on the layouts where the alt keys produce another symbol, and releases them when the window loses the focus. Both come from a2sdl3. a2ebiten keeps Ctrl-F5 for the readout of the speed it draws on the corner of the screen, the only key that does something else anywhere. The mouse leaves the pointer where it was on a window without size instead of dividing by zero, which SDL3 can report when asking for the size fails. What needs no machine is tested: the codes of the keyboard, the paddles, the mouse and the drive the drop targets mark.
This commit is contained in:
1 parent
66b5cac559
commit
88e59ae78d
35 files changed
+1290
-978
No files matched your search
@@ -97,7 +97,7 @@ The project includes E2E boot tests (`e2e_boot_test.go`, `e2e_woz_test.go`) that
|
||||
- Main emulator code is in the root package `izapple2`
|
||||
- Subpackages include: `storage`, `screen`, `fujinet`, `component`
|
||||
- Frontend implementations are in `frontend/` directory
|
||||
- `frontend/shared` has the code more than one frontend needs and the emulator library has no place for, like the drop targets screen
|
||||
- `frontend/shared` has the code more than one frontend needs and the emulator library has no place for, like the screen each frontend shows and the drop targets
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
|
||||
@@ -105,6 +105,11 @@ Escape mapped to what the Apple II understands. The rest:
|
||||
|
||||
On the Base64A, F3 is the Delete key of that keyboard.
|
||||
|
||||
The Apple keys are a place on the keyboard and are found by scancode, so they
|
||||
work on the layouts where the alt keys produce another symbol. Losing the
|
||||
window focus releases them, so one held while switching windows does not stay
|
||||
pressed.
|
||||
|
||||
The title bar says which machine is running and shows `PAUSED!` while it is
|
||||
paused. With the four panels or the character map on screen it shows what is
|
||||
being displayed instead.
|
||||
|
||||
@@ -44,11 +44,8 @@ The same command line as every other frontend, see
|
||||
|
||||
The same as [a2sdl](frontend_sdl2.md#keys), including the paste with
|
||||
Shift-Insert or Cmd-V and the Open-Apple and Closed-Apple on the alt or option
|
||||
keys.
|
||||
|
||||
The Apple keys are found by scancode instead of by keycode. On the keyboard
|
||||
layouts where the right alt is AltGr, SDL3 does not report it as an alt key,
|
||||
and the Closed-Apple would never be pressed if it were looked up by keycode.
|
||||
keys. Both frontends decide the keys with the same code, so they answer to the
|
||||
same ones.
|
||||
|
||||
## What is different from SDL2
|
||||
|
||||
@@ -58,8 +55,6 @@ Nothing that you use, and a few things underneath:
|
||||
areas of the drives it can be dropped on is shown while it moves, with the
|
||||
one under the pointer marked. SDL2 only knows about the file once it is
|
||||
dropped, and has to show the areas with F8 or after the drop.
|
||||
- Losing the window focus releases the joystick keys, so an Open-Apple held
|
||||
while switching windows does not stay pressed.
|
||||
- Ctrl-C on the terminal quits the same way as closing the window, releasing
|
||||
everything on the way out.
|
||||
- The scaling of the picture is a property of the renderer instead of the
|
||||
|
||||
@@ -6,7 +6,9 @@ import (
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/audio"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
a2audio "github.com/ivanizag/izapple2/audio"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
)
|
||||
|
||||
// ebitenAudio sends the mixed audio of the machine to the ebiten audio
|
||||
@@ -19,9 +21,9 @@ type ebitenAudio struct {
|
||||
samples []float32
|
||||
}
|
||||
|
||||
func newEbitenAudio(clockMhz float64) *ebitenAudio {
|
||||
func newEbitenAudio(a *izapple2.Apple2) *ebitenAudio {
|
||||
return &ebitenAudio{
|
||||
mixer: a2audio.NewMixer(clockMhz),
|
||||
mixer: shared.NewMixer(a),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
@@ -106,22 +105,6 @@ func (d *ebitenDropTargets) dropped() int {
|
||||
return drive
|
||||
}
|
||||
|
||||
// showing returns whether the areas take over the screen, either because the
|
||||
// user asked for them with F8 or because a file has just been dropped
|
||||
func (d *ebitenDropTargets) showing(requested bool) bool {
|
||||
return requested || d.targets.Flashing()
|
||||
}
|
||||
|
||||
// snapshot returns the screen with the areas, highlighting the drive that got
|
||||
// the last file while the flash lasts, or the one under the pointer
|
||||
func (d *ebitenDropTargets) snapshot() *image.RGBA {
|
||||
selected := d.targets.FlashDrive()
|
||||
if !d.targets.Flashing() {
|
||||
selected = d.pointedDrive()
|
||||
}
|
||||
return d.targets.Snapshot(selected)
|
||||
}
|
||||
|
||||
// pointedDrive returns the drive the mouse pointer is on. The positions are on
|
||||
// the virtual screen the game is laid out on, not on the window.
|
||||
func (d *ebitenDropTargets) pointedDrive() int {
|
||||
|
||||
@@ -4,33 +4,25 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
)
|
||||
|
||||
// ebitenKeyboard turns the key events of Ebitengine into the keys the
|
||||
// frontends share
|
||||
type ebitenKeyboard struct {
|
||||
a *izapple2.Apple2
|
||||
keyChannel *izapple2.KeyboardChannel
|
||||
keyboard *shared.Keyboard
|
||||
|
||||
showHelp bool
|
||||
showPages bool
|
||||
showCharGen bool
|
||||
showAltText bool
|
||||
showFreq bool
|
||||
showDropTargets bool
|
||||
screenMode int
|
||||
showFreq bool // The speed on the HUD, only this frontend has one
|
||||
|
||||
debug bool
|
||||
}
|
||||
|
||||
func newEbitenKeyBoard(a *izapple2.Apple2) *ebitenKeyboard {
|
||||
func newEbitenKeyBoard(a *izapple2.Apple2, view *shared.View) *ebitenKeyboard {
|
||||
var k ebitenKeyboard
|
||||
k.a = a
|
||||
k.keyChannel = izapple2.NewKeyboardChannel(a)
|
||||
|
||||
k.screenMode = screen.ScreenModeColorScanlines
|
||||
k.keyboard = shared.NewKeyboard(a, view)
|
||||
return &k
|
||||
}
|
||||
|
||||
@@ -54,123 +46,76 @@ func (k *ebitenKeyboard) update() {
|
||||
}
|
||||
|
||||
func (k *ebitenKeyboard) putText(text string) {
|
||||
k.keyChannel.PutText(text)
|
||||
k.keyboard.PutText(text)
|
||||
}
|
||||
|
||||
func (k *ebitenKeyboard) putKey(key ebiten.Key) {
|
||||
/*
|
||||
See "Apple II reference manual", page 5
|
||||
|
||||
To get keys as understood by the Apple2 hardware run:
|
||||
10 A=PEEK(49152)
|
||||
20 PRINT A, A - 128
|
||||
30 GOTO 10
|
||||
*/
|
||||
|
||||
ctrl := ebiten.IsKeyPressed(ebiten.KeyControl)
|
||||
shift := ebiten.IsKeyPressed(ebiten.KeyShift)
|
||||
|
||||
if ctrl {
|
||||
if key >= ebiten.KeyA && key <= ebiten.KeyZ {
|
||||
fmt.Println("Control Key: ", key.String())
|
||||
k.keyChannel.PutChar(uint8(key-ebiten.KeyA) - 97 + 1)
|
||||
return
|
||||
}
|
||||
if ctrl && key >= ebiten.KeyA && key <= ebiten.KeyZ {
|
||||
k.keyboard.PutCtrlLetter('a' + rune(key-ebiten.KeyA))
|
||||
return
|
||||
}
|
||||
|
||||
result := uint8(0)
|
||||
if ctrl && key == ebiten.KeyF5 {
|
||||
// Only this frontend draws the speed, the others report it
|
||||
k.showFreq = !k.showFreq
|
||||
return
|
||||
}
|
||||
|
||||
k.keyboard.PutKey(ebitenKey(key), ctrl, shift)
|
||||
}
|
||||
|
||||
// ebitenKey translates a key of Ebitengine
|
||||
func ebitenKey(key ebiten.Key) shared.Key {
|
||||
switch key {
|
||||
case ebiten.KeyEscape:
|
||||
result = 27
|
||||
return shared.KeyEscape
|
||||
case ebiten.KeyBackspace:
|
||||
result = 8
|
||||
case ebiten.KeyEnter:
|
||||
result = 13
|
||||
case ebiten.KeyNumpadEnter:
|
||||
result = 13
|
||||
case ebiten.KeyLeft:
|
||||
if ctrl {
|
||||
result = 31 // Base64A
|
||||
} else {
|
||||
result = 8
|
||||
}
|
||||
case ebiten.KeyRight:
|
||||
result = 21
|
||||
|
||||
// Apple //e
|
||||
case ebiten.KeyUp:
|
||||
result = 11 // 31 in the Base64A
|
||||
case ebiten.KeyDown:
|
||||
result = 10
|
||||
return shared.KeyBackspace
|
||||
case ebiten.KeyEnter, ebiten.KeyNumpadEnter:
|
||||
return shared.KeyReturn
|
||||
case ebiten.KeyTab:
|
||||
result = 9
|
||||
return shared.KeyTab
|
||||
case ebiten.KeyDelete:
|
||||
result = 127 // 24 in the Base64A
|
||||
return shared.KeyDelete
|
||||
case ebiten.KeyLeft:
|
||||
return shared.KeyLeft
|
||||
case ebiten.KeyRight:
|
||||
return shared.KeyRight
|
||||
case ebiten.KeyUp:
|
||||
return shared.KeyUp
|
||||
case ebiten.KeyDown:
|
||||
return shared.KeyDown
|
||||
|
||||
// Base64A clone particularities
|
||||
case ebiten.KeyF3:
|
||||
result = 127 // Base64A
|
||||
|
||||
// Control of the emulator
|
||||
case ebiten.KeyF1:
|
||||
k.showHelp = !k.showHelp
|
||||
return shared.KeyF1
|
||||
case ebiten.KeyF2:
|
||||
// While the help is shown, F2 alone triggers a reset. This is handy
|
||||
// when the window manager (for example KDE) captures Ctrl-F2.
|
||||
if ctrl || k.showHelp {
|
||||
k.a.SendCommand(izapple2.CommandReset)
|
||||
k.showHelp = false
|
||||
}
|
||||
return shared.KeyF2
|
||||
case ebiten.KeyF3:
|
||||
return shared.KeyF3
|
||||
case ebiten.KeyF4:
|
||||
k.a.SendCommand(izapple2.CommandToggleCPUTrace)
|
||||
return shared.KeyF4
|
||||
case ebiten.KeyF5:
|
||||
if ctrl {
|
||||
k.showFreq = !k.showFreq
|
||||
} else {
|
||||
k.a.SendCommand(izapple2.CommandToggleSpeed)
|
||||
}
|
||||
return shared.KeyF5
|
||||
case ebiten.KeyF6:
|
||||
k.screenMode = screen.NextScreenMode(k.screenMode)
|
||||
return shared.KeyF6
|
||||
case ebiten.KeyF7:
|
||||
k.showPages = !k.showPages
|
||||
return shared.KeyF7
|
||||
case ebiten.KeyF8:
|
||||
k.showDropTargets = !k.showDropTargets
|
||||
if k.showDropTargets {
|
||||
// The help is shown on top of the drop targets, get it out of the way
|
||||
k.showHelp = false
|
||||
}
|
||||
return shared.KeyF8
|
||||
case ebiten.KeyF9:
|
||||
k.a.SendCommand(izapple2.CommandDumpDebugInfo)
|
||||
return shared.KeyF9
|
||||
case ebiten.KeyF10:
|
||||
if ctrl {
|
||||
k.showCharGen = !k.showCharGen
|
||||
} else if shift {
|
||||
k.showAltText = !k.showAltText
|
||||
} else {
|
||||
k.a.SendCommand(izapple2.CommandNextCharGenPage)
|
||||
}
|
||||
return shared.KeyF10
|
||||
case ebiten.KeyF12:
|
||||
fallthrough
|
||||
case ebiten.KeyPrintScreen:
|
||||
if ctrl {
|
||||
screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/")
|
||||
} else {
|
||||
err := screen.SaveSnapshot(k.a.GetVideoSource(), screen.ScreenModeColorScanlines, "snapshot.png")
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving snapshoot: %v.\n.", err)
|
||||
} else {
|
||||
fmt.Println("Saving snapshot 'snapshot.png'")
|
||||
}
|
||||
}
|
||||
return shared.KeyF12
|
||||
case ebiten.KeyPause:
|
||||
k.a.SendCommand(izapple2.CommandPauseUnpause)
|
||||
return shared.KeyPause
|
||||
case ebiten.KeyPrintScreen:
|
||||
return shared.KeyPrintScreen
|
||||
}
|
||||
|
||||
// Missing values 91 to 95. Usually control for [\]^_
|
||||
// On the Base64A it's control for \]./
|
||||
|
||||
if result != 0 {
|
||||
k.keyChannel.PutChar(result)
|
||||
}
|
||||
return shared.KeyNone
|
||||
}
|
||||
+10
-47
@@ -3,11 +3,10 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
a_screen "github.com/ivanizag/izapple2/screen"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/fonts"
|
||||
@@ -18,6 +17,7 @@ import (
|
||||
type Game struct {
|
||||
a *izapple2.Apple2
|
||||
image *ebiten.Image
|
||||
view *shared.View
|
||||
keyboard *ebitenKeyboard
|
||||
speaker *ebitenAudio
|
||||
dropTargets *ebitenDropTargets
|
||||
@@ -53,21 +53,10 @@ func (g *Game) Update() error {
|
||||
}
|
||||
|
||||
if g.updates%3 == 0 && !g.a.IsPaused() { // 20 times per second
|
||||
var img *image.RGBA
|
||||
vs := g.a.GetVideoSource()
|
||||
if g.keyboard.showHelp {
|
||||
img = a_screen.SnapshotMessageGenerator(vs, helpMessage, false /*is80Columns*/)
|
||||
} else if g.dropTargets.showing(g.keyboard.showDropTargets) {
|
||||
img = g.dropTargets.snapshot()
|
||||
} else if g.keyboard.showCharGen {
|
||||
cgPage, cgPages := g.a.GetCgPageInfo()
|
||||
img = a_screen.SnapshotCharacterGenerator(vs, g.keyboard.showAltText)
|
||||
ebiten.SetWindowTitle(fmt.Sprintf("%v character map, page %v/%v", g.a.Name, cgPage+1, cgPages))
|
||||
} else if g.keyboard.showPages {
|
||||
img = a_screen.SnapshotParts(vs, g.keyboard.screenMode)
|
||||
ebiten.SetWindowTitle(fmt.Sprintf("%v %v %vx%v", g.a.Name, a_screen.VideoModeName(vs), img.Rect.Dx()/2, img.Rect.Dy()/2))
|
||||
} else {
|
||||
img = a_screen.Snapshot(vs, g.keyboard.screenMode)
|
||||
img, viewTitle := g.view.Snapshot(g.a, g.dropTargets.targets,
|
||||
g.dropTargets.pointedDrive())
|
||||
if viewTitle != "" {
|
||||
ebiten.SetWindowTitle(viewTitle)
|
||||
}
|
||||
if img != nil {
|
||||
g.image = ebiten.NewImageFromImage(img)
|
||||
@@ -133,15 +122,14 @@ func ebitenRun(a *izapple2.Apple2) {
|
||||
title := "iz-" + a.Name + " (F1 for help)"
|
||||
ebiten.SetWindowTitle(title)
|
||||
|
||||
view := shared.NewView()
|
||||
game := &Game{
|
||||
a: a,
|
||||
keyboard: newEbitenKeyBoard(a),
|
||||
speaker: newEbitenAudio(a.GetClockMhz()),
|
||||
view: view,
|
||||
keyboard: newEbitenKeyBoard(a, view),
|
||||
speaker: newEbitenAudio(a),
|
||||
dropTargets: newEbitenDropTargets(a),
|
||||
}
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(game.speaker.mixer.NewSource())
|
||||
}
|
||||
|
||||
var err error
|
||||
game.fontSource, err = text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
|
||||
@@ -155,31 +143,6 @@ func ebitenRun(a *izapple2.Apple2) {
|
||||
}
|
||||
}
|
||||
|
||||
var helpMessage = `
|
||||
F1: Show/Hide help
|
||||
Ctrl-F2: Reset
|
||||
F1, F2: Reset
|
||||
F4: Show/Hide CPU trace
|
||||
F5: Fast/Normal speed
|
||||
Ctrl-F5: Show speed
|
||||
F6: Next screen mode
|
||||
F7: Show/Hide pages
|
||||
F8: Show/Hide drop targets
|
||||
F10: Next character set
|
||||
Ctrl-F10: Show/Hide character set
|
||||
Shift-F10: Show/Hide alternate text
|
||||
F12: Save screen snapshot
|
||||
Pause: Pause the emulation
|
||||
|
||||
Left alt or option key: Open-Apple
|
||||
Right alt or option key: Closed-Apple
|
||||
|
||||
Drop a file on a drive area to load it
|
||||
|
||||
Run izapple2 -h for more options
|
||||
https://github.com/ivanizag/izapple2
|
||||
`
|
||||
|
||||
/*
|
||||
To test the WebAssembly version, run:
|
||||
go run github.com/hajimehoshi/wasmserve@latest .
|
||||
|
||||
@@ -7,7 +7,9 @@ package main
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"github.com/ivanizag/izapple2"
|
||||
a2audio "github.com/ivanizag/izapple2/audio"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
)
|
||||
|
||||
const sampleRate = a2audio.SampleRate
|
||||
@@ -30,15 +32,15 @@ type audioOutput struct {
|
||||
stereo []C.int16_t
|
||||
}
|
||||
|
||||
func newAudioOutput(clockMhz float64) *audioOutput {
|
||||
samplesPerFrame := sampleRate * cyclesPerFrame / (clockMhz * 1_000_000)
|
||||
func newAudioOutput(a *izapple2.Apple2) *audioOutput {
|
||||
samplesPerFrame := sampleRate * cyclesPerFrame / (a.GetClockMhz() * 1_000_000)
|
||||
|
||||
// Room for a frame and a bit, the pending samples never add up to a full
|
||||
// extra sample
|
||||
size := int(samplesPerFrame) + 2
|
||||
|
||||
return &audioOutput{
|
||||
mixer: a2audio.NewMixer(clockMhz),
|
||||
mixer: shared.NewMixer(a),
|
||||
samplesPerFrame: samplesPerFrame,
|
||||
mono: make([]float32, size),
|
||||
stereo: make([]C.int16_t, 2*size),
|
||||
|
||||
@@ -75,13 +75,10 @@ func (c *core) build() bool {
|
||||
|
||||
c.a = a
|
||||
c.video = newVideoOutput(options.screenMode)
|
||||
c.audio = newAudioOutput(a.GetClockMhz())
|
||||
c.audio = newAudioOutput(a)
|
||||
c.keyboard = newKeyboard(a)
|
||||
c.joysticks = newJoysticks()
|
||||
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(c.audio.mixer.NewSource())
|
||||
}
|
||||
a.SetJoysticksProvider(c.joysticks)
|
||||
a.SetMouseProvider(newMouse())
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
)
|
||||
|
||||
// keyboard turns the key events of the frontend into the codes the Apple II
|
||||
@@ -70,8 +71,17 @@ func izapple2KeyboardEvent(down C.int, keycode C.uint, character C.uint32_t, key
|
||||
return
|
||||
}
|
||||
|
||||
if key, ok := specialKey(uint32(keycode), uint16(keyModifiers)); ok {
|
||||
theCore.keyboard.channel.PutChar(key)
|
||||
ctrl := keyModifiers&C.RETROKMOD_CTRL != 0
|
||||
|
||||
if ctrl && keycode >= C.RETROK_a && keycode <= C.RETROK_z {
|
||||
if char, ok := shared.CharForCtrlLetter('a' + rune(keycode-C.RETROK_a)); ok {
|
||||
theCore.keyboard.channel.PutChar(char)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if char, ok := shared.CharForKey(retroKey(uint32(keycode)), ctrl); ok {
|
||||
theCore.keyboard.channel.PutChar(char)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -80,32 +90,30 @@ func izapple2KeyboardEvent(down C.int, keycode C.uint, character C.uint32_t, key
|
||||
theCore.keyboard.channel.PutRune(rune(character))
|
||||
}
|
||||
|
||||
// specialKey returns the code of the keys that do not come as a printable
|
||||
// character, and of the control combinations
|
||||
func specialKey(keycode uint32, modifiers uint16) (uint8, bool) {
|
||||
// retroKey translates a key of libretro. Only the ones that do not come as a
|
||||
// printable character are needed, the frontend keeps the rest of the keyboard
|
||||
// for itself.
|
||||
func retroKey(keycode uint32) shared.Key {
|
||||
switch keycode {
|
||||
case C.RETROK_RETURN, C.RETROK_KP_ENTER:
|
||||
return 13, true
|
||||
return shared.KeyReturn
|
||||
case C.RETROK_TAB:
|
||||
return 9, true
|
||||
return shared.KeyTab
|
||||
case C.RETROK_ESCAPE:
|
||||
return 27, true
|
||||
case C.RETROK_BACKSPACE, C.RETROK_LEFT:
|
||||
return 8, true
|
||||
return shared.KeyEscape
|
||||
case C.RETROK_BACKSPACE:
|
||||
return shared.KeyBackspace
|
||||
case C.RETROK_LEFT:
|
||||
return shared.KeyLeft
|
||||
case C.RETROK_RIGHT:
|
||||
return 21, true
|
||||
return shared.KeyRight
|
||||
case C.RETROK_UP:
|
||||
return 11, true
|
||||
return shared.KeyUp
|
||||
case C.RETROK_DOWN:
|
||||
return 10, true
|
||||
return shared.KeyDown
|
||||
case C.RETROK_DELETE:
|
||||
return 127, true
|
||||
return shared.KeyDelete
|
||||
}
|
||||
|
||||
if modifiers&C.RETROKMOD_CTRL != 0 &&
|
||||
keycode >= C.RETROK_a && keycode <= C.RETROK_z {
|
||||
return uint8(keycode-C.RETROK_a) + 1, true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
return shared.KeyNone
|
||||
}
|
||||
+17
-51
@@ -4,11 +4,10 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
"github.com/pkg/profile"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
@@ -56,18 +55,16 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
sdl.EventState(sdl.DROPBEGIN, sdl.DISABLE)
|
||||
sdl.EventState(sdl.DROPCOMPLETE, sdl.DISABLE)
|
||||
|
||||
kp := newSDLKeyBoard(a)
|
||||
view := shared.NewView()
|
||||
kp := newSDLKeyBoard(a, view)
|
||||
|
||||
s := newSDLAudio(a.GetClockMhz())
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(s.mixer.NewSource())
|
||||
}
|
||||
s := newSDLAudio(a)
|
||||
s.start()
|
||||
|
||||
j := newSDLJoysticks(!a.UsesMouse())
|
||||
a.SetJoysticksProvider(j)
|
||||
a.SetJoysticksProvider(j.paddles)
|
||||
|
||||
m := newSDLMouse()
|
||||
m := shared.NewMouse()
|
||||
a.SetMouseProvider(m)
|
||||
|
||||
d := newSDLDropTargets(a, window)
|
||||
@@ -85,6 +82,10 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
case *sdl.KeyboardEvent:
|
||||
kp.putKey(t)
|
||||
j.putKey(t)
|
||||
case *sdl.WindowEvent:
|
||||
if t.Event == sdl.WINDOWEVENT_FOCUS_LOST {
|
||||
j.paddles.ReleaseAppleKeys()
|
||||
}
|
||||
case *sdl.TextInputEvent:
|
||||
kp.putText(t.GetText())
|
||||
case *sdl.JoyAxisEvent:
|
||||
@@ -94,10 +95,12 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
case *sdl.MouseMotionEvent:
|
||||
w, h := window.GetSize()
|
||||
j.putMouseMotionEvent(t, w, h)
|
||||
m.putMouseMotionEvent(t, w, h)
|
||||
m.SetPosition(int(t.X), int(t.Y), int(w), int(h))
|
||||
case *sdl.MouseButtonEvent:
|
||||
j.putMouseButtonEvent(t)
|
||||
m.putMouseButtonEvent(t)
|
||||
if sdlMouseButton(t.Button) == shared.MouseButtonLeft {
|
||||
m.SetButton(t.State == sdl.PRESSED)
|
||||
}
|
||||
case *sdl.DropEvent:
|
||||
switch t.Type {
|
||||
case sdl.DROPFILE:
|
||||
@@ -122,21 +125,9 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
}
|
||||
|
||||
if !a.IsPaused() {
|
||||
var img *image.RGBA
|
||||
vs := a.GetVideoSource()
|
||||
if kp.showHelp {
|
||||
img = screen.SnapshotMessageGenerator(vs, helpMessage, false /*is80Columns*/)
|
||||
} else if d.showing(kp.showDropTargets) {
|
||||
img = d.snapshot()
|
||||
} else if kp.showCharGen {
|
||||
cgPage, cgPages := a.GetCgPageInfo()
|
||||
img = screen.SnapshotCharacterGenerator(vs, kp.showAltText)
|
||||
window.SetTitle(fmt.Sprintf("%v character map, page %v/%v", a.Name, cgPage+1, cgPages))
|
||||
} else if kp.showPages {
|
||||
img = screen.SnapshotParts(vs, kp.screenMode)
|
||||
window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(vs), img.Rect.Dx()/2, img.Rect.Dy()/2))
|
||||
} else {
|
||||
img = screen.Snapshot(vs, kp.screenMode)
|
||||
img, viewTitle := view.Snapshot(a, d.targets, d.pointedDrive())
|
||||
if viewTitle != "" {
|
||||
window.SetTitle(viewTitle)
|
||||
}
|
||||
if img != nil {
|
||||
surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]),
|
||||
@@ -167,29 +158,4 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
|
||||
}
|
||||
|
||||
var helpMessage = `
|
||||
F1: Show/Hide help
|
||||
Ctrl-F2: Reset
|
||||
F1, F2: Reset
|
||||
F4: Show/Hide CPU trace
|
||||
F5: Fast/Normal speed
|
||||
Ctrl-F5: Show speed
|
||||
F6: Next screen mode
|
||||
F7: Show/Hide pages
|
||||
F8: Show/Hide drop targets
|
||||
F10: Next character set
|
||||
Ctrl-F10: Show/Hide character set
|
||||
Shift-F10: Show/Hide alternate text
|
||||
F12: Save screen snapshot
|
||||
Pause: Pause the emulation
|
||||
|
||||
Left alt or option key: Open-Apple
|
||||
Right alt or option key: Closed-Apple
|
||||
|
||||
Drop a file on a drive area to load it
|
||||
|
||||
Run izapple2 -h for more options
|
||||
https://github.com/ivanizag/izapple2
|
||||
`
|
||||
|
||||
///////////////////////////////////////
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/audio"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
@@ -30,9 +32,9 @@ because the callback runs on the SDL audio thread.
|
||||
*/
|
||||
var theSDLAudio atomic.Pointer[sdlAudio]
|
||||
|
||||
func newSDLAudio(clockMhz float64) *sdlAudio {
|
||||
func newSDLAudio(a *izapple2.Apple2) *sdlAudio {
|
||||
return &sdlAudio{
|
||||
mixer: audio.NewMixer(clockMhz),
|
||||
mixer: shared.NewMixer(a),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
@@ -54,22 +52,6 @@ func (d *sdlDropTargets) dropped() int {
|
||||
return drive
|
||||
}
|
||||
|
||||
// showing returns whether the areas take over the screen, either because the
|
||||
// user asked for them with F8 or because a file has just been dropped
|
||||
func (d *sdlDropTargets) showing(requested bool) bool {
|
||||
return requested || d.targets.Flashing()
|
||||
}
|
||||
|
||||
// snapshot returns the screen with the areas, highlighting the drive that got
|
||||
// the last file while the flash lasts, or the one under the pointer
|
||||
func (d *sdlDropTargets) snapshot() *image.RGBA {
|
||||
selected := d.targets.FlashDrive()
|
||||
if !d.targets.Flashing() {
|
||||
selected = d.pointedDrive()
|
||||
}
|
||||
return d.targets.Snapshot(selected)
|
||||
}
|
||||
|
||||
// pointedDrive returns the drive the mouse pointer is on, -1 when it is
|
||||
// outside the window
|
||||
func (d *sdlDropTargets) pointedDrive() int {
|
||||
|
||||
+36
-105
@@ -3,31 +3,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
/*
|
||||
Apple 2 supports four paddles and 3 pushbuttons. The first two paddles are
|
||||
the X, Y axis of the first joystick. The second two correspond the the second
|
||||
joystick.
|
||||
Button 0 is the primary button of joystick 0.
|
||||
Button 1 is the secondary button of joystick 0 but also the primary button of
|
||||
joystick 1.
|
||||
Button 2 is the secondary button of Joystick 1.
|
||||
*/
|
||||
|
||||
// sdlJoysticks turns the joystick, mouse and apple key events of SDL2 into the
|
||||
// paddles and buttons the machine reads
|
||||
type sdlJoysticks struct {
|
||||
paddle [4]uint8
|
||||
hasPaddle [4]bool
|
||||
button [4]bool
|
||||
keys [3]bool
|
||||
mousebuttons [3]bool
|
||||
useMouse bool
|
||||
paddles *shared.Paddles
|
||||
}
|
||||
|
||||
func newSDLJoysticks(useMouseAlt bool) *sdlJoysticks {
|
||||
var j sdlJoysticks
|
||||
|
||||
err := sdl.InitSubSystem(sdl.INIT_JOYSTICK)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -35,82 +21,30 @@ func newSDLJoysticks(useMouseAlt bool) *sdlJoysticks {
|
||||
|
||||
// Init up to two joysticks
|
||||
sdl.JoystickEventState(sdl.ENABLE)
|
||||
joyCount := sdl.NumJoysticks()
|
||||
for iJoy := 0; iJoy < joyCount && iJoy < 2; iJoy++ {
|
||||
/*joystick := */ sdl.JoystickOpen(iJoy)
|
||||
j.hasPaddle[iJoy*2] = true
|
||||
j.hasPaddle[iJoy*2+1] = true
|
||||
joyCount := min(sdl.NumJoysticks(), 2)
|
||||
for iJoy := range joyCount {
|
||||
sdl.JoystickOpen(iJoy)
|
||||
}
|
||||
|
||||
// Initialize to max resistance if unplugged
|
||||
j.paddle[0] = 255
|
||||
j.paddle[1] = 255
|
||||
j.paddle[2] = 255
|
||||
j.paddle[3] = 255
|
||||
|
||||
if useMouseAlt && !j.hasPaddle[0] {
|
||||
// Use the mouse as joystick
|
||||
j.useMouse = true
|
||||
j.hasPaddle[0] = true
|
||||
j.hasPaddle[1] = true
|
||||
j.paddle[0] = 127
|
||||
j.paddle[1] = 127
|
||||
}
|
||||
|
||||
// To enter Apple IIe on self test mode
|
||||
// j.keys[1] = true
|
||||
|
||||
var j sdlJoysticks
|
||||
j.paddles = shared.NewPaddles(joyCount, useMouseAlt)
|
||||
return &j
|
||||
}
|
||||
|
||||
func (j *sdlJoysticks) putAxisEvent(e *sdl.JoyAxisEvent) {
|
||||
if e.Which >= 2 || e.Axis >= 2 {
|
||||
// Process only the first two axis of the first two joysticks
|
||||
return
|
||||
}
|
||||
|
||||
j.paddle[uint8(e.Which)*2+e.Axis] = uint8((e.Value >> 8) + 128)
|
||||
j.paddles.SetAxis(int(e.Which), int(e.Axis), e.Value)
|
||||
}
|
||||
|
||||
func (j *sdlJoysticks) putButtonEvent(e *sdl.JoyButtonEvent) {
|
||||
if e.Which >= 2 {
|
||||
// Process only the buttons of the first two joysticks
|
||||
return
|
||||
}
|
||||
|
||||
j.button[uint8(e.Which)*2+(e.Button%2)] = (e.State != 0)
|
||||
}
|
||||
|
||||
func mouseToJoyCentered(x int32, w int32) uint8 {
|
||||
r := max(min(x-(w/2)+127, 255), 0)
|
||||
return uint8(r)
|
||||
|
||||
j.paddles.SetButton(int(e.Which), int(e.Button), e.State != 0)
|
||||
}
|
||||
|
||||
func (j *sdlJoysticks) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, height int32) {
|
||||
if j.useMouse {
|
||||
// The mouse moves on all the window
|
||||
// j.paddle[0] = mouseToJoyFull(e.X, width)
|
||||
// j.paddle[1] = mouseToJoyFull(e.Y, height)
|
||||
|
||||
// The mouse moves around the center of the window
|
||||
j.paddle[0] = mouseToJoyCentered(e.X, width)
|
||||
j.paddle[1] = mouseToJoyCentered(e.Y, height)
|
||||
}
|
||||
j.paddles.SetMousePosition(int(e.X), int(e.Y), int(width), int(height))
|
||||
}
|
||||
|
||||
func (j *sdlJoysticks) putMouseButtonEvent(e *sdl.MouseButtonEvent) {
|
||||
if j.useMouse {
|
||||
pressed := e.State == sdl.PRESSED
|
||||
switch e.Button {
|
||||
case 1: // BUTTON_LEFT
|
||||
j.mousebuttons[0] = pressed
|
||||
case 3: // BUTTON_RIGHT
|
||||
j.mousebuttons[1] = pressed
|
||||
case 2: // BUTTON_MIDDLE
|
||||
j.mousebuttons[2] = pressed
|
||||
}
|
||||
}
|
||||
j.paddles.SetMouseButton(sdlMouseButton(e.Button), e.State == sdl.PRESSED)
|
||||
}
|
||||
|
||||
func (j *sdlJoysticks) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
@@ -119,34 +53,31 @@ func (j *sdlJoysticks) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
Actually the Apple//e does this with the open and solid apple keys.
|
||||
Alt key - button 0 - Open apple
|
||||
AltGr key - button 1- Solid apple
|
||||
//Win key - button 2 (Not in the Apple //e keyboard)
|
||||
|
||||
The apple keys are a place on the keyboard, not a symbol, so we look at
|
||||
the scancode. The keycode is resolved through the current keymap: on
|
||||
layouts where the left alt key is Meta_L instead of Alt_L it is not
|
||||
K_LALT and the open apple would never be pressed.
|
||||
*/
|
||||
pressed := keyEvent.Type == sdl.KEYDOWN
|
||||
switch keyEvent.Keysym.Sym {
|
||||
case sdl.K_LALT:
|
||||
j.keys[0] = pressed
|
||||
case sdl.K_RALT:
|
||||
j.keys[1] = pressed
|
||||
// case sdl.K_LGUI:
|
||||
// j.keys[2] = pressed
|
||||
switch keyEvent.Keysym.Scancode {
|
||||
case sdl.SCANCODE_LALT:
|
||||
j.paddles.SetOpenApple(pressed)
|
||||
case sdl.SCANCODE_RALT:
|
||||
j.paddles.SetClosedApple(pressed)
|
||||
}
|
||||
}
|
||||
|
||||
// sdlMouseButton translates a button of the mouse of SDL2
|
||||
func sdlMouseButton(button uint8) int {
|
||||
switch button {
|
||||
case sdl.BUTTON_LEFT:
|
||||
return shared.MouseButtonLeft
|
||||
case sdl.BUTTON_RIGHT:
|
||||
return shared.MouseButtonRight
|
||||
case sdl.BUTTON_MIDDLE:
|
||||
return shared.MouseButtonMiddle
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (j *sdlJoysticks) ReadButton(i int) bool {
|
||||
var value bool
|
||||
switch i {
|
||||
case 0:
|
||||
value = j.button[0] || j.keys[0] || j.mousebuttons[0]
|
||||
case 1:
|
||||
// It can be secondary of first or primary of second
|
||||
value = j.button[1] || j.button[2] || j.keys[1] || j.mousebuttons[1]
|
||||
case 2:
|
||||
value = j.button[3] || j.keys[2] || j.mousebuttons[2]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (j *sdlJoysticks) ReadPaddle(i int) (uint8, bool) {
|
||||
return j.paddle[i], j.hasPaddle[i]
|
||||
return -1
|
||||
}
|
||||
+50
-132
@@ -3,49 +3,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
// sdlKeyboard turns the key events of SDL2 into the keys the frontends share
|
||||
type sdlKeyboard struct {
|
||||
a *izapple2.Apple2
|
||||
keyChannel *izapple2.KeyboardChannel
|
||||
|
||||
showHelp bool
|
||||
showPages bool
|
||||
showCharGen bool
|
||||
showAltText bool
|
||||
showDropTargets bool
|
||||
screenMode int
|
||||
keyboard *shared.Keyboard
|
||||
}
|
||||
|
||||
func newSDLKeyBoard(a *izapple2.Apple2) *sdlKeyboard {
|
||||
func newSDLKeyBoard(a *izapple2.Apple2, view *shared.View) *sdlKeyboard {
|
||||
var k sdlKeyboard
|
||||
k.a = a
|
||||
k.keyChannel = izapple2.NewKeyboardChannel(a)
|
||||
|
||||
k.screenMode = screen.ScreenModeColorScanlines
|
||||
k.keyboard = shared.NewKeyboard(a, view)
|
||||
return &k
|
||||
}
|
||||
|
||||
func (k *sdlKeyboard) putText(text string) {
|
||||
k.keyChannel.PutText(text)
|
||||
k.keyboard.PutText(text)
|
||||
}
|
||||
|
||||
func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
/*
|
||||
See "Apple II reference manual", page 5
|
||||
|
||||
To get keys as understood by the Apple2 hardware run:
|
||||
10 A=PEEK(49152)
|
||||
20 PRINT A, A - 128
|
||||
30 GOTO 10
|
||||
*/
|
||||
|
||||
if keyEvent.Type != sdl.KEYDOWN {
|
||||
// Process only key pushes
|
||||
return
|
||||
@@ -56,129 +34,69 @@ func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
shift := key.Mod&sdl.KMOD_SHIFT != 0
|
||||
command := key.Mod&sdl.KMOD_GUI != 0
|
||||
|
||||
if ctrl {
|
||||
if key.Sym >= 'a' && key.Sym <= 'z' {
|
||||
k.keyChannel.PutChar(uint8(key.Sym) - 97 + 1)
|
||||
return
|
||||
}
|
||||
if ctrl && k.keyboard.PutCtrlLetter(rune(key.Sym)) {
|
||||
return
|
||||
}
|
||||
|
||||
result := uint8(0)
|
||||
// Pasting reads the clipboard of SDL, it is not a shared command
|
||||
if (key.Sym == sdl.K_INSERT && shift) || (key.Sym == sdl.K_v && command) {
|
||||
text, _ := sdl.GetClipboardText()
|
||||
k.keyboard.Paste(text)
|
||||
return
|
||||
}
|
||||
|
||||
switch key.Sym {
|
||||
k.keyboard.PutKey(sdlKey(key.Sym), ctrl, shift)
|
||||
}
|
||||
|
||||
// sdlKey translates a key of SDL2
|
||||
func sdlKey(sym sdl.Keycode) shared.Key {
|
||||
switch sym {
|
||||
case sdl.K_ESCAPE:
|
||||
result = 27
|
||||
return shared.KeyEscape
|
||||
case sdl.K_BACKSPACE:
|
||||
result = 8
|
||||
case sdl.K_RETURN:
|
||||
result = 13
|
||||
case sdl.K_RETURN2:
|
||||
result = 13
|
||||
case sdl.K_LEFT:
|
||||
if ctrl {
|
||||
result = 31 // Base64A
|
||||
} else {
|
||||
result = 8
|
||||
}
|
||||
case sdl.K_RIGHT:
|
||||
result = 21
|
||||
|
||||
// Apple //e
|
||||
case sdl.K_UP:
|
||||
result = 11 // 31 in the Base64A
|
||||
case sdl.K_DOWN:
|
||||
result = 10
|
||||
return shared.KeyBackspace
|
||||
case sdl.K_RETURN, sdl.K_RETURN2:
|
||||
return shared.KeyReturn
|
||||
case sdl.K_TAB:
|
||||
result = 9
|
||||
return shared.KeyTab
|
||||
case sdl.K_DELETE:
|
||||
result = 127 // 24 in the Base64A
|
||||
return shared.KeyDelete
|
||||
case sdl.K_LEFT:
|
||||
return shared.KeyLeft
|
||||
case sdl.K_RIGHT:
|
||||
return shared.KeyRight
|
||||
case sdl.K_UP:
|
||||
return shared.KeyUp
|
||||
case sdl.K_DOWN:
|
||||
return shared.KeyDown
|
||||
|
||||
// Base64A clone particularities
|
||||
case sdl.K_F3:
|
||||
result = 127 // Base64A
|
||||
|
||||
// Control of the emulator
|
||||
case sdl.K_F1:
|
||||
k.showHelp = !k.showHelp
|
||||
return shared.KeyF1
|
||||
case sdl.K_F2:
|
||||
// While the help is shown, F2 alone triggers a reset. This is handy
|
||||
// when the window manager (for example KDE) captures Ctrl-F2.
|
||||
if ctrl || k.showHelp {
|
||||
k.a.SendCommand(izapple2.CommandReset)
|
||||
k.showHelp = false
|
||||
}
|
||||
return shared.KeyF2
|
||||
case sdl.K_F3:
|
||||
return shared.KeyF3
|
||||
case sdl.K_F4:
|
||||
k.a.SendCommand(izapple2.CommandToggleCPUTrace)
|
||||
return shared.KeyF4
|
||||
case sdl.K_F5:
|
||||
if ctrl {
|
||||
k.a.SendCommand(izapple2.CommandShowSpeed)
|
||||
} else {
|
||||
k.a.SendCommand(izapple2.CommandToggleSpeed)
|
||||
}
|
||||
return shared.KeyF5
|
||||
case sdl.K_F6:
|
||||
k.screenMode = screen.NextScreenMode(k.screenMode)
|
||||
return shared.KeyF6
|
||||
case sdl.K_F7:
|
||||
k.showPages = !k.showPages
|
||||
return shared.KeyF7
|
||||
case sdl.K_F8:
|
||||
k.showDropTargets = !k.showDropTargets
|
||||
if k.showDropTargets {
|
||||
// The help is shown on top of the drop targets, get it out of the way
|
||||
k.showHelp = false
|
||||
}
|
||||
return shared.KeyF8
|
||||
case sdl.K_F9:
|
||||
k.a.SendCommand(izapple2.CommandDumpDebugInfo)
|
||||
return shared.KeyF9
|
||||
case sdl.K_F10:
|
||||
if ctrl {
|
||||
k.showCharGen = !k.showCharGen
|
||||
} else if shift {
|
||||
k.showAltText = !k.showAltText
|
||||
} else {
|
||||
k.a.SendCommand(izapple2.CommandNextCharGenPage)
|
||||
}
|
||||
return shared.KeyF10
|
||||
case sdl.K_F12:
|
||||
fallthrough
|
||||
case sdl.K_PRINTSCREEN:
|
||||
if ctrl {
|
||||
screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/")
|
||||
} else {
|
||||
err := screen.SaveSnapshot(k.a.GetVideoSource(), screen.ScreenModeColorScanlines, "snapshot.png")
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving snapshoot: %v.\n.", err)
|
||||
} else {
|
||||
fmt.Println("Saving snapshot 'snapshot.png'")
|
||||
}
|
||||
}
|
||||
return shared.KeyF12
|
||||
case sdl.K_PAUSE:
|
||||
k.a.SendCommand(izapple2.CommandPauseUnpause)
|
||||
case sdl.K_INSERT:
|
||||
if shift {
|
||||
text, _ := sdl.GetClipboardText()
|
||||
go k.performPaste(text)
|
||||
}
|
||||
case sdl.K_v:
|
||||
if command {
|
||||
text, _ := sdl.GetClipboardText()
|
||||
go k.performPaste(text)
|
||||
}
|
||||
return shared.KeyPause
|
||||
case sdl.K_PRINTSCREEN:
|
||||
return shared.KeyPrintScreen
|
||||
}
|
||||
|
||||
// Missing values 91 to 95. Usually control for [\]^_
|
||||
// On the Base64A it's control for \]./
|
||||
|
||||
if result != 0 {
|
||||
k.keyChannel.PutChar(result)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *sdlKeyboard) performPaste(text string) {
|
||||
// Note 1: Pasting is too fast, so we slow it down.
|
||||
// Note 2: Need to translate CR/LF's
|
||||
for _, ch := range text {
|
||||
if ch == '\r' || ch == '\n' {
|
||||
k.keyChannel.PutChar(13)
|
||||
} else {
|
||||
k.keyChannel.PutRune(ch)
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
return shared.KeyNone
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//go:build !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
type sdlMouse struct {
|
||||
x uint16
|
||||
y uint16
|
||||
pressed bool
|
||||
}
|
||||
|
||||
func newSDLMouse() *sdlMouse {
|
||||
var m sdlMouse
|
||||
return &m
|
||||
}
|
||||
|
||||
func (m *sdlMouse) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, height int32) {
|
||||
m.x = uint16(65536 * e.X / width)
|
||||
m.y = uint16(65536 * e.Y / height)
|
||||
}
|
||||
|
||||
func (m *sdlMouse) putMouseButtonEvent(e *sdl.MouseButtonEvent) {
|
||||
if e.Button == 1 { // BUTTTON_LEFT
|
||||
m.pressed = e.State == sdl.PRESSED
|
||||
}
|
||||
}
|
||||
|
||||
func (m *sdlMouse) ReadMouse() (x uint16, y uint16, pressed bool) {
|
||||
return m.x, m.y, m.pressed
|
||||
}
|
||||
|
||||
// TODO: SDL_WarpMouseInWindow
|
||||
+14
-52
@@ -4,14 +4,13 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
"github.com/Zyko0/go-sdl3/bin/binsdl"
|
||||
"github.com/Zyko0/go-sdl3/sdl"
|
||||
@@ -64,19 +63,17 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
fmt.Printf("Error starting text input: %v.\n", err)
|
||||
}
|
||||
|
||||
kp := newSDL3Keyboard(a)
|
||||
view := shared.NewView()
|
||||
kp := newSDL3Keyboard(a, view)
|
||||
|
||||
s := newSDL3Audio(a.GetClockMhz())
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(s.mixer.NewSource())
|
||||
}
|
||||
s := newSDL3Audio(a)
|
||||
s.start()
|
||||
defer s.close()
|
||||
|
||||
j := newSDL3Joysticks(!a.UsesMouse())
|
||||
a.SetJoysticksProvider(j)
|
||||
a.SetJoysticksProvider(j.paddles)
|
||||
|
||||
m := newSDL3Mouse()
|
||||
m := shared.NewMouse()
|
||||
a.SetMouseProvider(m)
|
||||
|
||||
d := newSDL3DropTargets(a, window)
|
||||
@@ -112,7 +109,7 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
kp.putKey(e)
|
||||
j.putKey(e)
|
||||
case sdl.EVENT_WINDOW_FOCUS_LOST:
|
||||
j.releaseKeys()
|
||||
j.paddles.ReleaseAppleKeys()
|
||||
case sdl.EVENT_TEXT_INPUT:
|
||||
kp.putText(event.TextInputEvent().Text)
|
||||
case sdl.EVENT_JOYSTICK_AXIS_MOTION:
|
||||
@@ -123,12 +120,14 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
w, h, _ := window.Size()
|
||||
e := event.MouseMotionEvent()
|
||||
j.putMouseMotionEvent(e, w, h)
|
||||
m.putMouseMotionEvent(e, w, h)
|
||||
m.SetPosition(int(e.X), int(e.Y), int(w), int(h))
|
||||
d.dragEnded()
|
||||
case sdl.EVENT_MOUSE_BUTTON_DOWN, sdl.EVENT_MOUSE_BUTTON_UP:
|
||||
e := event.MouseButtonEvent()
|
||||
j.putMouseButtonEvent(e)
|
||||
m.putMouseButtonEvent(e)
|
||||
if sdl3MouseButton(e.Button) == shared.MouseButtonLeft {
|
||||
m.SetButton(e.Down)
|
||||
}
|
||||
case sdl.EVENT_DROP_BEGIN:
|
||||
// Unlike SDL2, SDL3 reports the file being dragged over the
|
||||
// window, so the drop targets can be shown while it moves.
|
||||
@@ -160,21 +159,9 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
}
|
||||
|
||||
if !a.IsPaused() {
|
||||
var img *image.RGBA
|
||||
vs := a.GetVideoSource()
|
||||
if kp.showHelp {
|
||||
img = screen.SnapshotMessageGenerator(vs, helpMessage, false /*is80Columns*/)
|
||||
} else if d.showing(kp.showDropTargets) {
|
||||
img = d.snapshot()
|
||||
} else if kp.showCharGen {
|
||||
cgPage, cgPages := a.GetCgPageInfo()
|
||||
img = screen.SnapshotCharacterGenerator(vs, kp.showAltText)
|
||||
window.SetTitle(fmt.Sprintf("%v character map, page %v/%v", a.Name, cgPage+1, cgPages))
|
||||
} else if kp.showPages {
|
||||
img = screen.SnapshotParts(vs, kp.screenMode)
|
||||
window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(vs), img.Rect.Dx()/2, img.Rect.Dy()/2))
|
||||
} else {
|
||||
img = screen.Snapshot(vs, kp.screenMode)
|
||||
img, viewTitle := view.Snapshot(a, d.targets, d.pointedDrive())
|
||||
if viewTitle != "" {
|
||||
window.SetTitle(viewTitle)
|
||||
}
|
||||
if img != nil {
|
||||
// image.RGBA stores the bytes as R, G, B, A. That is
|
||||
@@ -205,29 +192,4 @@ func sdlRun(a *izapple2.Apple2) {
|
||||
}
|
||||
}
|
||||
|
||||
var helpMessage = `
|
||||
F1: Show/Hide help
|
||||
Ctrl-F2: Reset
|
||||
F1, F2: Reset
|
||||
F4: Show/Hide CPU trace
|
||||
F5: Fast/Normal speed
|
||||
Ctrl-F5: Show speed
|
||||
F6: Next screen mode
|
||||
F7: Show/Hide pages
|
||||
F8: Show/Hide drop targets
|
||||
F10: Next character set
|
||||
Ctrl-F10: Show/Hide character set
|
||||
Shift-F10: Show/Hide alternate text
|
||||
F12: Save screen snapshot
|
||||
Pause: Pause the emulation
|
||||
|
||||
Left alt or option key: Open-Apple
|
||||
Right alt or option key: Closed-Apple
|
||||
|
||||
Drop a file on a drive area to load it
|
||||
|
||||
Run izapple2 -h for more options
|
||||
https://github.com/ivanizag/izapple2
|
||||
`
|
||||
|
||||
///////////////////////////////////////
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/audio"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
"github.com/Zyko0/go-sdl3/sdl"
|
||||
)
|
||||
@@ -23,9 +25,9 @@ type sdl3Audio struct {
|
||||
buf []float32
|
||||
}
|
||||
|
||||
func newSDL3Audio(clockMhz float64) *sdl3Audio {
|
||||
func newSDL3Audio(a *izapple2.Apple2) *sdl3Audio {
|
||||
return &sdl3Audio{
|
||||
mixer: audio.NewMixer(clockMhz),
|
||||
mixer: shared.NewMixer(a),
|
||||
buf: make([]float32, bufferSamples),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
@@ -24,10 +22,6 @@ file went.
|
||||
type sdl3DropTargets struct {
|
||||
targets *shared.DropTargets
|
||||
window *sdl.Window
|
||||
|
||||
dragging bool // A file is being dragged over the window
|
||||
dragKnown bool // The position of the file has been reported
|
||||
dragX float32 // Where the file is on the window
|
||||
}
|
||||
|
||||
func newSDL3DropTargets(a *izapple2.Apple2, window *sdl.Window) *sdl3DropTargets {
|
||||
@@ -40,22 +34,19 @@ func newSDL3DropTargets(a *izapple2.Apple2, window *sdl.Window) *sdl3DropTargets
|
||||
// dragStarted is called when a file begins to be dragged over the window,
|
||||
// before knowing where it is
|
||||
func (d *sdl3DropTargets) dragStarted() {
|
||||
d.dragging = true
|
||||
d.dragKnown = false
|
||||
d.targets.DragStarted()
|
||||
}
|
||||
|
||||
// dragMoved tracks a file being dragged over the window
|
||||
func (d *sdl3DropTargets) dragMoved(x float32) {
|
||||
d.dragging = true
|
||||
d.dragKnown = true
|
||||
d.dragX = x
|
||||
d.targets.DragMoved(d.driveAt(x))
|
||||
}
|
||||
|
||||
// dragEnded is called when the file is dropped or leaves the window. The mouse
|
||||
// motion events do not arrive while a file is being dragged, so the first one
|
||||
// also means that the drag is over, whatever SDL reported.
|
||||
func (d *sdl3DropTargets) dragEnded() {
|
||||
d.dragging = false
|
||||
d.targets.DragEnded()
|
||||
}
|
||||
|
||||
// dropped returns the drive that gets a file dropped at the given position of
|
||||
@@ -68,28 +59,6 @@ func (d *sdl3DropTargets) dropped(x float32) int {
|
||||
return drive
|
||||
}
|
||||
|
||||
// showing returns whether the areas take over the screen, either because a
|
||||
// file is being dragged, because the user asked for them with F8, or because a
|
||||
// file has just been dropped
|
||||
func (d *sdl3DropTargets) showing(requested bool) bool {
|
||||
return d.dragging || requested || d.targets.Flashing()
|
||||
}
|
||||
|
||||
// snapshot returns the screen with the areas, highlighting the drive under the
|
||||
// file being dragged or the one that got the last file
|
||||
func (d *sdl3DropTargets) snapshot() *image.RGBA {
|
||||
selected := -1
|
||||
if d.dragging {
|
||||
if d.dragKnown {
|
||||
selected = d.driveAt(d.dragX)
|
||||
}
|
||||
} else if d.targets.Flashing() {
|
||||
selected = d.targets.FlashDrive()
|
||||
}
|
||||
|
||||
return d.targets.Snapshot(selected)
|
||||
}
|
||||
|
||||
func (d *sdl3DropTargets) driveAt(x float32) int {
|
||||
width, _, err := d.window.Size()
|
||||
if err != nil {
|
||||
@@ -98,3 +67,19 @@ func (d *sdl3DropTargets) driveAt(x float32) int {
|
||||
|
||||
return d.targets.DriveAt(int(x), int(width))
|
||||
}
|
||||
|
||||
// pointedDrive returns the drive the mouse pointer is on, -1 when it is
|
||||
// outside the window
|
||||
func (d *sdl3DropTargets) pointedDrive() int {
|
||||
if sdl.GetMouseFocus() != d.window {
|
||||
return -1
|
||||
}
|
||||
|
||||
_, mouseX, _ := sdl.GetMouseState()
|
||||
width, _, err := d.window.Size()
|
||||
if err != nil || mouseX < 0 || mouseX >= float32(width) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return d.targets.DriveAt(int(mouseX), int(width))
|
||||
}
|
||||
@@ -3,126 +3,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
"github.com/Zyko0/go-sdl3/sdl"
|
||||
)
|
||||
|
||||
/*
|
||||
Apple 2 supports four paddles and 3 pushbuttons. The first two paddles are
|
||||
the X, Y axis of the first joystick. The second two correspond the the second
|
||||
joystick.
|
||||
Button 0 is the primary button of joystick 0.
|
||||
Button 1 is the secondary button of joystick 0 but also the primary button of
|
||||
joystick 1.
|
||||
Button 2 is the secondary button of Joystick 1.
|
||||
*/
|
||||
|
||||
// sdl3Joysticks turns the joystick, mouse and apple key events of SDL3 into
|
||||
// the paddles and buttons the machine reads
|
||||
type sdl3Joysticks struct {
|
||||
paddle [4]uint8
|
||||
hasPaddle [4]bool
|
||||
button [4]bool
|
||||
keys [3]bool
|
||||
mousebuttons [3]bool
|
||||
useMouse bool
|
||||
paddles *shared.Paddles
|
||||
|
||||
// SDL3 identifies joysticks with instance ids that are not indexes:
|
||||
// they start at 1 and grow as devices are plugged. The events carry
|
||||
// the instance id, so we keep the slot each one was assigned to.
|
||||
slots map[sdl.JoystickID]uint8
|
||||
slots map[sdl.JoystickID]int
|
||||
}
|
||||
|
||||
func newSDL3Joysticks(useMouseAlt bool) *sdl3Joysticks {
|
||||
var j sdl3Joysticks
|
||||
j.slots = make(map[sdl.JoystickID]uint8)
|
||||
j.slots = make(map[sdl.JoystickID]int)
|
||||
|
||||
sdl.SetJoystickEventsEnabled(true)
|
||||
|
||||
// Init up to two joysticks
|
||||
ids, err := sdl.GetJoysticks()
|
||||
if err == nil {
|
||||
for slot, id := range ids {
|
||||
if slot >= 2 {
|
||||
for _, id := range ids {
|
||||
if len(j.slots) >= 2 {
|
||||
break
|
||||
}
|
||||
_, err := id.OpenJoystick()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
j.slots[id] = uint8(slot)
|
||||
j.hasPaddle[slot*2] = true
|
||||
j.hasPaddle[slot*2+1] = true
|
||||
j.slots[id] = len(j.slots)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize to max resistance if unplugged
|
||||
j.paddle[0] = 255
|
||||
j.paddle[1] = 255
|
||||
j.paddle[2] = 255
|
||||
j.paddle[3] = 255
|
||||
|
||||
if useMouseAlt && !j.hasPaddle[0] {
|
||||
// Use the mouse as joystick
|
||||
j.useMouse = true
|
||||
j.hasPaddle[0] = true
|
||||
j.hasPaddle[1] = true
|
||||
j.paddle[0] = 127
|
||||
j.paddle[1] = 127
|
||||
}
|
||||
|
||||
// To enter Apple IIe on self test mode
|
||||
// j.keys[1] = true
|
||||
|
||||
j.paddles = shared.NewPaddles(len(j.slots), useMouseAlt)
|
||||
return &j
|
||||
}
|
||||
|
||||
func (j *sdl3Joysticks) putAxisEvent(e *sdl.JoyAxisEvent) {
|
||||
slot, ok := j.slots[e.Which]
|
||||
if !ok || e.Axis >= 2 {
|
||||
// Process only the first two axis of the first two joysticks
|
||||
return
|
||||
}
|
||||
|
||||
j.paddle[slot*2+e.Axis] = uint8((e.Value >> 8) + 128)
|
||||
j.paddles.SetAxis(j.slotOf(e.Which), int(e.Axis), e.Value)
|
||||
}
|
||||
|
||||
func (j *sdl3Joysticks) putButtonEvent(e *sdl.JoyButtonEvent) {
|
||||
slot, ok := j.slots[e.Which]
|
||||
if !ok {
|
||||
// Process only the buttons of the first two joysticks
|
||||
return
|
||||
}
|
||||
|
||||
j.button[slot*2+(e.Button%2)] = e.Down
|
||||
}
|
||||
|
||||
func mouseToJoyCentered(x int32, w int32) uint8 {
|
||||
r := max(min(x-(w/2)+127, 255), 0)
|
||||
return uint8(r)
|
||||
|
||||
j.paddles.SetButton(j.slotOf(e.Which), int(e.Button), e.Down)
|
||||
}
|
||||
|
||||
func (j *sdl3Joysticks) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, height int32) {
|
||||
if j.useMouse {
|
||||
// The mouse moves on all the window
|
||||
// j.paddle[0] = mouseToJoyFull(int32(e.X), width)
|
||||
// j.paddle[1] = mouseToJoyFull(int32(e.Y), height)
|
||||
|
||||
// The mouse moves around the center of the window
|
||||
j.paddle[0] = mouseToJoyCentered(int32(e.X), width)
|
||||
j.paddle[1] = mouseToJoyCentered(int32(e.Y), height)
|
||||
}
|
||||
j.paddles.SetMousePosition(int(e.X), int(e.Y), int(width), int(height))
|
||||
}
|
||||
|
||||
func (j *sdl3Joysticks) putMouseButtonEvent(e *sdl.MouseButtonEvent) {
|
||||
if j.useMouse {
|
||||
switch e.Button {
|
||||
case 1: // BUTTON_LEFT
|
||||
j.mousebuttons[0] = e.Down
|
||||
case 3: // BUTTON_RIGHT
|
||||
j.mousebuttons[1] = e.Down
|
||||
case 2: // BUTTON_MIDDLE
|
||||
j.mousebuttons[2] = e.Down
|
||||
}
|
||||
}
|
||||
j.paddles.SetMouseButton(sdl3MouseButton(e.Button), e.Down)
|
||||
}
|
||||
|
||||
func (j *sdl3Joysticks) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
@@ -131,7 +66,6 @@ func (j *sdl3Joysticks) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
Actually the Apple//e does this with the open and solid apple keys.
|
||||
Alt key - button 0 - Open apple
|
||||
AltGr key - button 1- Solid apple
|
||||
//Win key - button 2 (Not in the Apple //e keyboard)
|
||||
|
||||
The apple keys are a place on the keyboard, not a symbol, so we look at
|
||||
the scancode. Unlike SDL2, SDL3 resolves the keycode through the current
|
||||
@@ -140,36 +74,33 @@ func (j *sdl3Joysticks) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
*/
|
||||
switch keyEvent.Scancode {
|
||||
case sdl.SCANCODE_LALT:
|
||||
j.keys[0] = keyEvent.Down
|
||||
j.paddles.SetOpenApple(keyEvent.Down)
|
||||
case sdl.SCANCODE_RALT:
|
||||
j.keys[1] = keyEvent.Down
|
||||
// case sdl.SCANCODE_LGUI:
|
||||
// j.keys[2] = keyEvent.Down
|
||||
j.paddles.SetClosedApple(keyEvent.Down)
|
||||
}
|
||||
}
|
||||
|
||||
// slotOf returns the joystick an instance id was assigned to, -1 for the ones
|
||||
// left out
|
||||
func (j *sdl3Joysticks) slotOf(id sdl.JoystickID) int {
|
||||
slot, ok := j.slots[id]
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
|
||||
return slot
|
||||
}
|
||||
|
||||
// releaseKeys forgets the apple keys being pressed. The key up event of a key
|
||||
// held while the window loses the focus goes to whoever took it, leaving the
|
||||
// apple key pressed forever.
|
||||
func (j *sdl3Joysticks) releaseKeys() {
|
||||
j.keys = [3]bool{}
|
||||
}
|
||||
|
||||
func (j *sdl3Joysticks) ReadButton(i int) bool {
|
||||
var value bool
|
||||
switch i {
|
||||
case 0:
|
||||
value = j.button[0] || j.keys[0] || j.mousebuttons[0]
|
||||
case 1:
|
||||
// It can be secondary of first or primary of second
|
||||
value = j.button[1] || j.button[2] || j.keys[1] || j.mousebuttons[1]
|
||||
case 2:
|
||||
value = j.button[3] || j.keys[2] || j.mousebuttons[2]
|
||||
// sdl3MouseButton translates a button of the mouse of SDL3
|
||||
func sdl3MouseButton(button uint8) int {
|
||||
switch sdl.MouseButtonFlags(button) {
|
||||
case sdl.BUTTON_LEFT:
|
||||
return shared.MouseButtonLeft
|
||||
case sdl.BUTTON_RIGHT:
|
||||
return shared.MouseButtonRight
|
||||
case sdl.BUTTON_MIDDLE:
|
||||
return shared.MouseButtonMiddle
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (j *sdl3Joysticks) ReadPaddle(i int) (uint8, bool) {
|
||||
return j.paddle[i], j.hasPaddle[i]
|
||||
return -1
|
||||
}
|
||||
+50
-132
@@ -3,50 +3,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
"github.com/Zyko0/go-sdl3/sdl"
|
||||
)
|
||||
|
||||
// sdl3Keyboard turns the key events of SDL3 into the keys the frontends share
|
||||
type sdl3Keyboard struct {
|
||||
a *izapple2.Apple2
|
||||
keyChannel *izapple2.KeyboardChannel
|
||||
|
||||
showHelp bool
|
||||
showPages bool
|
||||
showCharGen bool
|
||||
showAltText bool
|
||||
showDropTargets bool
|
||||
screenMode int
|
||||
keyboard *shared.Keyboard
|
||||
}
|
||||
|
||||
func newSDL3Keyboard(a *izapple2.Apple2) *sdl3Keyboard {
|
||||
func newSDL3Keyboard(a *izapple2.Apple2, view *shared.View) *sdl3Keyboard {
|
||||
var k sdl3Keyboard
|
||||
k.a = a
|
||||
k.keyChannel = izapple2.NewKeyboardChannel(a)
|
||||
|
||||
k.screenMode = screen.ScreenModeColorScanlines
|
||||
k.keyboard = shared.NewKeyboard(a, view)
|
||||
return &k
|
||||
}
|
||||
|
||||
func (k *sdl3Keyboard) putText(text string) {
|
||||
k.keyChannel.PutText(text)
|
||||
k.keyboard.PutText(text)
|
||||
}
|
||||
|
||||
func (k *sdl3Keyboard) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
/*
|
||||
See "Apple II reference manual", page 5
|
||||
|
||||
To get keys as understood by the Apple2 hardware run:
|
||||
10 A=PEEK(49152)
|
||||
20 PRINT A, A - 128
|
||||
30 GOTO 10
|
||||
*/
|
||||
|
||||
if !keyEvent.Down {
|
||||
// Process only key pushes
|
||||
return
|
||||
@@ -56,129 +34,69 @@ func (k *sdl3Keyboard) putKey(keyEvent *sdl.KeyboardEvent) {
|
||||
shift := keyEvent.Mod&sdl.KMOD_SHIFT != 0
|
||||
command := keyEvent.Mod&sdl.KMOD_GUI != 0
|
||||
|
||||
if ctrl {
|
||||
if keyEvent.Key >= 'a' && keyEvent.Key <= 'z' {
|
||||
k.keyChannel.PutChar(uint8(keyEvent.Key) - 97 + 1)
|
||||
return
|
||||
}
|
||||
if ctrl && k.keyboard.PutCtrlLetter(rune(keyEvent.Key)) {
|
||||
return
|
||||
}
|
||||
|
||||
result := uint8(0)
|
||||
// Pasting reads the clipboard of SDL, it is not a shared command
|
||||
if (keyEvent.Key == sdl.K_INSERT && shift) || (keyEvent.Key == 'v' && command) {
|
||||
text, _ := sdl.GetClipboardText()
|
||||
k.keyboard.Paste(text)
|
||||
return
|
||||
}
|
||||
|
||||
switch keyEvent.Key {
|
||||
k.keyboard.PutKey(sdl3Key(keyEvent.Key), ctrl, shift)
|
||||
}
|
||||
|
||||
// sdl3Key translates a key of SDL3
|
||||
func sdl3Key(key sdl.Keycode) shared.Key {
|
||||
switch key {
|
||||
case sdl.K_ESCAPE:
|
||||
result = 27
|
||||
return shared.KeyEscape
|
||||
case sdl.K_BACKSPACE:
|
||||
result = 8
|
||||
case sdl.K_RETURN:
|
||||
result = 13
|
||||
case sdl.K_RETURN2:
|
||||
result = 13
|
||||
case sdl.K_LEFT:
|
||||
if ctrl {
|
||||
result = 31 // Base64A
|
||||
} else {
|
||||
result = 8
|
||||
}
|
||||
case sdl.K_RIGHT:
|
||||
result = 21
|
||||
|
||||
// Apple //e
|
||||
case sdl.K_UP:
|
||||
result = 11 // 31 in the Base64A
|
||||
case sdl.K_DOWN:
|
||||
result = 10
|
||||
return shared.KeyBackspace
|
||||
case sdl.K_RETURN, sdl.K_RETURN2:
|
||||
return shared.KeyReturn
|
||||
case sdl.K_TAB:
|
||||
result = 9
|
||||
return shared.KeyTab
|
||||
case sdl.K_DELETE:
|
||||
result = 127 // 24 in the Base64A
|
||||
return shared.KeyDelete
|
||||
case sdl.K_LEFT:
|
||||
return shared.KeyLeft
|
||||
case sdl.K_RIGHT:
|
||||
return shared.KeyRight
|
||||
case sdl.K_UP:
|
||||
return shared.KeyUp
|
||||
case sdl.K_DOWN:
|
||||
return shared.KeyDown
|
||||
|
||||
// Base64A clone particularities
|
||||
case sdl.K_F3:
|
||||
result = 127 // Base64A
|
||||
|
||||
// Control of the emulator
|
||||
case sdl.K_F1:
|
||||
k.showHelp = !k.showHelp
|
||||
return shared.KeyF1
|
||||
case sdl.K_F2:
|
||||
// While the help is shown, F2 alone triggers a reset. This is handy
|
||||
// when the window manager (for example KDE) captures Ctrl-F2.
|
||||
if ctrl || k.showHelp {
|
||||
k.a.SendCommand(izapple2.CommandReset)
|
||||
k.showHelp = false
|
||||
}
|
||||
return shared.KeyF2
|
||||
case sdl.K_F3:
|
||||
return shared.KeyF3
|
||||
case sdl.K_F4:
|
||||
k.a.SendCommand(izapple2.CommandToggleCPUTrace)
|
||||
return shared.KeyF4
|
||||
case sdl.K_F5:
|
||||
if ctrl {
|
||||
k.a.SendCommand(izapple2.CommandShowSpeed)
|
||||
} else {
|
||||
k.a.SendCommand(izapple2.CommandToggleSpeed)
|
||||
}
|
||||
return shared.KeyF5
|
||||
case sdl.K_F6:
|
||||
k.screenMode = screen.NextScreenMode(k.screenMode)
|
||||
return shared.KeyF6
|
||||
case sdl.K_F7:
|
||||
k.showPages = !k.showPages
|
||||
return shared.KeyF7
|
||||
case sdl.K_F8:
|
||||
k.showDropTargets = !k.showDropTargets
|
||||
if k.showDropTargets {
|
||||
// The help is shown on top of the drop targets, get it out of the way
|
||||
k.showHelp = false
|
||||
}
|
||||
return shared.KeyF8
|
||||
case sdl.K_F9:
|
||||
k.a.SendCommand(izapple2.CommandDumpDebugInfo)
|
||||
return shared.KeyF9
|
||||
case sdl.K_F10:
|
||||
if ctrl {
|
||||
k.showCharGen = !k.showCharGen
|
||||
} else if shift {
|
||||
k.showAltText = !k.showAltText
|
||||
} else {
|
||||
k.a.SendCommand(izapple2.CommandNextCharGenPage)
|
||||
}
|
||||
return shared.KeyF10
|
||||
case sdl.K_F12:
|
||||
fallthrough
|
||||
case sdl.K_PRINTSCREEN:
|
||||
if ctrl {
|
||||
screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/")
|
||||
} else {
|
||||
err := screen.SaveSnapshot(k.a.GetVideoSource(), screen.ScreenModeColorScanlines, "snapshot.png")
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving snapshoot: %v.\n.", err)
|
||||
} else {
|
||||
fmt.Println("Saving snapshot 'snapshot.png'")
|
||||
}
|
||||
}
|
||||
return shared.KeyF12
|
||||
case sdl.K_PAUSE:
|
||||
k.a.SendCommand(izapple2.CommandPauseUnpause)
|
||||
case sdl.K_INSERT:
|
||||
if shift {
|
||||
text, _ := sdl.GetClipboardText()
|
||||
go k.performPaste(text)
|
||||
}
|
||||
case 'v':
|
||||
if command {
|
||||
text, _ := sdl.GetClipboardText()
|
||||
go k.performPaste(text)
|
||||
}
|
||||
return shared.KeyPause
|
||||
case sdl.K_PRINTSCREEN:
|
||||
return shared.KeyPrintScreen
|
||||
}
|
||||
|
||||
// Missing values 91 to 95. Usually control for [\]^_
|
||||
// On the Base64A it's control for \]./
|
||||
|
||||
if result != 0 {
|
||||
k.keyChannel.PutChar(result)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *sdl3Keyboard) performPaste(text string) {
|
||||
// Note 1: Pasting is too fast, so we slow it down.
|
||||
// Note 2: Need to translate CR/LF's
|
||||
for _, ch := range text {
|
||||
if ch == '\r' || ch == '\n' {
|
||||
k.keyChannel.PutChar(13)
|
||||
} else {
|
||||
k.keyChannel.PutRune(ch)
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
return shared.KeyNone
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
//go:build !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/Zyko0/go-sdl3/sdl"
|
||||
)
|
||||
|
||||
type sdl3Mouse struct {
|
||||
x uint16
|
||||
y uint16
|
||||
pressed bool
|
||||
}
|
||||
|
||||
func newSDL3Mouse() *sdl3Mouse {
|
||||
var m sdl3Mouse
|
||||
return &m
|
||||
}
|
||||
|
||||
func (m *sdl3Mouse) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, height int32) {
|
||||
m.x = uint16(65536 * int32(e.X) / width)
|
||||
m.y = uint16(65536 * int32(e.Y) / height)
|
||||
}
|
||||
|
||||
func (m *sdl3Mouse) putMouseButtonEvent(e *sdl.MouseButtonEvent) {
|
||||
if e.Button == 1 { // BUTTTON_LEFT
|
||||
m.pressed = e.Down
|
||||
}
|
||||
}
|
||||
|
||||
func (m *sdl3Mouse) ReadMouse() (x uint16, y uint16, pressed bool) {
|
||||
return m.x, m.y, m.pressed
|
||||
}
|
||||
|
||||
// TODO: SDL_WarpMouseInWindow
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/frontend/shared"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
@@ -53,77 +54,58 @@ func (k *wasmKeyboard) putChar(ch uint8) {
|
||||
}
|
||||
|
||||
func (k *wasmKeyboard) putKey(key ebiten.Key) {
|
||||
/*
|
||||
See "Apple II reference manual", page 5
|
||||
|
||||
To get keys as understood by the Apple2 hardware run:
|
||||
10 A=PEEK(49152)
|
||||
20 PRINT A, A - 128
|
||||
30 GOTO 10
|
||||
*/
|
||||
|
||||
ctrl := ebiten.IsKeyPressed(ebiten.KeyControl)
|
||||
//#shift := ebiten.IsKeyPressed(ebiten.KeyShift)
|
||||
|
||||
if ctrl {
|
||||
if key >= ebiten.KeyA && key <= ebiten.KeyZ {
|
||||
if ctrl && key >= ebiten.KeyA && key <= ebiten.KeyZ {
|
||||
if char, ok := shared.CharForCtrlLetter('a' + rune(key-ebiten.KeyA)); ok {
|
||||
fmt.Println("Control Key: ", key.String())
|
||||
k.keyChannel.PutChar(uint8(key-ebiten.KeyA) - 97 + 1)
|
||||
return
|
||||
k.keyChannel.PutChar(char)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
result := uint8(0)
|
||||
if char, ok := shared.CharForKey(wasmKey(key), ctrl); ok {
|
||||
k.keyChannel.PutChar(char)
|
||||
return
|
||||
}
|
||||
|
||||
// Control of the emulator. The page has its own controls for the rest.
|
||||
switch key {
|
||||
case ebiten.KeyEscape:
|
||||
result = 27
|
||||
case ebiten.KeyBackspace:
|
||||
result = 8
|
||||
case ebiten.KeyEnter:
|
||||
result = 13
|
||||
case ebiten.KeyNumpadEnter:
|
||||
result = 13
|
||||
case ebiten.KeyLeft:
|
||||
if ctrl {
|
||||
result = 31 // Base64A
|
||||
} else {
|
||||
result = 8
|
||||
}
|
||||
case ebiten.KeyRight:
|
||||
result = 21
|
||||
|
||||
// Apple //e
|
||||
case ebiten.KeyUp:
|
||||
result = 11 // 31 in the Base64A
|
||||
case ebiten.KeyDown:
|
||||
result = 10
|
||||
case ebiten.KeyTab:
|
||||
result = 9
|
||||
case ebiten.KeyDelete:
|
||||
result = 127 // 24 in the Base64A
|
||||
|
||||
// Base64A clone particularities
|
||||
case ebiten.KeyF3:
|
||||
result = 127 // Base64A
|
||||
|
||||
// Control of the emulator
|
||||
case ebiten.KeyF2:
|
||||
if ctrl {
|
||||
k.a.SendCommand(izapple2.CommandReset)
|
||||
}
|
||||
case ebiten.KeyF9:
|
||||
k.a.SendCommand(izapple2.CommandDumpDebugInfo)
|
||||
case ebiten.KeyF12:
|
||||
fallthrough
|
||||
case ebiten.KeyPause:
|
||||
case ebiten.KeyF12, ebiten.KeyPause:
|
||||
k.a.SendCommand(izapple2.CommandPauseUnpause)
|
||||
}
|
||||
}
|
||||
|
||||
// Missing values 91 to 95. Usually control for [\]^_
|
||||
// On the Base64A it's control for \]./
|
||||
// wasmKey translates a key of Ebitengine
|
||||
func wasmKey(key ebiten.Key) shared.Key {
|
||||
switch key {
|
||||
case ebiten.KeyEscape:
|
||||
return shared.KeyEscape
|
||||
case ebiten.KeyBackspace:
|
||||
return shared.KeyBackspace
|
||||
case ebiten.KeyEnter, ebiten.KeyNumpadEnter:
|
||||
return shared.KeyReturn
|
||||
case ebiten.KeyTab:
|
||||
return shared.KeyTab
|
||||
case ebiten.KeyDelete:
|
||||
return shared.KeyDelete
|
||||
case ebiten.KeyLeft:
|
||||
return shared.KeyLeft
|
||||
case ebiten.KeyRight:
|
||||
return shared.KeyRight
|
||||
case ebiten.KeyUp:
|
||||
return shared.KeyUp
|
||||
case ebiten.KeyDown:
|
||||
return shared.KeyDown
|
||||
case ebiten.KeyF3:
|
||||
return shared.KeyF3
|
||||
}
|
||||
|
||||
if result != 0 {
|
||||
k.keyChannel.PutChar(result)
|
||||
}
|
||||
return shared.KeyNone
|
||||
}
|
||||
@@ -51,17 +51,21 @@ type DropTargets struct {
|
||||
|
||||
flashUntil time.Time // The areas are shown for a while after a drop
|
||||
flashDrive int
|
||||
|
||||
dragging bool // A file is being dragged over the window
|
||||
dragDrive int // The drive under it, -1 while it is not known
|
||||
}
|
||||
|
||||
func NewDropTargets(a *izapple2.Apple2) *DropTargets {
|
||||
var d DropTargets
|
||||
d.a = a
|
||||
d.flashDrive = -1
|
||||
d.dragDrive = -1
|
||||
return &d
|
||||
}
|
||||
|
||||
// Count returns how many drives a file can be dropped on
|
||||
func (d *DropTargets) Count() int {
|
||||
// count returns how many drives a file can be dropped on
|
||||
func (d *DropTargets) count() int {
|
||||
return len(d.a.GetRemovableMediaDrives())
|
||||
}
|
||||
|
||||
@@ -69,7 +73,7 @@ func (d *DropTargets) Count() int {
|
||||
// a window of the given width. It returns -1 if there are no drives to drop a
|
||||
// file on.
|
||||
func (d *DropTargets) DriveAt(x int, width int) int {
|
||||
return DropTargetIndex(x, width, d.Count())
|
||||
return DropTargetIndex(x, width, d.count())
|
||||
}
|
||||
|
||||
// Dropped notes the drive a file has just gone to, to show the areas for a
|
||||
@@ -79,19 +83,64 @@ func (d *DropTargets) Dropped(drive int) {
|
||||
d.flashUntil = time.Now().Add(DropTargetsFlashDuration)
|
||||
}
|
||||
|
||||
// Flashing returns whether the areas are being shown after a drop
|
||||
func (d *DropTargets) Flashing() bool {
|
||||
// DragStarted is called when a file begins to be dragged over the window,
|
||||
// before knowing where it is
|
||||
func (d *DropTargets) DragStarted() {
|
||||
d.dragging = true
|
||||
d.dragDrive = -1
|
||||
}
|
||||
|
||||
// DragMoved tracks the drive a file dragged over the window is on. Pass -1
|
||||
// while it is not known.
|
||||
func (d *DropTargets) DragMoved(drive int) {
|
||||
d.dragging = true
|
||||
d.dragDrive = drive
|
||||
}
|
||||
|
||||
// DragEnded is called when the file is dropped or leaves the window
|
||||
func (d *DropTargets) DragEnded() {
|
||||
d.dragging = false
|
||||
}
|
||||
|
||||
// Showing returns whether the areas take over the screen: because a file is
|
||||
// being dragged over the window, because the user asked for them with F8, or
|
||||
// because a file has just been dropped
|
||||
func (d *DropTargets) Showing(requested bool) bool {
|
||||
return d.dragging || requested || d.flashing()
|
||||
}
|
||||
|
||||
/*
|
||||
SnapshotAt returns the screen with the areas of the drives, highlighting the
|
||||
drive under the file being dragged, or the one that got the last file while the
|
||||
flash lasts, or the one the pointer is on.
|
||||
|
||||
Pass -1 as the drive pointed at where the pointer is outside the window or its
|
||||
position is not known.
|
||||
*/
|
||||
func (d *DropTargets) SnapshotAt(pointed int) *image.RGBA {
|
||||
return d.snapshot(d.selected(pointed))
|
||||
}
|
||||
|
||||
// selected returns the drive the areas highlight, -1 for none of them
|
||||
func (d *DropTargets) selected(pointed int) int {
|
||||
switch {
|
||||
case d.dragging:
|
||||
return d.dragDrive
|
||||
case d.flashing():
|
||||
return d.flashDrive
|
||||
}
|
||||
|
||||
return pointed
|
||||
}
|
||||
|
||||
// flashing returns whether the areas are being shown after a drop
|
||||
func (d *DropTargets) flashing() bool {
|
||||
return time.Now().Before(d.flashUntil)
|
||||
}
|
||||
|
||||
// FlashDrive returns the drive the last file dropped went to
|
||||
func (d *DropTargets) FlashDrive() int {
|
||||
return d.flashDrive
|
||||
}
|
||||
|
||||
// Snapshot returns the screen with the areas of the drives, with the one
|
||||
// snapshot returns the screen with the areas of the drives, with the one
|
||||
// passed highlighted. Pass -1 to highlight none of them.
|
||||
func (d *DropTargets) Snapshot(selected int) *image.RGBA {
|
||||
func (d *DropTargets) snapshot(selected int) *image.RGBA {
|
||||
drives := d.a.GetRemovableMediaDrives()
|
||||
return screen.SnapshotMessageGenerator(d.a.GetVideoSource(),
|
||||
dropTargetsMessage(drives, selected), true /*is80Columns*/)
|
||||
|
||||
@@ -3,6 +3,7 @@ package shared
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
)
|
||||
@@ -35,6 +36,44 @@ func TestDropTargetIndex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDropTargetsDrag follows a file dragged over the window and dropped,
|
||||
// checking when the areas are shown and which drive they highlight
|
||||
func TestDropTargetsDrag(t *testing.T) {
|
||||
d := NewDropTargets(nil)
|
||||
|
||||
steps := []struct {
|
||||
name string
|
||||
do func()
|
||||
|
||||
showing bool // With the areas not asked for
|
||||
selected int // With the pointer on drive 2
|
||||
}{
|
||||
{"at rest", func() {}, false, 2},
|
||||
{"a file enters the window", d.DragStarted, true, -1},
|
||||
{"the file moves over drive 1", func() { d.DragMoved(1) }, true, 1},
|
||||
{"the file is dropped on drive 1", func() { d.Dropped(1); d.DragEnded() }, true, 1},
|
||||
{"the flash is over", func() { d.flashUntil = time.Now() }, false, 2},
|
||||
}
|
||||
|
||||
for _, step := range steps {
|
||||
step.do()
|
||||
|
||||
if actual := d.Showing(false); actual != step.showing {
|
||||
t.Errorf("%v: Showing(false) is %v, expected %v",
|
||||
step.name, actual, step.showing)
|
||||
}
|
||||
if actual := d.selected(2); actual != step.selected {
|
||||
t.Errorf("%v: selected(2) is %v, expected %v",
|
||||
step.name, actual, step.selected)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing is going on by now, only the user asking shows the areas
|
||||
if !d.Showing(true) {
|
||||
t.Error("the areas asked for should be shown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitInLines(t *testing.T) {
|
||||
cases := []struct {
|
||||
text string
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
)
|
||||
|
||||
/*
|
||||
The keyboard of the frontends with a window: what the user types goes to the
|
||||
machine, and the keys that command the emulator act on it and on the view.
|
||||
|
||||
The frontends only translate the keys of their toolkit, they all answer to the
|
||||
same ones. The help screen lists them, see HelpMessage.
|
||||
*/
|
||||
|
||||
// Pasting is too fast for the Apple II, so it is slowed down to this per
|
||||
// character
|
||||
const pasteDelay = 20 * time.Millisecond
|
||||
|
||||
// Keyboard sends what the user types to the machine and runs the commands the
|
||||
// keys have on the emulator
|
||||
type Keyboard struct {
|
||||
a *izapple2.Apple2
|
||||
channel *izapple2.KeyboardChannel
|
||||
view *View
|
||||
}
|
||||
|
||||
func NewKeyboard(a *izapple2.Apple2, view *View) *Keyboard {
|
||||
var k Keyboard
|
||||
k.a = a
|
||||
k.channel = izapple2.NewKeyboardChannel(a)
|
||||
k.view = view
|
||||
return &k
|
||||
}
|
||||
|
||||
// PutText sends a text to the machine as the characters it is made of
|
||||
func (k *Keyboard) PutText(text string) {
|
||||
k.channel.PutText(text)
|
||||
}
|
||||
|
||||
// PutChar sends a character to the machine
|
||||
func (k *Keyboard) PutChar(char uint8) {
|
||||
k.channel.PutChar(char)
|
||||
}
|
||||
|
||||
// PutKey acts on a key pressed: it sends the character the Apple II keyboard
|
||||
// would produce for it, or runs the command it has on the emulator
|
||||
func (k *Keyboard) PutKey(key Key, ctrl bool, shift bool) {
|
||||
if char, ok := CharForKey(key, ctrl); ok {
|
||||
k.channel.PutChar(char)
|
||||
return
|
||||
}
|
||||
|
||||
k.command(key, ctrl, shift)
|
||||
}
|
||||
|
||||
// PutCtrlLetter sends a letter pressed with the control key, and returns
|
||||
// whether it was a letter at all
|
||||
func (k *Keyboard) PutCtrlLetter(letter rune) bool {
|
||||
char, ok := CharForCtrlLetter(letter)
|
||||
if ok {
|
||||
k.channel.PutChar(char)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// Paste types a text on the machine. It returns at once, the text is typed on
|
||||
// its own goroutine slowly enough for the Apple II to keep up.
|
||||
func (k *Keyboard) Paste(text string) {
|
||||
go func() {
|
||||
for _, ch := range text {
|
||||
if ch == '\r' || ch == '\n' {
|
||||
// Translate the CR/LFs
|
||||
k.channel.PutChar(13)
|
||||
} else {
|
||||
k.channel.PutRune(ch)
|
||||
}
|
||||
time.Sleep(pasteDelay)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// command runs what a key does to the emulator and to what is shown
|
||||
func (k *Keyboard) command(key Key, ctrl bool, shift bool) {
|
||||
switch key {
|
||||
case KeyF1:
|
||||
k.view.ToggleHelp()
|
||||
case KeyF2:
|
||||
// While the help is shown, F2 alone triggers a reset. This is handy
|
||||
// when the window manager (for example KDE) captures Ctrl-F2.
|
||||
if ctrl || k.view.ShowHelp {
|
||||
k.a.SendCommand(izapple2.CommandReset)
|
||||
k.view.ShowHelp = false
|
||||
}
|
||||
case KeyF4:
|
||||
k.a.SendCommand(izapple2.CommandToggleCPUTrace)
|
||||
case KeyF5:
|
||||
if ctrl {
|
||||
k.a.SendCommand(izapple2.CommandShowSpeed)
|
||||
} else {
|
||||
k.a.SendCommand(izapple2.CommandToggleSpeed)
|
||||
}
|
||||
case KeyF6:
|
||||
k.view.NextScreenMode()
|
||||
case KeyF7:
|
||||
k.view.ShowPages = !k.view.ShowPages
|
||||
case KeyF8:
|
||||
k.view.ToggleDropTargets()
|
||||
case KeyF9:
|
||||
k.a.SendCommand(izapple2.CommandDumpDebugInfo)
|
||||
case KeyF10:
|
||||
if ctrl {
|
||||
k.view.ShowCharGen = !k.view.ShowCharGen
|
||||
} else if shift {
|
||||
k.view.ShowAltText = !k.view.ShowAltText
|
||||
} else {
|
||||
k.a.SendCommand(izapple2.CommandNextCharGenPage)
|
||||
}
|
||||
case KeyF12, KeyPrintScreen:
|
||||
if ctrl {
|
||||
screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/")
|
||||
} else {
|
||||
k.saveSnapshot()
|
||||
}
|
||||
case KeyPause:
|
||||
k.a.SendCommand(izapple2.CommandPauseUnpause)
|
||||
}
|
||||
}
|
||||
|
||||
func (k *Keyboard) saveSnapshot() {
|
||||
err := screen.SaveSnapshot(k.a.GetVideoSource(),
|
||||
screen.ScreenModeColorScanlines, "snapshot.png")
|
||||
if err != nil {
|
||||
fmt.Printf("Error saving snapshoot: %v.\n.", err)
|
||||
} else {
|
||||
fmt.Println("Saving snapshot 'snapshot.png'")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package shared
|
||||
|
||||
/*
|
||||
Every toolkit has its own constants for the keys of the keyboard. The frontends
|
||||
translate theirs to the keys here, and this decides what the Apple II keyboard
|
||||
would produce for them, so that all the frontends type the same.
|
||||
|
||||
See "Apple II reference manual", page 5.
|
||||
|
||||
To get the keys as understood by the Apple II hardware run:
|
||||
|
||||
10 A=PEEK(49152)
|
||||
20 PRINT A, A - 128
|
||||
30 GOTO 10
|
||||
*/
|
||||
|
||||
// Key is a key of the keyboard, whatever the toolkit of the frontend calls it
|
||||
type Key int
|
||||
|
||||
const (
|
||||
KeyNone Key = iota // Not a key the frontends care about
|
||||
|
||||
KeyEscape
|
||||
KeyBackspace
|
||||
KeyReturn
|
||||
KeyTab
|
||||
KeyDelete
|
||||
KeyInsert
|
||||
KeyLeft
|
||||
KeyRight
|
||||
KeyUp
|
||||
KeyDown
|
||||
|
||||
KeyF1
|
||||
KeyF2
|
||||
KeyF3
|
||||
KeyF4
|
||||
KeyF5
|
||||
KeyF6
|
||||
KeyF7
|
||||
KeyF8
|
||||
KeyF9
|
||||
KeyF10
|
||||
KeyF12
|
||||
KeyPause
|
||||
KeyPrintScreen
|
||||
)
|
||||
|
||||
// CharForKey returns the character the Apple II keyboard produces for a key,
|
||||
// and whether it produces one at all
|
||||
func CharForKey(key Key, ctrl bool) (uint8, bool) {
|
||||
switch key {
|
||||
case KeyEscape:
|
||||
return 27, true
|
||||
case KeyBackspace:
|
||||
return 8, true
|
||||
case KeyReturn:
|
||||
return 13, true
|
||||
case KeyLeft:
|
||||
if ctrl {
|
||||
return 31, true // Base64A
|
||||
}
|
||||
return 8, true
|
||||
case KeyRight:
|
||||
return 21, true
|
||||
|
||||
// Apple //e
|
||||
case KeyUp:
|
||||
return 11, true // 31 in the Base64A
|
||||
case KeyDown:
|
||||
return 10, true
|
||||
case KeyTab:
|
||||
return 9, true
|
||||
case KeyDelete:
|
||||
return 127, true // 24 in the Base64A
|
||||
|
||||
// Base64A clone particularities
|
||||
case KeyF3:
|
||||
return 127, true // Base64A
|
||||
}
|
||||
|
||||
// Missing values 91 to 95. Usually control for [\]^_
|
||||
// On the Base64A it's control for \]./
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// CharForCtrlLetter returns the character a letter pressed with the control
|
||||
// key produces, and whether it was a letter at all
|
||||
func CharForCtrlLetter(letter rune) (uint8, bool) {
|
||||
if letter >= 'A' && letter <= 'Z' {
|
||||
letter += 'a' - 'A'
|
||||
}
|
||||
if letter < 'a' || letter > 'z' {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
return uint8(letter-'a') + 1, true
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package shared
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCharForKey(t *testing.T) {
|
||||
cases := []struct {
|
||||
key Key
|
||||
ctrl bool
|
||||
expected uint8
|
||||
is bool
|
||||
}{
|
||||
{KeyEscape, false, 27, true},
|
||||
{KeyBackspace, false, 8, true},
|
||||
{KeyReturn, false, 13, true},
|
||||
{KeyTab, false, 9, true},
|
||||
{KeyDelete, false, 127, true},
|
||||
{KeyLeft, false, 8, true},
|
||||
{KeyLeft, true, 31, true}, // Base64A
|
||||
{KeyRight, false, 21, true},
|
||||
{KeyUp, false, 11, true},
|
||||
{KeyDown, false, 10, true},
|
||||
{KeyF3, false, 127, true}, // Base64A
|
||||
{KeyF1, false, 0, false}, // A command of the emulator, not a character
|
||||
{KeyF12, true, 0, false},
|
||||
{KeyNone, false, 0, false},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual, is := CharForKey(c.key, c.ctrl)
|
||||
if actual != c.expected || is != c.is {
|
||||
t.Errorf("CharForKey(%v, %v) is (%v, %v), expected (%v, %v)",
|
||||
c.key, c.ctrl, actual, is, c.expected, c.is)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCharForCtrlLetter(t *testing.T) {
|
||||
letters := map[rune]uint8{
|
||||
'a': 1, 'm': 13, 'z': 26,
|
||||
'A': 1, 'M': 13, 'Z': 26, // The upper case letters are the same keys
|
||||
}
|
||||
|
||||
for letter, expected := range letters {
|
||||
actual, is := CharForCtrlLetter(letter)
|
||||
if actual != expected || !is {
|
||||
t.Errorf("CharForCtrlLetter(%q) is (%v, %v), expected (%v, true)",
|
||||
letter, actual, is, expected)
|
||||
}
|
||||
}
|
||||
|
||||
for _, letter := range []rune{'0', ' ', '[', '@', 'ñ'} {
|
||||
if _, is := CharForCtrlLetter(letter); is {
|
||||
t.Errorf("CharForCtrlLetter(%q) should not be a letter", letter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/audio"
|
||||
)
|
||||
|
||||
// NewMixer creates the mixer of a machine with every sound source of it
|
||||
// attached, ready for the frontend to send the samples to its audio device
|
||||
func NewMixer(a *izapple2.Apple2) *audio.Mixer {
|
||||
mixer := audio.NewMixer(a.GetClockMhz())
|
||||
for _, source := range a.GetAudioSources() {
|
||||
source.SetAudioSink(mixer.NewSource())
|
||||
}
|
||||
|
||||
return mixer
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package shared
|
||||
|
||||
// Mouse is the position and the button of the pointer, as the mouse card of
|
||||
// the machine reads them
|
||||
type Mouse struct {
|
||||
x uint16
|
||||
y uint16
|
||||
pressed bool
|
||||
}
|
||||
|
||||
func NewMouse() *Mouse {
|
||||
var m Mouse
|
||||
return &m
|
||||
}
|
||||
|
||||
// SetPosition places the pointer, from where it is on a window of the given
|
||||
// size
|
||||
func (m *Mouse) SetPosition(x int, y int, width int, height int) {
|
||||
if width <= 0 || height <= 0 {
|
||||
// The window is not sized yet
|
||||
return
|
||||
}
|
||||
|
||||
m.x = uint16(65536 * x / width)
|
||||
m.y = uint16(65536 * y / height)
|
||||
}
|
||||
|
||||
// SetButton presses or releases the button of the mouse
|
||||
func (m *Mouse) SetButton(pressed bool) {
|
||||
m.pressed = pressed
|
||||
}
|
||||
|
||||
// ReadMouse returns the position of the pointer on the window and whether its
|
||||
// button is pressed
|
||||
func (m *Mouse) ReadMouse() (x uint16, y uint16, pressed bool) {
|
||||
return m.x, m.y, m.pressed
|
||||
}
|
||||
|
||||
// TODO: SDL_WarpMouseInWindow
|
||||
@@ -0,0 +1,25 @@
|
||||
package shared
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMouse(t *testing.T) {
|
||||
m := NewMouse()
|
||||
|
||||
m.SetPosition(320, 120, 640, 480)
|
||||
x, y, pressed := m.ReadMouse()
|
||||
if x != 32768 || y != 16384 || pressed {
|
||||
t.Errorf("the mouse is at (%v, %v, %v), expected (32768, 16384, false)",
|
||||
x, y, pressed)
|
||||
}
|
||||
|
||||
m.SetButton(true)
|
||||
if _, _, pressed := m.ReadMouse(); !pressed {
|
||||
t.Error("the button of the mouse should be pressed")
|
||||
}
|
||||
|
||||
// A window not sized yet leaves the pointer where it was
|
||||
m.SetPosition(10, 10, 0, 0)
|
||||
if x, y, _ := m.ReadMouse(); x != 32768 || y != 16384 {
|
||||
t.Errorf("the mouse moved to (%v, %v) on a window without size", x, y)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package shared
|
||||
|
||||
/*
|
||||
The Apple II supports four paddles and 3 pushbuttons. The first two paddles are
|
||||
the X, Y axis of the first joystick. The second two correspond to the second
|
||||
joystick.
|
||||
|
||||
Button 0 is the primary button of joystick 0.
|
||||
Button 1 is the secondary button of joystick 0 but also the primary button
|
||||
of joystick 1.
|
||||
Button 2 is the secondary button of joystick 1.
|
||||
|
||||
The Apple //e reads the open and closed apple keys as the first two buttons.
|
||||
Where there is no joystick, the mouse can move the first two paddles.
|
||||
*/
|
||||
|
||||
// The buttons of the mouse, when it is what moves the paddles
|
||||
const (
|
||||
MouseButtonLeft = iota
|
||||
MouseButtonRight
|
||||
MouseButtonMiddle
|
||||
)
|
||||
|
||||
const (
|
||||
paddleUnplugged = 255 // Max resistance when unplugged
|
||||
paddleCentered = 127
|
||||
)
|
||||
|
||||
// Paddles builds the paddles and buttons the machine reads from the events of
|
||||
// a frontend
|
||||
type Paddles struct {
|
||||
paddle [4]uint8
|
||||
hasPaddle [4]bool
|
||||
button [4]bool
|
||||
appleKeys [2]bool // The open and the closed apple keys
|
||||
mouseButtons [3]bool
|
||||
useMouse bool
|
||||
}
|
||||
|
||||
/*
|
||||
NewPaddles creates the paddles of a machine with the given number of joysticks,
|
||||
of which only the first two are used.
|
||||
|
||||
Without joysticks, useMouseAlt moves the first two paddles with the mouse.
|
||||
*/
|
||||
func NewPaddles(joysticks int, useMouseAlt bool) *Paddles {
|
||||
var p Paddles
|
||||
|
||||
for i := range p.paddle {
|
||||
p.paddle[i] = paddleUnplugged
|
||||
}
|
||||
for slot := 0; slot < joysticks && slot < 2; slot++ {
|
||||
p.hasPaddle[slot*2] = true
|
||||
p.hasPaddle[slot*2+1] = true
|
||||
}
|
||||
|
||||
if useMouseAlt && !p.hasPaddle[0] {
|
||||
// Use the mouse as joystick
|
||||
p.useMouse = true
|
||||
p.hasPaddle[0] = true
|
||||
p.hasPaddle[1] = true
|
||||
p.paddle[0] = paddleCentered
|
||||
p.paddle[1] = paddleCentered
|
||||
}
|
||||
|
||||
// To enter Apple IIe on self test mode
|
||||
// p.appleKeys[1] = true
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
// SetAxis moves a paddle of a joystick, with the value on the full range of
|
||||
// the axis
|
||||
func (p *Paddles) SetAxis(joystick int, axis int, value int16) {
|
||||
if joystick < 0 || joystick >= 2 || axis < 0 || axis >= 2 {
|
||||
// Only the first two axis of the first two joysticks
|
||||
return
|
||||
}
|
||||
|
||||
p.paddle[joystick*2+axis] = uint8((value >> 8) + 128)
|
||||
}
|
||||
|
||||
// SetButton presses or releases a button of a joystick. The buttons beyond the
|
||||
// second are read as the second one.
|
||||
func (p *Paddles) SetButton(joystick int, button int, pressed bool) {
|
||||
if joystick < 0 || joystick >= 2 || button < 0 {
|
||||
// Only the buttons of the first two joysticks
|
||||
return
|
||||
}
|
||||
|
||||
p.button[joystick*2+(button%2)] = pressed
|
||||
}
|
||||
|
||||
// SetOpenApple presses or releases the open apple key, the first button
|
||||
func (p *Paddles) SetOpenApple(pressed bool) {
|
||||
p.appleKeys[0] = pressed
|
||||
}
|
||||
|
||||
// SetClosedApple presses or releases the closed apple key, the second button
|
||||
func (p *Paddles) SetClosedApple(pressed bool) {
|
||||
p.appleKeys[1] = pressed
|
||||
}
|
||||
|
||||
// ReleaseAppleKeys forgets the apple keys being pressed. The key up event of a
|
||||
// key held while the window loses the focus goes to whoever took it, leaving
|
||||
// the apple key pressed forever.
|
||||
func (p *Paddles) ReleaseAppleKeys() {
|
||||
p.appleKeys = [2]bool{}
|
||||
}
|
||||
|
||||
// SetMousePosition moves the paddles with the pointer on a window of the given
|
||||
// size, where the mouse is what the machine has instead of a joystick. The
|
||||
// paddles are centered on the center of the window.
|
||||
func (p *Paddles) SetMousePosition(x int, y int, width int, height int) {
|
||||
if !p.useMouse {
|
||||
return
|
||||
}
|
||||
|
||||
p.paddle[0] = mouseToPaddle(x, width)
|
||||
p.paddle[1] = mouseToPaddle(y, height)
|
||||
}
|
||||
|
||||
// SetMouseButton presses or releases a button of the mouse, where the mouse is
|
||||
// what the machine has instead of a joystick
|
||||
func (p *Paddles) SetMouseButton(button int, pressed bool) {
|
||||
if !p.useMouse || button < 0 || button >= len(p.mouseButtons) {
|
||||
return
|
||||
}
|
||||
|
||||
p.mouseButtons[button] = pressed
|
||||
}
|
||||
|
||||
// ReadButton returns whether a pushbutton is pressed, as the machine reads it
|
||||
func (p *Paddles) ReadButton(i int) bool {
|
||||
switch i {
|
||||
case 0:
|
||||
return p.button[0] || p.appleKeys[0] || p.mouseButtons[MouseButtonLeft]
|
||||
case 1:
|
||||
// It can be secondary of first or primary of second
|
||||
return p.button[1] || p.button[2] || p.appleKeys[1] ||
|
||||
p.mouseButtons[MouseButtonRight]
|
||||
case 2:
|
||||
return p.button[3] || p.mouseButtons[MouseButtonMiddle]
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ReadPaddle returns the resistance of a paddle and whether it is plugged
|
||||
func (p *Paddles) ReadPaddle(i int) (uint8, bool) {
|
||||
return p.paddle[i], p.hasPaddle[i]
|
||||
}
|
||||
|
||||
// mouseToPaddle turns a position on the window into the value of a paddle
|
||||
// centered on the center of the window
|
||||
func mouseToPaddle(x int, width int) uint8 {
|
||||
return uint8(max(min(x-(width/2)+paddleCentered, 255), 0))
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package shared
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestPaddlesUnplugged(t *testing.T) {
|
||||
p := NewPaddles(0, false)
|
||||
|
||||
for i := range 4 {
|
||||
value, plugged := p.ReadPaddle(i)
|
||||
if plugged {
|
||||
t.Errorf("paddle %v should not be plugged", i)
|
||||
}
|
||||
if value != paddleUnplugged {
|
||||
t.Errorf("paddle %v is %v, expected %v", i, value, paddleUnplugged)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range 3 {
|
||||
if p.ReadButton(i) {
|
||||
t.Errorf("button %v should not be pressed", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddlesAxis(t *testing.T) {
|
||||
p := NewPaddles(2, false)
|
||||
|
||||
cases := []struct {
|
||||
joystick int
|
||||
axis int
|
||||
value int16
|
||||
paddle int
|
||||
expected uint8
|
||||
}{
|
||||
{0, 0, -32768, 0, 0}, // Full left
|
||||
{0, 1, 0, 1, 128}, // Centered
|
||||
{1, 0, 32767, 2, 255}, // Full right
|
||||
{1, 1, -32768, 3, 0},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
p.SetAxis(c.joystick, c.axis, c.value)
|
||||
actual, plugged := p.ReadPaddle(c.paddle)
|
||||
if actual != c.expected || !plugged {
|
||||
t.Errorf("SetAxis(%v, %v, %v) leaves paddle %v at (%v, %v), expected (%v, true)",
|
||||
c.joystick, c.axis, c.value, c.paddle, actual, plugged, c.expected)
|
||||
}
|
||||
}
|
||||
|
||||
// The joysticks and axis beyond the ones the Apple II has are ignored
|
||||
p.SetAxis(2, 0, 32767)
|
||||
p.SetAxis(0, 2, 32767)
|
||||
p.SetAxis(-1, 0, 32767)
|
||||
}
|
||||
|
||||
func TestPaddlesButtons(t *testing.T) {
|
||||
cases := []struct {
|
||||
joystick int
|
||||
button int
|
||||
expected int // The button of the machine it is read as
|
||||
}{
|
||||
{0, 0, 0}, // Primary of the first joystick
|
||||
{0, 1, 1}, // Secondary of the first, also primary of the second
|
||||
{1, 0, 1},
|
||||
{1, 1, 2}, // Secondary of the second
|
||||
{0, 2, 0}, // The buttons beyond the second repeat
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
p := NewPaddles(2, false)
|
||||
p.SetButton(c.joystick, c.button, true)
|
||||
|
||||
for i := range 3 {
|
||||
expected := i == c.expected
|
||||
if p.ReadButton(i) != expected {
|
||||
t.Errorf("button %v of joystick %v: button %v of the machine is %v, expected %v",
|
||||
c.button, c.joystick, i, p.ReadButton(i), expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddlesAppleKeys(t *testing.T) {
|
||||
p := NewPaddles(1, false)
|
||||
|
||||
p.SetOpenApple(true)
|
||||
p.SetClosedApple(true)
|
||||
if !p.ReadButton(0) || !p.ReadButton(1) {
|
||||
t.Error("the apple keys should be read as the first two buttons")
|
||||
}
|
||||
|
||||
p.ReleaseAppleKeys()
|
||||
if p.ReadButton(0) || p.ReadButton(1) {
|
||||
t.Error("the apple keys should be released")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddlesMouse(t *testing.T) {
|
||||
p := NewPaddles(0, true)
|
||||
|
||||
// The mouse takes the place of the first joystick, centered
|
||||
value, plugged := p.ReadPaddle(0)
|
||||
if !plugged || value != paddleCentered {
|
||||
t.Errorf("the mouse paddle is (%v, %v), expected (%v, true)",
|
||||
value, plugged, paddleCentered)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
x, y int
|
||||
expected [2]uint8
|
||||
}{
|
||||
{200, 100, [2]uint8{127, 127}}, // The center of the window
|
||||
{0, 0, [2]uint8{0, 27}},
|
||||
{400, 200, [2]uint8{255, 227}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
p.SetMousePosition(c.x, c.y, 400, 200)
|
||||
x, _ := p.ReadPaddle(0)
|
||||
y, _ := p.ReadPaddle(1)
|
||||
if x != c.expected[0] || y != c.expected[1] {
|
||||
t.Errorf("the pointer at %v,%v leaves the paddles at %v,%v, expected %v",
|
||||
c.x, c.y, x, y, c.expected)
|
||||
}
|
||||
}
|
||||
|
||||
p.SetMouseButton(MouseButtonRight, true)
|
||||
if !p.ReadButton(1) {
|
||||
t.Error("the right button should be read as the second button")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaddlesMouseWithJoystick(t *testing.T) {
|
||||
// With a joystick plugged the mouse is left for the machine to use
|
||||
p := NewPaddles(1, true)
|
||||
|
||||
p.SetAxis(0, 0, 0)
|
||||
p.SetMousePosition(0, 0, 400, 200)
|
||||
p.SetMouseButton(MouseButtonLeft, true)
|
||||
|
||||
if value, _ := p.ReadPaddle(0); value != 128 {
|
||||
t.Errorf("the paddle is %v, the mouse should not have moved it", value)
|
||||
}
|
||||
if p.ReadButton(0) {
|
||||
t.Error("the mouse should not press the buttons")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
|
||||
"github.com/ivanizag/izapple2"
|
||||
"github.com/ivanizag/izapple2/screen"
|
||||
)
|
||||
|
||||
/*
|
||||
The frontends with a window show the screen of the machine, and on demand one
|
||||
of the screens that take over it: the help, the areas of the drives a file can
|
||||
be dropped on, the character generator or the pages of video memory. What is
|
||||
shown and what the window is titled is the same on all of them, only the way
|
||||
the image reaches the window changes.
|
||||
*/
|
||||
|
||||
// View is the screen a frontend shows and the choices the user made about it
|
||||
type View struct {
|
||||
ShowHelp bool
|
||||
ShowPages bool
|
||||
ShowCharGen bool
|
||||
ShowAltText bool
|
||||
ShowDropTargets bool
|
||||
ScreenMode int
|
||||
}
|
||||
|
||||
// NewView creates the view a frontend starts with
|
||||
func NewView() *View {
|
||||
var v View
|
||||
v.ScreenMode = screen.ScreenModeColorScanlines
|
||||
return &v
|
||||
}
|
||||
|
||||
// ToggleHelp shows or hides the help
|
||||
func (v *View) ToggleHelp() {
|
||||
v.ShowHelp = !v.ShowHelp
|
||||
}
|
||||
|
||||
// ToggleDropTargets shows or hides the areas of the drives a file can be
|
||||
// dropped on
|
||||
func (v *View) ToggleDropTargets() {
|
||||
v.ShowDropTargets = !v.ShowDropTargets
|
||||
if v.ShowDropTargets {
|
||||
// The help is shown on top of the drop targets, get it out of the way
|
||||
v.ShowHelp = false
|
||||
}
|
||||
}
|
||||
|
||||
// NextScreenMode moves to the next way of rendering the screen
|
||||
func (v *View) NextScreenMode() {
|
||||
v.ScreenMode = screen.NextScreenMode(v.ScreenMode)
|
||||
}
|
||||
|
||||
/*
|
||||
Snapshot returns the image to put on the window and the title it should have,
|
||||
empty when the title does not change.
|
||||
|
||||
The drive the pointer is on highlights the areas of the drop targets while they
|
||||
are shown. Pass -1 where the pointer is outside the window or its position is
|
||||
not known.
|
||||
*/
|
||||
func (v *View) Snapshot(a *izapple2.Apple2, d *DropTargets, pointed int) (*image.RGBA, string) {
|
||||
vs := a.GetVideoSource()
|
||||
|
||||
switch {
|
||||
case v.ShowHelp:
|
||||
return screen.SnapshotMessageGenerator(vs, HelpMessage, false /*is80Columns*/), ""
|
||||
|
||||
case d.Showing(v.ShowDropTargets):
|
||||
return d.SnapshotAt(pointed), ""
|
||||
|
||||
case v.ShowCharGen:
|
||||
cgPage, cgPages := a.GetCgPageInfo()
|
||||
return screen.SnapshotCharacterGenerator(vs, v.ShowAltText),
|
||||
fmt.Sprintf("%v character map, page %v/%v", a.Name, cgPage+1, cgPages)
|
||||
|
||||
case v.ShowPages:
|
||||
img := screen.SnapshotParts(vs, v.ScreenMode)
|
||||
return img, fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(vs),
|
||||
img.Rect.Dx()/2, img.Rect.Dy()/2)
|
||||
}
|
||||
|
||||
return screen.Snapshot(vs, v.ScreenMode), ""
|
||||
}
|
||||
|
||||
// HelpMessage is the text shown with F1, the keys the frontends with a window
|
||||
// have in common
|
||||
const HelpMessage = `
|
||||
F1: Show/Hide help
|
||||
Ctrl-F2: Reset
|
||||
F1, F2: Reset
|
||||
F4: Show/Hide CPU trace
|
||||
F5: Fast/Normal speed
|
||||
Ctrl-F5: Show speed
|
||||
F6: Next screen mode
|
||||
F7: Show/Hide pages
|
||||
F8: Show/Hide drop targets
|
||||
F10: Next character set
|
||||
Ctrl-F10: Show/Hide character set
|
||||
Shift-F10: Show/Hide alternate text
|
||||
F12: Save screen snapshot
|
||||
Pause: Pause the emulation
|
||||
|
||||
Left alt or option key: Open-Apple
|
||||
Right alt or option key: Closed-Apple
|
||||
|
||||
Drop a file on a drive area to load it
|
||||
|
||||
Run izapple2 -h for more options
|
||||
https://github.com/ivanizag/izapple2
|
||||
`
|
||||
@@ -0,0 +1,21 @@
|
||||
package shared
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestViewToggleDropTargets(t *testing.T) {
|
||||
v := NewView()
|
||||
v.ToggleHelp()
|
||||
|
||||
v.ToggleDropTargets()
|
||||
if !v.ShowDropTargets {
|
||||
t.Error("the drop targets should be shown")
|
||||
}
|
||||
if v.ShowHelp {
|
||||
t.Error("the help should be out of the way of the drop targets")
|
||||
}
|
||||
|
||||
v.ToggleDropTargets()
|
||||
if v.ShowDropTargets {
|
||||
t.Error("the drop targets should be hidden")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user