Skip to content

Commit

Permalink
Add clipboard support and paste functionality (#112)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
highbyte authored Sep 6, 2024
1 parent 2bd708b commit 1cd8b08
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
<PackageReference Include="MonoGame.Framework.DesktopGL" Version="3.8.1.303" />
<PackageReference Include="SadConsole.Extended" Version="10.4.0" />
<PackageReference Include="TextCopy" Version="6.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ public override void OnAfterStart(EmulatorState emulatorStateBeforeStart)
{
_monitorConsole.Init();
}
_sadConsoleEmulatorConsole.IsFocused = true;

SetEmulatorConsoleFocus();

if (_infoConsole.IsVisible)
{
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<PackageReference Include="Silk.NET.OpenGL" Version="2.21.0" />
<PackageReference Include="Silk.NET.Windowing" Version="2.21.0" />
<PackageReference Include="Silk.NET.OpenGL.Extensions.ImGui" Version="2.21.0" />
<PackageReference Include="TextCopy" Version="6.2.1" />
</ItemGroup>

<ItemGroup>
Expand Down
14 changes: 14 additions & 0 deletions src/apps/Highbyte.DotNet6502.App.SilkNetNative/SilkNetImGuiMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Highbyte.DotNet6502.Utils;
using Microsoft.Extensions.Logging;
using NativeFileDialogSharp;
using TextCopy;

namespace Highbyte.DotNet6502.App.SilkNetNative;

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
<PackageReference Include="SkiaSharp.Views.Blazor" Version="3.0.0-preview.4.1" />
<PackageReference Include="PublishSPAforGitHubPages.Build" Version="2.2.0" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
<PackageReference Include="TextCopy" Version="6.2.1" />
<PackageReference Include="Toolbelt.Blazor.Gamepad" Version="9.0.0" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -54,6 +55,10 @@
</select>
<button @onclick="OnLoadBasicExample" disabled=@OnFilePickerDisabled>Load</button>
</div>

<p>Misc.</p>
<button @onclick="PasteText" disabled=@OnPasteTextDisabled>Paste</button>

</div>

<div class="validation-message">
Expand Down Expand Up @@ -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!;
Expand Down Expand Up @@ -208,6 +214,8 @@

protected bool OnBasicFilePickerDisabled => Parent.CurrentEmulatorState == EmulatorState.Uninitialized;

protected bool OnPasteTextDisabled => Parent.CurrentEmulatorState == EmulatorState.Uninitialized;

/// <summary>
/// Open Load binary file dialog
/// </summary>
Expand Down Expand Up @@ -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)
Expand All @@ -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();
}
}
2 changes: 1 addition & 1 deletion src/apps/Highbyte.DotNet6502.App.WASM/Pages/Index.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down
2 changes: 2 additions & 0 deletions src/apps/Highbyte.DotNet6502.App.WASM/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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>("#app");
Expand All @@ -12,6 +13,7 @@
builder.Services.AddBlazoredModal();
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddGamepadList();
builder.Services.InjectClipboard();

builder.Logging.ClearProviders();
builder.Logging.AddDotNet6502Console();
Expand Down
14 changes: 10 additions & 4 deletions src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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[]
//{
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -160,7 +164,7 @@ public ExecEvaluatorTriggerResult ExecuteOneInstruction(
return ExecEvaluatorTriggerResult.NotTriggered;
}

private C64(ILogger logger)
private C64(ILogger logger, ILoggerFactory loggerFactory)

Check warning on line 167 in src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'TextPaste' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.

Check warning on line 167 in src/libraries/Highbyte.DotNet6502.Systems.Commodore64/C64.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Non-nullable property 'TextPaste' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
{
_logger = logger;
_spriteCollisionStat = Instrumentations.Add($"{StatsCategory}-SpriteCollision", new ElapsedMillisecondsTimedStatSystem(this));
Expand Down Expand Up @@ -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<C64>();
var c64 = new C64(logger, loggerFactory)
{
Model = c64Model,
RAM = ram,
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,17 +148,21 @@ public byte GetPressedKeysForSelectedMatrixRow()
/// <summary>
/// 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).
/// </summary>
/// <param name="petsciiChar"></param>
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;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<char> _charQueue = new();
private readonly ILogger<C64TextPaste> _logger;
private readonly C64 _c64;

internal bool HasCharactersPending => _charQueue.Count > 0;


public C64TextPaste(C64 c64, ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<C64TextPaste>();
_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.");
}
}
}

0 comments on commit 1cd8b08

Please sign in to comment.