From 1cd8b08c690fd66eed2e6247afe2b5623bda719e Mon Sep 17 00:00:00 2001 From: Christer C Date: Fri, 6 Sep 2024 23:20:38 +0200 Subject: [PATCH] Add clipboard support and paste functionality (#112) Integrated the `TextCopy` library to enable clipboard operations across multiple projects. Added a "Paste" button to the `C64MenuConsole`, `SilkNetImGuiMenu`, and Blazor-based `C64Menu.razor` components to allow pasting text into the C64 emulator. --- .../ConfigUI/C64MenuConsole.cs | 32 ++++++++-- .../Highbyte.DotNet6502.App.SadConsole.csproj | 1 + .../SadConsoleHostApp.cs | 9 ++- ...ghbyte.DotNet6502.App.SilkNetNative.csproj | 1 + .../SilkNetImGuiMenu.cs | 14 ++++ .../Highbyte.DotNet6502.App.WASM.csproj | 1 + .../Pages/Commodore64/C64Menu.razor | 31 ++++++--- .../Pages/Index.razor.cs | 2 +- .../Highbyte.DotNet6502.App.WASM/Program.cs | 2 + .../C64.cs | 14 ++-- .../TimerAndPeripheral/C64Keyboard.cs | 8 ++- .../Utils/C64TextPaste.cs | 64 +++++++++++++++++++ 12 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Utils/C64TextPaste.cs diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs index 47417266..cb52737e 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/ConfigUI/C64MenuConsole.cs @@ -5,6 +5,7 @@ using SadRogue.Primitives; using Microsoft.Extensions.Logging; using Highbyte.DotNet6502.Utils; +using TextCopy; namespace Highbyte.DotNet6502.App.SadConsole.ConfigUI; public class C64MenuConsole : ControlsConsole @@ -60,16 +61,27 @@ private void DrawUIItems() Controls.Add(c64SaveBasicButton); + // Save Basic + var c64PasteTextButton = new Button("Paste") + { + Name = "c64PasteTextButton", + Position = (1, c64SaveBasicButton.Bounds.MaxExtentY + 2), + }; + c64PasteTextButton.Click += C64PasteTextButton_Click; + Controls.Add(c64PasteTextButton); + + // Config var c64ConfigButton = new Button("C64 Config") { Name = "c64ConfigButton", - Position = (1, c64SaveBasicButton.Bounds.MaxExtentY + 2), + Position = (1, c64PasteTextButton.Bounds.MaxExtentY + 2), }; c64ConfigButton.Click += C64ConfigButton_Click; Controls.Add(c64ConfigButton); - var validationMessageValueLabel = CreateLabelValue(new string(' ', 20), 1, c64ConfigButton.Bounds.MaxExtentY + 2, "validationMessageValueLabel"); + + var validationMessageValueLabel = CreateLabelValue(new string(' ', 20), 1, c64PasteTextButton.Bounds.MaxExtentY + 2, "validationMessageValueLabel"); validationMessageValueLabel.TextColor = Controls.GetThemeColors().Red; // Helper function to create a label and add it to the console @@ -201,6 +213,15 @@ private void C64ConfigButton_Click(object sender, EventArgs e) window.Show(true); } + private void C64PasteTextButton_Click(object sender, EventArgs e) + { + var c64 = (C64)_sadConsoleHostApp.CurrentRunningSystem!; + var text = ClipboardService.GetText(); + if (string.IsNullOrEmpty(text)) + return; + c64.TextPaste.Paste(text); + } + protected override void OnIsDirtyChanged() { if (IsDirty) @@ -215,8 +236,11 @@ private void SetControlStates() var c64SaveBasicButton = Controls["c64SaveBasicButton"]; c64SaveBasicButton.IsEnabled = _sadConsoleHostApp.EmulatorState != Systems.EmulatorState.Uninitialized; - var systemComboBox = Controls["c64ConfigButton"]; - systemComboBox.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Uninitialized; + var c64ConfigButton = Controls["c64ConfigButton"]; + c64ConfigButton.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Uninitialized; + + var c64PasteTextButton = Controls["c64PasteTextButton"]; + c64PasteTextButton.IsEnabled = _sadConsoleHostApp.EmulatorState == Systems.EmulatorState.Running; var validationMessageValueLabel = Controls["validationMessageValueLabel"] as Label; (var isOk, var validationErrors) = _sadConsoleHostApp.IsValidConfigWithDetails().Result; diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj index 522ea780..03cb8c71 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/Highbyte.DotNet6502.App.SadConsole.csproj @@ -34,6 +34,7 @@ + diff --git a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs index b3b2a9b0..6a06f579 100644 --- a/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs +++ b/src/apps/Highbyte.DotNet6502.App.SadConsole/SadConsoleHostApp.cs @@ -279,7 +279,8 @@ public override void OnAfterStart(EmulatorState emulatorStateBeforeStart) { _monitorConsole.Init(); } - _sadConsoleEmulatorConsole.IsFocused = true; + + SetEmulatorConsoleFocus(); if (_infoConsole.IsVisible) { @@ -557,6 +558,12 @@ public void SetVolumePercent(float volumePercent) _audioHandlerContext.SetMasterVolumePercent(masterVolumePercent: volumePercent); } + public void SetEmulatorConsoleFocus() + { + if (_sadConsoleEmulatorConsole != null) + _sadConsoleEmulatorConsole.IsFocused = true; + } + private void HandleUIKeyboardInput() { var keyboard = GameHost.Instance.Keyboard; diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj index 020c99f1..67b95b59 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/Highbyte.DotNet6502.App.SilkNetNative.csproj @@ -34,6 +34,7 @@ + diff --git a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs index 6dc2a573..e26e8fc1 100644 --- a/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs +++ b/src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs @@ -8,6 +8,7 @@ using Highbyte.DotNet6502.Utils; using Microsoft.Extensions.Logging; using NativeFileDialogSharp; +using TextCopy; namespace Highbyte.DotNet6502.App.SilkNetNative; @@ -443,6 +444,19 @@ private void DrawC64Config() } ImGui.EndDisabled(); + // C64 paste text + ImGui.BeginDisabled(disabled: EmulatorState == EmulatorState.Uninitialized); + if (ImGui.Button("Paste")) + { + var c64 = (C64)_silkNetHostApp.CurrentRunningSystem!; + var text = ClipboardService.GetText(); + if (string.IsNullOrEmpty(text)) + return; + c64.TextPaste.Paste(text); + + } + ImGui.EndDisabled(); + // C64 config ImGui.BeginDisabled(disabled: !(EmulatorState == EmulatorState.Uninitialized)); if (_c64ConfigUI == null) diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj b/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj index 1d8bb3ad..dd8aff23 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Highbyte.DotNet6502.App.WASM.csproj @@ -37,6 +37,7 @@ + diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor index 6402d613..87bb49f7 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Commodore64/C64Menu.razor @@ -5,6 +5,7 @@ @using static Highbyte.DotNet6502.App.WASM.Pages.Index; @using Highbyte.DotNet6502.App.WASM.Emulator.SystemSetup; @using Highbyte.DotNet6502.Utils; +@using TextCopy @if(Parent.Initialized && Parent.WasmHost.SelectedSystemName == SYSTEM_NAME) { @@ -54,6 +55,10 @@ + +

Misc.

+ +
@@ -93,6 +98,7 @@ @inject IJSRuntime Js @inject HttpClient HttpClient @inject ILoggerFactory LoggerFactory + @inject IClipboard Clipboard [Parameter] public Highbyte.DotNet6502.App.WASM.Pages.Index Parent { get; set; } = default!; @@ -208,6 +214,8 @@ protected bool OnBasicFilePickerDisabled => Parent.CurrentEmulatorState == EmulatorState.Uninitialized; + protected bool OnPasteTextDisabled => Parent.CurrentEmulatorState == EmulatorState.Uninitialized; + /// /// Open Load binary file dialog /// @@ -447,14 +455,9 @@ c64.InitBasicMemoryVariables(loadedAtAddress, fileLength); } - // Send "list" + Enter to the keyboard buffer to immediately list the loaded program - var c64Keyboard = c64.Cia.Keyboard; - // Bypass keyboard matrix scanning and send directly to keyboard buffer? - c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii['l']); - c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii['i']); - c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii['s']); - c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii['t']); - c64Keyboard.InsertPetsciiCharIntoBuffer(Petscii.CharToPetscii[(char)13]); + // Send "list" + NewLine (Return) to the keyboard buffer to immediately list the loaded program. + // Bypass keyboard matrix scanning and send directly to keyboard buffer. + c64.TextPaste.Paste("list\n"); } catch (Exception ex) @@ -465,4 +468,16 @@ await Parent.OnStart(new()); } + + + private async Task PasteText() + { + var c64 = (C64)Parent.WasmHost.CurrentRunningSystem!; + var text = await Clipboard.GetTextAsync(); + if (string.IsNullOrEmpty(text)) + return; + c64.TextPaste.Paste(text); + + await Parent.FocusEmulator(); + } } \ No newline at end of file diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs index 29bc78a6..4b72e223 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs @@ -640,7 +640,7 @@ private void OnKeyUpMonitor(KeyboardEventArgs e) _wasmHost.Monitor.OnKeyUp(e); } - private async Task FocusEmulator() + public async Task FocusEmulator() { await Js!.InvokeVoidAsync("focusId", "emulatorSKGLView", 100); // Hack: Delay of x ms for focus to work. } diff --git a/src/apps/Highbyte.DotNet6502.App.WASM/Program.cs b/src/apps/Highbyte.DotNet6502.App.WASM/Program.cs index 4c4ebcae..0980a55e 100644 --- a/src/apps/Highbyte.DotNet6502.App.WASM/Program.cs +++ b/src/apps/Highbyte.DotNet6502.App.WASM/Program.cs @@ -4,6 +4,7 @@ using Blazored.LocalStorage; using Toolbelt.Blazor.Extensions.DependencyInjection; using Highbyte.DotNet6502.Systems.Logging.Console; +using TextCopy; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); @@ -12,6 +13,7 @@ builder.Services.AddBlazoredModal(); builder.Services.AddBlazoredLocalStorage(); builder.Services.AddGamepadList(); +builder.Services.InjectClipboard(); builder.Logging.ClearProviders(); builder.Logging.AddDotNet6502Console(); diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs index 947dae64..597550d5 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs @@ -9,6 +9,7 @@ using Highbyte.DotNet6502.Systems.Instrumentation; using Highbyte.DotNet6502.Systems.Instrumentation.Stats; using Highbyte.DotNet6502.Utils; +using Highbyte.DotNet6502.Systems.Commodore64.Utils; namespace Highbyte.DotNet6502.Systems.Commodore64; @@ -53,9 +54,9 @@ public class C64 : ISystem, ISystemMonitor private readonly ElapsedMillisecondsTimedStatSystem _postInstructionAudioCallbackStat; private readonly ElapsedMillisecondsTimedStatSystem _postInstructionVideoCallbackStat; - public bool RememberVic2RegistersPerRasterLine { get; set; } = true; + public C64TextPaste TextPaste { get; private set; } //public static ROM[] ROMS = new ROM[] //{ @@ -100,6 +101,9 @@ public ExecEvaluatorTriggerResult ExecuteOneFrame( _postInstructionAudioCallbackStat.Stop(); // Stop stat (was continiously updated after each instruction) _postInstructionVideoCallbackStat.Stop(); // Stop stat (was continiously updated after each instruction) + // Check if any text should be pasted to the keyboard buffer (pasted text set by host system, and each character insterted to the C64 keyboard buffer one character per frame) + TextPaste.InsertNextCharacterToKeyboardBuffer(); + // Update sprite collision state _spriteCollisionStat.Start(); Vic2.SpriteManager.SetCollitionDetectionStatesAndIRQ(); @@ -160,7 +164,7 @@ public ExecEvaluatorTriggerResult ExecuteOneInstruction( return ExecEvaluatorTriggerResult.NotTriggered; } - private C64(ILogger logger) + private C64(ILogger logger, ILoggerFactory loggerFactory) { _logger = logger; _spriteCollisionStat = Instrumentations.Add($"{StatsCategory}-SpriteCollision", new ElapsedMillisecondsTimedStatSystem(this)); @@ -193,8 +197,8 @@ public static C64 BuildC64(C64Config c64Config, ILoggerFactory loggerFactory) var vic2Model = c64Model.Vic2Models.Single(x => x.Name == c64Config.Vic2Model); - var logger = loggerFactory.CreateLogger(typeof(C64).Name); - var c64 = new C64(logger) + var logger = loggerFactory.CreateLogger(); + var c64 = new C64(logger, loggerFactory) { Model = c64Model, RAM = ram, @@ -220,6 +224,8 @@ public static C64 BuildC64(C64Config c64Config, ILoggerFactory loggerFactory) var mem = c64.CreateC64Memory(ram, io, romData); c64.Mem = mem; + c64.TextPaste = new C64TextPaste(c64, loggerFactory); + // Configure the current memory configuration on startup SetStartupBank(c64); diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/TimerAndPeripheral/C64Keyboard.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/TimerAndPeripheral/C64Keyboard.cs index 9a3647b9..d04cc328 100644 --- a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/TimerAndPeripheral/C64Keyboard.cs +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/TimerAndPeripheral/C64Keyboard.cs @@ -148,17 +148,21 @@ public byte GetPressedKeysForSelectedMatrixRow() /// /// Inserts a PETSCII character directly into the keyboard buffer, bypassing keyboard matrix. /// Can be useful when wanting to type Basic commands on behalf of the user. + /// + /// Returns true if the character was inserted into the buffer, false if the buffer is full (and character couldn't be inserted). /// /// - public void InsertPetsciiCharIntoBuffer(byte petsciiChar) + /// + public bool InsertPetsciiCharIntoBuffer(byte petsciiChar) { // Address: 0x00c6: Keyboard buffer index // Address: 0x0277 - 0x0280: Keyboard buffer var bufferIndex = _c64.Mem[0x00c6]; if (bufferIndex >= 10) - return; + return false; _c64.Mem[0x00c6]++; _c64.Mem[(ushort)(0x0277 + bufferIndex)] = petsciiChar; + return true; } /// diff --git a/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Utils/C64TextPaste.cs b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Utils/C64TextPaste.cs new file mode 100644 index 00000000..4507782b --- /dev/null +++ b/src/libraries/Highbyte.DotNet6502.Systems.Commodore64/Utils/C64TextPaste.cs @@ -0,0 +1,64 @@ +using Highbyte.DotNet6502.Systems.Commodore64.Video; +using Microsoft.Extensions.Logging; + +namespace Highbyte.DotNet6502.Systems.Commodore64.Utils; +public class C64TextPaste +{ + private readonly Queue _charQueue = new(); + private readonly ILogger _logger; + private readonly C64 _c64; + + internal bool HasCharactersPending => _charQueue.Count > 0; + + + public C64TextPaste(C64 c64, ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + _c64 = c64; + } + + public void Paste(string text) + { + foreach (char c in text) + _charQueue.Enqueue(c); + } + + internal void InsertNextCharacterToKeyboardBuffer() + { + bool foundChar = _charQueue.TryPeek(out char ansiChar); + if (!foundChar) + return; + + // In Windows, a new line is CRLF (Carrige Return 13 and Line Feed 10) + // In Linux and macOS, a new line is only LF (Line feed 10). + // C64 only uses LF (13) which is "Return" for new line. + // + // Ignore Windows LF (10), and map Line Feed for all systems (10) to C64 Return (13). + if (ansiChar == 13) + { + _charQueue.Dequeue(); + return; + } + + if (ansiChar == 10) + ansiChar = (char)13; + + if (!Petscii.CharToPetscii.ContainsKey(ansiChar)) + { + _charQueue.Dequeue(); + _logger.LogWarning($"'{ansiChar}' has no mapped PetscII char."); + return; + } + + var petsciiChar = Petscii.CharToPetscii[ansiChar]; + var inserted = _c64.Cia.Keyboard.InsertPetsciiCharIntoBuffer(petsciiChar); + if (inserted) + { + _charQueue.Dequeue(); + } + else + { + _logger.LogWarning($"'{ansiChar}' could not be inserted into keyboard buffer."); + } + } +}