From a6602206c51300feddcec1c9d8803a4cb812b2e7 Mon Sep 17 00:00:00 2001 From: Misaka Date: Fri, 15 May 2026 22:09:31 +0800 Subject: [PATCH] Replace focused text from Android source --- .../SyncMessageTests.cs | 55 +++++++++++++++++++ .../Models/SyncAction.cs | 1 + .../Models/SyncActionJsonConverter.cs | 2 + .../Services/IKeyboardInjectionService.cs | 2 + .../Services/KeyboardInjectionService.cs | 46 ++++++++++++++++ .../Services/SyncMessageHandler.cs | 28 +++++++++- 6 files changed, 133 insertions(+), 1 deletion(-) diff --git a/WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs b/WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs index ad5cdb9..3038668 100644 --- a/WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs +++ b/WirelessTextSyncer.Windows.Tests/SyncMessageTests.cs @@ -1,5 +1,6 @@ using System.Text.Json; using WirelessTextSyncer.Windows.Models; +using WirelessTextSyncer.Windows.Services; namespace WirelessTextSyncer.Windows.Tests; @@ -28,4 +29,58 @@ public sealed class SyncMessageTests Assert.IsNotNull(message); Assert.AreEqual(SyncAction.Backspace, message.Action); } + + [TestMethod] + public void DeserializeAndroidReplaceAllMessage() + { + var message = JsonSerializer.Deserialize( + """{"action":"replaceAll","text":"final text"}""", + new JsonSerializerOptions(JsonSerializerDefaults.Web)); + + Assert.IsNotNull(message); + Assert.AreEqual(SyncAction.ReplaceAll, message.Action); + Assert.AreEqual("final text", message.Text); + } + + [TestMethod] + public void ReplaceAllUsesFocusedTextReplacement() + { + var keyboard = new RecordingKeyboardInjectionService(); + var handler = new SyncMessageHandler(keyboard); + + handler.Handle(new SyncMessage + { + Action = SyncAction.ReplaceAll, + Text = "final text" + }); + + CollectionAssert.AreEqual( + new[] { "replace:final text" }, + keyboard.Calls); + } + + private sealed class RecordingKeyboardInjectionService : IKeyboardInjectionService + { + public List Calls { get; } = []; + + public void InsertText(string text) + { + Calls.Add($"insert:{text}"); + } + + public void ReplaceFocusedText(string text) + { + Calls.Add($"replace:{text}"); + } + + public void Backspace() + { + Calls.Add("backspace"); + } + + public void Enter() + { + Calls.Add("enter"); + } + } } diff --git a/WirelessTextSyncer.Windows/Models/SyncAction.cs b/WirelessTextSyncer.Windows/Models/SyncAction.cs index d7f0bee..726b8fd 100644 --- a/WirelessTextSyncer.Windows/Models/SyncAction.cs +++ b/WirelessTextSyncer.Windows/Models/SyncAction.cs @@ -6,6 +6,7 @@ using System.Text.Json.Serialization; public enum SyncAction { InsertText, + ReplaceAll, Backspace, Enter, Ping diff --git a/WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs b/WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs index 7b0712a..5131ddd 100644 --- a/WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs +++ b/WirelessTextSyncer.Windows/Models/SyncActionJsonConverter.cs @@ -15,6 +15,7 @@ public sealed class SyncActionJsonConverter : JsonConverter return value switch { "insertText" => SyncAction.InsertText, + "replaceAll" => SyncAction.ReplaceAll, "backspace" => SyncAction.Backspace, "enter" => SyncAction.Enter, "ping" => SyncAction.Ping, @@ -30,6 +31,7 @@ public sealed class SyncActionJsonConverter : JsonConverter var action = value switch { SyncAction.InsertText => "insertText", + SyncAction.ReplaceAll => "replaceAll", SyncAction.Backspace => "backspace", SyncAction.Enter => "enter", SyncAction.Ping => "ping", diff --git a/WirelessTextSyncer.Windows/Services/IKeyboardInjectionService.cs b/WirelessTextSyncer.Windows/Services/IKeyboardInjectionService.cs index 351a7b7..0b76999 100644 --- a/WirelessTextSyncer.Windows/Services/IKeyboardInjectionService.cs +++ b/WirelessTextSyncer.Windows/Services/IKeyboardInjectionService.cs @@ -4,6 +4,8 @@ public interface IKeyboardInjectionService { void InsertText(string text); + void ReplaceFocusedText(string text); + void Backspace(); void Enter(); diff --git a/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs b/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs index 0759b72..7ea810c 100644 --- a/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs +++ b/WirelessTextSyncer.Windows/Services/KeyboardInjectionService.cs @@ -7,7 +7,9 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService private const uint InputKeyboard = 1; private const uint KeyEventFKeyUp = 0x0002; private const uint KeyEventFUnicode = 0x0004; + private const ushort VirtualKeyA = 0x41; private const ushort VirtualKeyBack = 0x08; + private const ushort VirtualKeyControl = 0x11; private const ushort VirtualKeyReturn = 0x0D; public void InsertText(string text) @@ -24,6 +26,15 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService } } + public void ReplaceFocusedText(string text) + { + AppLogger.Info($"Replacing focused text length: {text.Length}"); + SendModifiedKey(VirtualKeyControl, VirtualKeyA); + Backspace(); + InsertText(text); + AppLogger.Info("Finished replacing focused text."); + } + public void Backspace() { AppLogger.Info("Injecting backspace."); @@ -52,6 +63,16 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService ]); } + private static void SendModifiedKey(ushort modifierKey, ushort key) + { + Send([ + CreateVirtualKeyInput(modifierKey, 0), + CreateVirtualKeyInput(key, 0), + CreateVirtualKeyInput(key, KeyEventFKeyUp), + CreateVirtualKeyInput(modifierKey, KeyEventFKeyUp) + ]); + } + private static void Send(INPUT[] inputs) { var sent = SendInput((uint)inputs.Length, inputs, Marshal.SizeOf()); @@ -108,8 +129,25 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService [StructLayout(LayoutKind.Explicit)] private struct InputUnion { + [FieldOffset(0)] + public MOUSEINPUT mi; + [FieldOffset(0)] public KEYBDINPUT ki; + + [FieldOffset(0)] + public HARDWAREINPUT hi; + } + + [StructLayout(LayoutKind.Sequential)] + private struct MOUSEINPUT + { + public int dx; + public int dy; + public uint mouseData; + public uint dwFlags; + public uint time; + public UIntPtr dwExtraInfo; } [StructLayout(LayoutKind.Sequential)] @@ -121,4 +159,12 @@ public sealed class KeyboardInjectionService : IKeyboardInjectionService public uint time; public UIntPtr dwExtraInfo; } + + [StructLayout(LayoutKind.Sequential)] + private struct HARDWAREINPUT + { + public uint uMsg; + public ushort wParamL; + public ushort wParamH; + } } diff --git a/WirelessTextSyncer.Windows/Services/SyncMessageHandler.cs b/WirelessTextSyncer.Windows/Services/SyncMessageHandler.cs index a5dbf75..3fe575d 100644 --- a/WirelessTextSyncer.Windows/Services/SyncMessageHandler.cs +++ b/WirelessTextSyncer.Windows/Services/SyncMessageHandler.cs @@ -5,6 +5,7 @@ namespace WirelessTextSyncer.Windows.Services; public sealed class SyncMessageHandler { private readonly IKeyboardInjectionService keyboard; + private string remoteText = string.Empty; public SyncMessageHandler(IKeyboardInjectionService keyboard) { @@ -16,13 +17,24 @@ public sealed class SyncMessageHandler switch (message.Action) { case SyncAction.InsertText: - keyboard.InsertText(message.Text ?? string.Empty); + var insertedText = message.Text ?? string.Empty; + keyboard.InsertText(insertedText); + remoteText += insertedText; + break; + case SyncAction.ReplaceAll: + ReplaceAll(message.Text ?? string.Empty); break; case SyncAction.Backspace: keyboard.Backspace(); + if (remoteText.Length > 0) + { + remoteText = remoteText[..^1]; + } + break; case SyncAction.Enter: keyboard.Enter(); + remoteText += Environment.NewLine; break; case SyncAction.Ping: break; @@ -30,4 +42,18 @@ public sealed class SyncMessageHandler throw new InvalidOperationException($"Unsupported sync action: {message.Action}"); } } + + private void ReplaceAll(string newText) + { + if (newText == remoteText) + { + return; + } + + AppLogger.Info( + $"Hard replacing focused text. Old mirrored length: {remoteText.Length}, new length: {newText.Length}"); + + keyboard.ReplaceFocusedText(newText); + remoteText = newText; + } }