diff --git a/src/Spectron/Assets/Keyboard.png b/src/Spectron/Assets/Keyboard.png index 6ba95de9..ceff378a 100644 Binary files a/src/Spectron/Assets/Keyboard.png and b/src/Spectron/Assets/Keyboard.png differ diff --git a/src/Spectron/Input/KeyboardHandler.cs b/src/Spectron/Input/KeyboardHandler.cs index 49f60704..8cc4e1b4 100644 --- a/src/Spectron/Input/KeyboardHandler.cs +++ b/src/Spectron/Input/KeyboardHandler.cs @@ -8,7 +8,7 @@ namespace OldBit.Spectron.Input; -internal sealed class KeyboardHandler +public sealed class KeyboardHandler { private Key _capsShift = Key.LeftShift; private Key _symbolShift = Key.RightAlt; @@ -44,6 +44,12 @@ internal void UpdateSettings(KeyboardSettings keyboardSettings) _shouldHandleExtendedKeys = keyboardSettings.ShouldHandleExtendedKeys; } + internal void SimulateKeyPress(SpectrumKey key) => + SpectrumKeyPressed?.Invoke(this, new SpectrumKeyEventArgs([key], Key.None, isKeyPressed: true, isSimulated: true)); + + internal void SimulateKeyRelease(SpectrumKey key) => + SpectrumKeyReleased?.Invoke(this, new SpectrumKeyEventArgs([key], Key.None, isKeyPressed: false, isSimulated: true)); + internal static JoystickInput ToJoystickAction(Key key, Key fireKey) => key switch { Key.Left => JoystickInput.Left, diff --git a/src/Spectron/Input/SpectrumKeyEventArgs.cs b/src/Spectron/Input/SpectrumKeyEventArgs.cs index b7ae6c3d..36e31342 100644 --- a/src/Spectron/Input/SpectrumKeyEventArgs.cs +++ b/src/Spectron/Input/SpectrumKeyEventArgs.cs @@ -5,11 +5,13 @@ namespace OldBit.Spectron.Input; -public class SpectrumKeyEventArgs(List keys, Key key, bool isKeyPressed) : EventArgs +public class SpectrumKeyEventArgs(List keys, Key key, bool isKeyPressed, bool isSimulated = false) : EventArgs { public List Keys { get; } = keys; public Key Key { get; } = key; public bool IsKeyPressed { get; } = isKeyPressed; + + public bool IsSimulated { get; } = isSimulated; } diff --git a/src/Spectron/Messages/ShowKeyboardViewMessage.cs b/src/Spectron/Messages/ShowKeyboardViewMessage.cs index d624944c..09b55b10 100644 --- a/src/Spectron/Messages/ShowKeyboardViewMessage.cs +++ b/src/Spectron/Messages/ShowKeyboardViewMessage.cs @@ -1,3 +1,5 @@ +using OldBit.Spectron.ViewModels; + namespace OldBit.Spectron.Messages; -public record ShowKeyboardViewMessage(); \ No newline at end of file +public record ShowKeyboardViewMessage(KeyboardViewModel ViewModel); \ No newline at end of file diff --git a/src/Spectron/ViewModels/KeyboardViewModel.cs b/src/Spectron/ViewModels/KeyboardViewModel.cs new file mode 100644 index 00000000..cfebf29f --- /dev/null +++ b/src/Spectron/ViewModels/KeyboardViewModel.cs @@ -0,0 +1,273 @@ +using System.Threading.Tasks; +using Avalonia; +using OldBit.Spectron.Emulation.Devices.Keyboard; +using OldBit.Spectron.Input; + +namespace OldBit.Spectron.ViewModels; + +public class KeyboardViewModel +{ + private const int Distance = 83; + private const int KeyWidth = 61; + + private readonly int[] _leftOffsets = [40, 80, 104, 146]; + + private readonly (int Min, int Max)[] _rowVerticalBounds = + [ + (17, 103), // 1..0 + (114, 186), // Q..P + (198, 270), // A..Enter + (281, 353) // Caps..Space + ]; + + public required KeyboardHandler KeyboardHandler { get; init; } + + public void DoubleClick(Point point) + { + var row = GetRow(point); + + if (row == -1) + { + return; + } + + var (key, x, y) = GetKey(row, point); + var spectrumKey = MapSpectrumKey(row, key); + + if (spectrumKey == SpectrumKey.None) + { + return; + } + + SimulateKey(spectrumKey, x, y, row); + } + + private void SimulateKey(SpectrumKey spectrumKey, double x, double y, int row) + { + var isGreenCommand = false; + var isRedCommand = false; + var isCapsCommand = false; + var isSymbolCommand = false; + + // Check modifiers needed + if (spectrumKey is not (SpectrumKey.CapsShift or SpectrumKey.SymbolShift or SpectrumKey.Space or SpectrumKey.Enter)) + { + switch (y) + { + case < 12: + isGreenCommand = true; + break; + + case > 12 and < 26 when row == 0: + isCapsCommand = true; + break; + + case > 63 when row > 0: + case > 75 when row == 0: + isRedCommand = true; + break; + + case > 21 and < 38 when x > 33 && row > 0: + case > 51 and < 65 when x > 33 && row == 0: + isSymbolCommand = true; + break; + } + } + + if (isGreenCommand) + { + SimulateGreenCommand(spectrumKey); + } + else if (isRedCommand) + { + SimulateRedCommand(spectrumKey); + } + else if (isCapsCommand) + { + SimulateCapsCommand(spectrumKey); + } + else if (isSymbolCommand) + { + SimulateSymbolCommand(spectrumKey); + } + else + { + Task.Delay(5).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyPress(spectrumKey); + Task.Delay(20).ContinueWith(_ => KeyboardHandler.SimulateKeyRelease(spectrumKey)); + }); + } + } + + private void SimulateGreenCommand(SpectrumKey spectrumKey) + { + KeyboardHandler.SimulateKeyPress(SpectrumKey.SymbolShift); + KeyboardHandler.SimulateKeyPress(SpectrumKey.CapsShift); + + Task.Delay(30).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyRelease(SpectrumKey.SymbolShift); + KeyboardHandler.SimulateKeyRelease(SpectrumKey.CapsShift); + + Task.Delay(30).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyPress(spectrumKey); + Task.Delay(30).ContinueWith(_ => KeyboardHandler.SimulateKeyRelease(spectrumKey)); + }); + }); + } + + private void SimulateRedCommand(SpectrumKey spectrumKey) + { + KeyboardHandler.SimulateKeyPress(SpectrumKey.SymbolShift); + KeyboardHandler.SimulateKeyPress(SpectrumKey.CapsShift); + + Task.Delay(30).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyRelease(SpectrumKey.CapsShift); + + Task.Delay(5).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyPress(spectrumKey); + + Task.Delay(30).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyRelease(SpectrumKey.SymbolShift); + KeyboardHandler.SimulateKeyRelease(spectrumKey); + }); + }); + }); + } + + private void SimulateCapsCommand(SpectrumKey spectrumKey) + { + KeyboardHandler.SimulateKeyPress(SpectrumKey.CapsShift); + + Task.Delay(30).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyPress(spectrumKey); + + Task.Delay(30).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyRelease(SpectrumKey.CapsShift); + KeyboardHandler.SimulateKeyRelease(spectrumKey); + }); + }); + } + + private void SimulateSymbolCommand(SpectrumKey spectrumKey) + { + KeyboardHandler.SimulateKeyPress(SpectrumKey.SymbolShift); + + Task.Delay(30).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyPress(spectrumKey); + + Task.Delay(30).ContinueWith(_ => + { + KeyboardHandler.SimulateKeyRelease(SpectrumKey.SymbolShift); + KeyboardHandler.SimulateKeyRelease(spectrumKey); + }); + }); + } + + private static SpectrumKey MapSpectrumKey(int row, int key) => (row, key) switch + { + (0, 0) => SpectrumKey.D1, + (0, 1) => SpectrumKey.D2, + (0, 2) => SpectrumKey.D3, + (0, 3) => SpectrumKey.D4, + (0, 4) => SpectrumKey.D5, + (0, 5) => SpectrumKey.D6, + (0, 6) => SpectrumKey.D7, + (0, 7) => SpectrumKey.D8, + (0, 8) => SpectrumKey.D9, + (0, 9) => SpectrumKey.D0, + + (1, 0) => SpectrumKey.Q, + (1, 1) => SpectrumKey.W, + (1, 2) => SpectrumKey.E, + (1, 3) => SpectrumKey.R, + (1, 4) => SpectrumKey.T, + (1, 5) => SpectrumKey.Y, + (1, 6) => SpectrumKey.U, + (1, 7) => SpectrumKey.I, + (1, 8) => SpectrumKey.O, + (1, 9) => SpectrumKey.P, + + (2, 0) => SpectrumKey.A, + (2, 1) => SpectrumKey.S, + (2, 2) => SpectrumKey.D, + (2, 3) => SpectrumKey.F, + (2, 4) => SpectrumKey.G, + (2, 5) => SpectrumKey.H, + (2, 6) => SpectrumKey.J, + (2, 7) => SpectrumKey.K, + (2, 8) => SpectrumKey.L, + (2, 9) => SpectrumKey.Enter, + + (3, 0) => SpectrumKey.Z, + (3, 1) => SpectrumKey.X, + (3, 2) => SpectrumKey.C, + (3, 3) => SpectrumKey.V, + (3, 4) => SpectrumKey.B, + (3, 5) => SpectrumKey.N, + (3, 6) => SpectrumKey.M, + (3, 7) => SpectrumKey.SymbolShift, + (3, 8) => SpectrumKey.CapsShift, + (3, 9) => SpectrumKey.Space, + + _ => SpectrumKey.None + }; + + private int GetRow(Point point) + { + for (var row = 0; row < _rowVerticalBounds.Length; row++) + { + if (point.Y >= _rowVerticalBounds[row].Min && point.Y <= _rowVerticalBounds[row].Max) + { + return row; + } + } + + return -1; + } + + private (int Key, double X, double Y) GetKey(int row, Point point) + { + var key = -1; + + if (row == 3) + { + key = point.X switch + { + >= 39 and <= 124 => 8, + >= 810 and <= 910 => 9, + _ => key + }; + } + + var pos = (point.X - _leftOffsets[row]) / Distance; + var mod = (point.X - _leftOffsets[row]) % Distance; + + if (key == -1 && mod is >= 0 and <= KeyWidth) + { + key = (int)pos; + } + + var x = point.X - _leftOffsets[row] - key * Distance; + var y = point.Y - _rowVerticalBounds[row].Min; + + // Caps, Space, Symbol, Enter - no modifiers -> reduced bounds + if (row == 3 && key is 7 or 8 or 9 || row == 2 && key == 9) + { + if (y is < 16 or > 58) + { + key = -1; + } + } + + return (key, x, y); + } +} \ No newline at end of file diff --git a/src/Spectron/ViewModels/MainWindowViewModel.Dialogs.cs b/src/Spectron/ViewModels/MainWindowViewModel.Dialogs.cs index c58142c7..488f4b03 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.Dialogs.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.Dialogs.cs @@ -121,8 +121,9 @@ private async Task OpenTimeMachineWindow() private static void OpenAboutWindow() => WeakReferenceMessenger.Default.Send(new ShowAboutViewMessage()); - private static void ShowKeyboardHelpWindow() => - WeakReferenceMessenger.Default.Send(new ShowKeyboardViewMessage()); + private void ShowKeyboardHelpWindow() => + WeakReferenceMessenger.Default.Send(new ShowKeyboardViewMessage( + new KeyboardViewModel { KeyboardHandler = _keyboardHandler })); private void ShowLogViewWindow() => WeakReferenceMessenger.Default.Send(new ShowLogViewMessage(new LogViewModel(_logStore))); diff --git a/src/Spectron/ViewModels/MainWindowViewModel.Input.cs b/src/Spectron/ViewModels/MainWindowViewModel.Input.cs index 6fdc1068..d656e428 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.Input.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.Input.cs @@ -52,7 +52,7 @@ private void HandleKeyUp(KeyEventArgs e) private void HandleSpectrumKeyPressed(object? sender, SpectrumKeyEventArgs e) { - if (MainWindow?.IsActive != true) + if (MainWindow?.IsActive != true && !e.IsSimulated) { return; } @@ -67,7 +67,7 @@ private void HandleSpectrumKeyPressed(object? sender, SpectrumKeyEventArgs e) private void HandleSpectrumKeyReleased(object? sender, SpectrumKeyEventArgs e) { - if (MainWindow?.IsActive != true || IsPaused) + if (MainWindow?.IsActive != true && !e.IsSimulated || IsPaused) { return; } diff --git a/src/Spectron/ViewModels/MainWindowViewModel.cs b/src/Spectron/ViewModels/MainWindowViewModel.cs index 0d044bb9..12f65250 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.cs @@ -280,7 +280,7 @@ private void TakeScreenshot() private static void ShowAboutView() => OpenAboutWindow(); [RelayCommand] - private static void ShowKeyboardHelpView() => ShowKeyboardHelpWindow(); + private void ShowKeyboardHelpView() => ShowKeyboardHelpWindow(); [RelayCommand] private void ShowLogView() => ShowLogViewWindow(); diff --git a/src/Spectron/Views/HelpKeyboardView.axaml.cs b/src/Spectron/Views/HelpKeyboardView.axaml.cs deleted file mode 100644 index 3b7c3682..00000000 --- a/src/Spectron/Views/HelpKeyboardView.axaml.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Input; - -namespace OldBit.Spectron.Views; - -public partial class HelpKeyboardView : Window -{ - public HelpKeyboardView() - { - InitializeComponent(); - } - - protected override void OnPointerPressed(PointerPressedEventArgs e) - { - if (!e.Pointer.IsPrimary) - { - return; - } - - BeginMoveDrag(e); - e.Handled = true; - } - - protected override void OnKeyDown(KeyEventArgs e) - { - if (e.Key != Key.Escape && e.Key != Key.F1) - { - return; - } - - Close(); - e.Handled = true; - } -} \ No newline at end of file diff --git a/src/Spectron/Views/HelpKeyboardView.axaml b/src/Spectron/Views/KeyboardView.axaml similarity index 80% rename from src/Spectron/Views/HelpKeyboardView.axaml rename to src/Spectron/Views/KeyboardView.axaml index ee3c163c..f81b3c7b 100644 --- a/src/Spectron/Views/HelpKeyboardView.axaml +++ b/src/Spectron/Views/KeyboardView.axaml @@ -2,15 +2,19 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:viewModels="clr-namespace:OldBit.Spectron.ViewModels" mc:Ignorable="d" d:DesignWidth="472" d:DesignHeight="200" - x:Class="OldBit.Spectron.Views.HelpKeyboardView" + x:Class="OldBit.Spectron.Views.KeyboardView" + x:DataType="viewModels:KeyboardViewModel" Background="Transparent" Width="945" Height="400" Focusable="True" WindowStartupLocation="CenterOwner" SystemDecorations="BorderOnly" - Title="ZX Spectrum Keyboard"> + Title="ZX Spectrum Keyboard" + Name="Window"> + InitializeComponent(); + + protected override void OnDataContextChanged(EventArgs e) + { + if (DataContext is not KeyboardViewModel viewModel) + { + return; + } + + _viewModel = viewModel; + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (e.ClickCount > 1) + { + var point = e.GetPosition(this); + _viewModel?.DoubleClick(point); + + return; + } + + BeginMoveDrag(e); + e.Handled = true; + } + + protected override void OnKeyDown(KeyEventArgs e) + { + if (e.Key != Key.Escape && e.Key != Key.F1) + { + return; + } + + Close(); + e.Handled = true; + } +} \ No newline at end of file diff --git a/src/Spectron/Views/MainWindow.axaml.cs b/src/Spectron/Views/MainWindow.axaml.cs index 565b6cf5..ccdb811b 100644 --- a/src/Spectron/Views/MainWindow.axaml.cs +++ b/src/Spectron/Views/MainWindow.axaml.cs @@ -33,8 +33,8 @@ public MainWindow() WeakReferenceMessenger.Default.Register(this, (window, m) => Show(window, m.ViewModel!)); - WeakReferenceMessenger.Default.Register(this, (window, _) => - Show(window)); + WeakReferenceMessenger.Default.Register(this, (window, m) => + Show(window, m.ViewModel)); WeakReferenceMessenger.Default.Register(this, (_, m) => Show(null, m.ViewModel)); @@ -117,7 +117,7 @@ public MainWindow() if (_windows.TryGetValue(viewType, out var window)) { - if (viewType == nameof(HelpKeyboardView)) + if (viewType == nameof(KeyboardView)) { window.Close(); }