From 6a7d567dc96b8261f739bbd10f2319734d7fe720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Kondratiuk?= Date: Sun, 31 Mar 2024 15:31:48 -0300 Subject: [PATCH] Split input classes (#2577) --- lib/PuppeteerSharp/Cdp/CdpKeyboard.cs | 229 ++++++++++++ lib/PuppeteerSharp/Cdp/CdpMouse.cs | 428 +++++++++++++++++++++++ lib/PuppeteerSharp/Cdp/CdpPage.cs | 15 +- lib/PuppeteerSharp/Cdp/CdpTouchscreen.cs | 107 ++++++ lib/PuppeteerSharp/Input/Keyboard.cs | 196 +---------- lib/PuppeteerSharp/Input/Mouse.cs | 384 +------------------- lib/PuppeteerSharp/Input/Touchscreen.cs | 76 +--- 7 files changed, 794 insertions(+), 641 deletions(-) create mode 100644 lib/PuppeteerSharp/Cdp/CdpKeyboard.cs create mode 100644 lib/PuppeteerSharp/Cdp/CdpMouse.cs create mode 100644 lib/PuppeteerSharp/Cdp/CdpTouchscreen.cs diff --git a/lib/PuppeteerSharp/Cdp/CdpKeyboard.cs b/lib/PuppeteerSharp/Cdp/CdpKeyboard.cs new file mode 100644 index 000000000..7d946475f --- /dev/null +++ b/lib/PuppeteerSharp/Cdp/CdpKeyboard.cs @@ -0,0 +1,229 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System.Collections.Generic; +using System.Globalization; +using System.Threading.Tasks; +using PuppeteerSharp.Cdp.Messaging; +using PuppeteerSharp.Input; + +namespace PuppeteerSharp.Cdp; + +/// +public class CdpKeyboard : Keyboard +{ + private readonly HashSet _pressedKeys = []; + private CDPSession _client; + + internal CdpKeyboard(CDPSession client) + { + _client = client; + } + + /// + public override Task DownAsync(string key, DownOptions options = null) + { + var description = KeyDescriptionForString(key); + + var autoRepeat = _pressedKeys.Contains(description.Code); + _pressedKeys.Add(description.Code); + Modifiers |= ModifierBit(key); + + var text = options?.Text == null ? description.Text : options.Text; + + return _client.SendAsync("Input.dispatchKeyEvent", new InputDispatchKeyEventRequest + { + Type = text != null ? DispatchKeyEventType.KeyDown : DispatchKeyEventType.RawKeyDown, + Modifiers = Modifiers, + WindowsVirtualKeyCode = description.KeyCode, + Code = description.Code, + Key = description.Key, + Text = text, + UnmodifiedText = text, + AutoRepeat = autoRepeat, + Location = description.Location, + IsKeypad = description.Location == 3, + }); + } + + /// + public override Task UpAsync(string key) + { + var description = KeyDescriptionForString(key); + + Modifiers &= ~ModifierBit(key); + _pressedKeys.Remove(description.Code); + + return _client.SendAsync("Input.dispatchKeyEvent", new InputDispatchKeyEventRequest + { + Type = DispatchKeyEventType.KeyUp, + Modifiers = Modifiers, + Key = description.Key, + WindowsVirtualKeyCode = description.KeyCode, + Code = description.Code, + Location = description.Location, + }); + } + + /// + public override Task SendCharacterAsync(string charText) + => _client.SendAsync("Input.insertText", new InputInsertTextRequest + { + Text = charText, + }); + + /// + public override async Task TypeAsync(string text, TypeOptions options = null) + { + var delay = 0; + if (options?.Delay != null) + { + delay = (int)options.Delay; + } + + var textParts = StringInfo.GetTextElementEnumerator(text); + while (textParts.MoveNext()) + { + var letter = textParts.Current; + if (KeyDefinitions.ContainsKey(letter.ToString())) + { + await PressAsync(letter.ToString(), new PressOptions { Delay = delay }).ConfigureAwait(false); + } + else + { + if (delay > 0) + { + await Task.Delay(delay).ConfigureAwait(false); + } + + await SendCharacterAsync(letter.ToString()).ConfigureAwait(false); + } + } + } + + /// + public override async Task PressAsync(string key, PressOptions options = null) + { + await DownAsync(key, options).ConfigureAwait(false); + if (options?.Delay > 0) + { + await Task.Delay((int)options.Delay).ConfigureAwait(false); + } + + await UpAsync(key).ConfigureAwait(false); + } + + internal void UpdateClient(CDPSession newSession) => _client = newSession; + + private int ModifierBit(string key) + { + if (key == "Alt") + { + return 1; + } + + if (key == "Control") + { + return 2; + } + + if (key == "Meta") + { + return 4; + } + + if (key == "Shift") + { + return 8; + } + + return 0; + } + + private KeyDefinition KeyDescriptionForString(string keyString) + { + var shift = Modifiers & 8; + var description = new KeyDefinition + { + Key = string.Empty, + KeyCode = 0, + Code = string.Empty, + Text = string.Empty, + Location = 0, + }; + + var definition = KeyDefinitions.Get(keyString); + + if (!string.IsNullOrEmpty(definition.Key)) + { + description.Key = definition.Key; + } + + if (shift > 0 && !string.IsNullOrEmpty(definition.ShiftKey)) + { + description.Key = definition.ShiftKey; + } + + if (definition.KeyCode > 0) + { + description.KeyCode = definition.KeyCode; + } + + if (shift > 0 && definition.ShiftKeyCode != null) + { + description.KeyCode = (int)definition.ShiftKeyCode; + } + + if (!string.IsNullOrEmpty(definition.Code)) + { + description.Code = definition.Code; + } + + if (definition.Location != 0) + { + description.Location = definition.Location; + } + + if (description.Key.Length == 1) + { + description.Text = description.Key; + } + + if (!string.IsNullOrEmpty(definition.Text)) + { + description.Text = definition.Text; + } + + if (shift > 0 && !string.IsNullOrEmpty(definition.ShiftText)) + { + description.Text = definition.ShiftText; + } + + // if any modifiers besides shift are pressed, no text should be sent + if ((Modifiers & ~8) > 0) + { + description.Text = string.Empty; + } + + return description; + } +} diff --git a/lib/PuppeteerSharp/Cdp/CdpMouse.cs b/lib/PuppeteerSharp/Cdp/CdpMouse.cs new file mode 100644 index 000000000..f6b5ba8fa --- /dev/null +++ b/lib/PuppeteerSharp/Cdp/CdpMouse.cs @@ -0,0 +1,428 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using PuppeteerSharp.Cdp.Messaging; +using PuppeteerSharp.Helpers; +using PuppeteerSharp.Input; + +namespace PuppeteerSharp.Cdp; + +/// +public class CdpMouse : Mouse +{ + private readonly Keyboard _keyboard; + private readonly MouseState _mouseState = new(); + private readonly TaskQueue _actionsQueue = new(); + private readonly TaskQueue _multipleActionsQueue = new(); + private MouseTransaction.TransactionData _inFlightTransaction = null; + private CDPSession _client; + + /// + internal CdpMouse(CDPSession client, Keyboard keyboard) + { + _client = client; + _keyboard = keyboard; + } + + /// + public override async Task MoveAsync(decimal x, decimal y, MoveOptions options = null) + { + options ??= new MoveOptions(); + + var position = GetState().Position; + var fromX = position.X; + var fromY = position.Y; + var steps = options.Steps; + + for (var i = 1; i <= steps; i++) + { + await WithTransactionAsync(async (updateState) => + { + updateState(new MouseTransaction.TransactionData + { + Position = new Point + { + X = fromX + ((x - fromX) * ((decimal)i / steps)), + Y = fromY + ((y - fromY) * ((decimal)i / steps)), + }, + }); + + var state = GetState(); + + await _client.SendAsync("Input.dispatchMouseEvent", new InputDispatchMouseEventRequest + { + Type = MouseEventType.MouseMoved, + Modifiers = _keyboard.Modifiers, + Buttons = (int)state.Buttons, + Button = GetButtonFromPressedButtons(state.Buttons), + X = state.Position.X, + Y = state.Position.Y, + }).ConfigureAwait(false); + }).ConfigureAwait(false); + } + } + + /// + public override Task ClickAsync(decimal x, decimal y, ClickOptions options = null) + { + options ??= new ClickOptions(); + + return _multipleActionsQueue.Enqueue(async () => + { + if (options.Delay > 0) + { + await Task.WhenAll( + MoveAsync(x, y), + DownAsync(options)).ConfigureAwait(false); + + await Task.Delay(options.Delay).ConfigureAwait(false); + await UpAsync(options).ConfigureAwait(false); + } + else + { + await Task.WhenAll( + MoveAsync(x, y), + DownAsync(options), + UpAsync(options)).ConfigureAwait(false); + } + }); + } + + /// + public override Task DownAsync(ClickOptions options = null) + { + return WithTransactionAsync((updateState) => + { + options ??= new ClickOptions(); + + if (GetState().Buttons.HasFlag(options.Button)) + { + throw new PuppeteerException($"{options.Button} is already pressed"); + } + + updateState(new MouseTransaction.TransactionData + { + Buttons = GetState().Buttons | options.Button, + }); + + var state = GetState(); + return _client.SendAsync("Input.dispatchMouseEvent", new InputDispatchMouseEventRequest + { + Type = MouseEventType.MousePressed, + Modifiers = _keyboard.Modifiers, + ClickCount = options.ClickCount, + Buttons = (int)state.Buttons, + Button = options.Button, + X = state.Position.X, + Y = state.Position.Y, + }); + }); + } + + /// + public override Task UpAsync(ClickOptions options = null) + { + return WithTransactionAsync((updateState) => + { + options ??= new ClickOptions(); + + if (!GetState().Buttons.HasFlag(options.Button)) + { + throw new PuppeteerException($"{options.Button} is not pressed"); + } + + updateState(new MouseTransaction.TransactionData + { + Buttons = GetState().Buttons & ~options.Button, + }); + + var state = GetState(); + return _client.SendAsync("Input.dispatchMouseEvent", new InputDispatchMouseEventRequest + { + Type = MouseEventType.MouseReleased, + Modifiers = _keyboard.Modifiers, + ClickCount = options.ClickCount, + Buttons = (int)state.Buttons, + Button = options.Button, + X = state.Position.X, + Y = state.Position.Y, + }); + }); + } + + /// + public override Task WheelAsync(decimal deltaX, decimal deltaY) + { + var state = GetState(); + + return _client.SendAsync( + "Input.dispatchMouseEvent", + new InputDispatchMouseEventRequest + { + Type = MouseEventType.MouseWheel, + DeltaX = deltaX, + DeltaY = deltaY, + X = state.Position.X, + Y = state.Position.Y, + Modifiers = _keyboard.Modifiers, + PointerType = PointerType.Mouse, + }); + } + + /// + public override Task DragAsync(decimal startX, decimal startY, decimal endX, decimal endY) + { + return _multipleActionsQueue.Enqueue(async () => + { + var result = new TaskCompletionSource(); + + void DragIntercepted(object sender, MessageEventArgs e) + { + if (e.MessageID == "Input.dragIntercepted") + { + result.TrySetResult(e.MessageData.SelectToken("data").ToObject()); + _client.MessageReceived -= DragIntercepted; + } + } + + _client.MessageReceived += DragIntercepted; + await MoveAsync(startX, startY).ConfigureAwait(false); + await DownAsync().ConfigureAwait(false); + await MoveAsync(endX, endY).ConfigureAwait(false); + + return await result.Task.ConfigureAwait(false); + }); + } + + /// + public override Task DragEnterAsync(decimal x, decimal y, DragData data) + => _client.SendAsync( + "Input.dispatchDragEvent", + new InputDispatchDragEventRequest + { + Type = DragEventType.DragEnter, + X = x, + Y = y, + Modifiers = _keyboard.Modifiers, + Data = data, + }); + + /// + public override Task DragOverAsync(decimal x, decimal y, DragData data) + => _client.SendAsync( + "Input.dispatchDragEvent", + new InputDispatchDragEventRequest + { + Type = DragEventType.DragOver, + X = x, + Y = y, + Modifiers = _keyboard.Modifiers, + Data = data, + }); + + /// + public override Task DropAsync(decimal x, decimal y, DragData data) + => _client.SendAsync( + "Input.dispatchDragEvent", + new InputDispatchDragEventRequest + { + Type = DragEventType.Drop, + X = x, + Y = y, + Modifiers = _keyboard.Modifiers, + Data = data, + }); + + /// + public override async Task DragAndDropAsync(decimal startX, decimal startY, decimal endX, decimal endY, int delay = 0) + { + // DragAsync is already using _multipleActionsQueue + var data = await DragAsync(startX, startY, endX, endY).ConfigureAwait(false); + await _multipleActionsQueue.Enqueue(async () => + { + await DragEnterAsync(endX, endY, data).ConfigureAwait(false); + await DragOverAsync(endX, endY, data).ConfigureAwait(false); + + if (delay > 0) + { + await Task.Delay(delay).ConfigureAwait(false); + } + + await DropAsync(endX, endY, data).ConfigureAwait(false); + await UpAsync().ConfigureAwait(false); + }).ConfigureAwait(false); + } + + /// + public override Task ResetAsync() + { + return _multipleActionsQueue.Enqueue(() => + { + var actions = new List(); + var state = GetState(); + + foreach (var button in new[] + { + MouseButton.Left, + MouseButton.Middle, + MouseButton.Right, + MouseButton.Back, + MouseButton.Forward, + }) + { + if (state.Buttons.HasFlag(button)) + { + actions.Add(UpAsync(new() + { + Button = button, + })); + } + } + + if (state.Position.X != 0 || state.Position.Y != 0) + { + actions.Add(MoveAsync(0, 0)); + } + + return Task.WhenAll(actions); + }); + } + + internal void UpdateClient(CDPSession newSession) => _client = newSession; + + /// + protected override void Dispose(bool disposing) + { + if (disposing) + { + _actionsQueue.Dispose(); + _multipleActionsQueue.Dispose(); + } + } + + private MouseTransaction CreateTransaction() + { + _inFlightTransaction = new MouseTransaction.TransactionData(); + + return new MouseTransaction() + { + Update = updates => + { + if (updates.Position.HasValue) + { + _inFlightTransaction.Position = updates.Position.Value; + } + + if (updates.Buttons.HasValue) + { + _inFlightTransaction.Buttons = updates.Buttons.Value; + } + }, + Commit = () => + { + _mouseState.Position = _inFlightTransaction.Position ?? _mouseState.Position; + _mouseState.Buttons = _inFlightTransaction.Buttons ?? _mouseState.Buttons; + _inFlightTransaction = null; + }, + Rollback = () => _inFlightTransaction = null, + }; + } + + private Task WithTransactionAsync(Func, Task> action) + { + return _actionsQueue.Enqueue(async () => + { + var transaction = CreateTransaction(); + try + { + await action(transaction.Update).ConfigureAwait(false); + transaction.Commit(); + } + catch (Exception ex) + { + transaction.Rollback(); + throw new PuppeteerException("Failed to perform mouse action", ex); + } + }); + } + + private MouseButton GetButtonFromPressedButtons(MouseButton buttons) + { + if (buttons.HasFlag(MouseButton.Left)) + { + return MouseButton.Left; + } + + if (buttons.HasFlag(MouseButton.Right)) + { + return MouseButton.Right; + } + + if (buttons.HasFlag(MouseButton.Middle)) + { + return MouseButton.Middle; + } + + if (buttons.HasFlag(MouseButton.Back)) + { + return MouseButton.Back; + } + + if (buttons.HasFlag(MouseButton.Forward)) + { + return MouseButton.Forward; + } + + return MouseButton.None; + } + + private MouseState GetState() + { + var state = new MouseTransaction.TransactionData() + { + Position = _mouseState.Position, + Buttons = _mouseState.Buttons, + }; + + if (_inFlightTransaction != null) + { + if (_inFlightTransaction.Position.HasValue) + { + state.Position = _inFlightTransaction.Position.Value; + } + + if (_inFlightTransaction.Buttons.HasValue) + { + state.Buttons = _inFlightTransaction.Buttons.Value; + } + } + + return new MouseState + { + Position = state.Position.Value, + Buttons = state.Buttons.Value, + }; + } +} diff --git a/lib/PuppeteerSharp/Cdp/CdpPage.cs b/lib/PuppeteerSharp/Cdp/CdpPage.cs index 4d15a70b2..6106322f2 100644 --- a/lib/PuppeteerSharp/Cdp/CdpPage.cs +++ b/lib/PuppeteerSharp/Cdp/CdpPage.cs @@ -31,7 +31,6 @@ using PuppeteerSharp.Cdp.Messaging; using PuppeteerSharp.Helpers; using PuppeteerSharp.Helpers.Json; -using PuppeteerSharp.Input; using PuppeteerSharp.Media; using PuppeteerSharp.PageAccessibility; using PuppeteerSharp.PageCoverage; @@ -72,9 +71,9 @@ private CdpPage( TabTarget = (CdpTarget)TabTargetClient.Target; PrimaryTarget = target; _targetManager = target.TargetManager; - Keyboard = new Keyboard(client); - Mouse = new Mouse(client, Keyboard); - Touchscreen = new Touchscreen(client, Keyboard); + Keyboard = new CdpKeyboard(client); + Mouse = new CdpMouse(client, Keyboard); + Touchscreen = new CdpTouchscreen(client, Keyboard); Tracing = new Tracing(client); Coverage = new Coverage(client); @@ -906,7 +905,7 @@ private void SetupPrimaryTargetListeners() private void OnAttachedToTarget(object sender, SessionEventArgs e) { var session = e.Session as CDPSession; - System.Diagnostics.Debug.Assert(session != null, nameof(session) + " != null"); + Debug.Assert(session != null, nameof(session) + " != null"); FrameManager.OnAttachedToTarget(new TargetChangedArgs { Target = session.Target }); if (session.Target.Type == TargetType.Worker) @@ -981,9 +980,9 @@ private async Task OnActivationAsync(CdpCDPSession newSession) { PrimaryTargetClient = newSession; PrimaryTarget = (CdpTarget)PrimaryTargetClient.Target; - Keyboard.UpdateClient(Client); - Mouse.UpdateClient(Client); - Touchscreen.UpdateClient(Client); + ((CdpKeyboard)Keyboard).UpdateClient(Client); + ((CdpMouse)Mouse).UpdateClient(Client); + ((CdpTouchscreen)Touchscreen).UpdateClient(Client); Accessibility.UpdateClient(Client); _emulationManager.UpdateClient(Client); Tracing.UpdateClient(Client); diff --git a/lib/PuppeteerSharp/Cdp/CdpTouchscreen.cs b/lib/PuppeteerSharp/Cdp/CdpTouchscreen.cs new file mode 100644 index 000000000..29325860e --- /dev/null +++ b/lib/PuppeteerSharp/Cdp/CdpTouchscreen.cs @@ -0,0 +1,107 @@ +// * MIT License +// * +// * Copyright (c) Darío Kondratiuk +// * +// * Permission is hereby granted, free of charge, to any person obtaining a copy +// * of this software and associated documentation files (the "Software"), to deal +// * in the Software without restriction, including without limitation the rights +// * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// * copies of the Software, and to permit persons to whom the Software is +// * furnished to do so, subject to the following conditions: +// * +// * The above copyright notice and this permission notice shall be included in all +// * copies or substantial portions of the Software. +// * +// * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// * SOFTWARE. + +using System; +using System.Threading.Tasks; +using PuppeteerSharp.Cdp.Messaging; +using PuppeteerSharp.Input; + +namespace PuppeteerSharp.Cdp; + +/// +public class CdpTouchscreen : Touchscreen +{ + private readonly Keyboard _keyboard; + private CDPSession _client; + + /// + internal CdpTouchscreen(CDPSession client, Keyboard keyboard) + { + _client = client; + _keyboard = keyboard; + } + + /// + public override Task TouchStartAsync(decimal x, decimal y) + { + var touchPoints = new[] + { + new TouchPoint + { + X = Math.Round(x, MidpointRounding.AwayFromZero), + Y = Math.Round(y, MidpointRounding.AwayFromZero), + RadiusX = 0.5m, + RadiusY = 0.5m, + Force = 0.5m, + }, + }; + + return _client.SendAsync( + "Input.dispatchTouchEvent", + new InputDispatchTouchEventRequest + { + Type = "touchStart", + TouchPoints = touchPoints, + Modifiers = _keyboard.Modifiers, + }); + } + + /// + public override Task TouchMoveAsync(decimal x, decimal y) + { + var touchPoints = new[] + { + new TouchPoint + { + X = Math.Round(x, MidpointRounding.AwayFromZero), + Y = Math.Round(y, MidpointRounding.AwayFromZero), + RadiusX = 0.5m, + RadiusY = 0.5m, + Force = 0.5m, + }, + }; + + return _client.SendAsync( + "Input.dispatchTouchEvent", + new InputDispatchTouchEventRequest + { + Type = "touchStart", + TouchPoints = touchPoints, + Modifiers = _keyboard.Modifiers, + }); + } + + /// + public override Task TouchEndAsync() + { + return _client.SendAsync( + "Input.dispatchTouchEvent", + new InputDispatchTouchEventRequest + { + Type = "touchEnd", + TouchPoints = [], + Modifiers = _keyboard.Modifiers, + }); + } + + internal void UpdateClient(CDPSession newSession) => _client = newSession; +} diff --git a/lib/PuppeteerSharp/Input/Keyboard.cs b/lib/PuppeteerSharp/Input/Keyboard.cs index b72c566ee..98745876f 100644 --- a/lib/PuppeteerSharp/Input/Keyboard.cs +++ b/lib/PuppeteerSharp/Input/Keyboard.cs @@ -1,209 +1,25 @@ -using System.Collections.Generic; -using System.Globalization; using System.Threading.Tasks; -using PuppeteerSharp.Cdp.Messaging; namespace PuppeteerSharp.Input { /// - public class Keyboard : IKeyboard + public abstract class Keyboard : IKeyboard { - private readonly HashSet _pressedKeys = []; - private CDPSession _client; - - internal Keyboard(CDPSession client) - { - _client = client; - } - internal int Modifiers { get; set; } /// - public Task DownAsync(string key, DownOptions options = null) - { - var description = KeyDescriptionForString(key); - - var autoRepeat = _pressedKeys.Contains(description.Code); - _pressedKeys.Add(description.Code); - Modifiers |= ModifierBit(key); - - var text = options?.Text == null ? description.Text : options.Text; - - return _client.SendAsync("Input.dispatchKeyEvent", new InputDispatchKeyEventRequest - { - Type = text != null ? DispatchKeyEventType.KeyDown : DispatchKeyEventType.RawKeyDown, - Modifiers = Modifiers, - WindowsVirtualKeyCode = description.KeyCode, - Code = description.Code, - Key = description.Key, - Text = text, - UnmodifiedText = text, - AutoRepeat = autoRepeat, - Location = description.Location, - IsKeypad = description.Location == 3, - }); - } + public abstract Task DownAsync(string key, DownOptions options = null); /// - public Task UpAsync(string key) - { - var description = KeyDescriptionForString(key); - - Modifiers &= ~ModifierBit(key); - _pressedKeys.Remove(description.Code); - - return _client.SendAsync("Input.dispatchKeyEvent", new InputDispatchKeyEventRequest - { - Type = DispatchKeyEventType.KeyUp, - Modifiers = Modifiers, - Key = description.Key, - WindowsVirtualKeyCode = description.KeyCode, - Code = description.Code, - Location = description.Location, - }); - } + public abstract Task UpAsync(string key); /// - public Task SendCharacterAsync(string charText) - => _client.SendAsync("Input.insertText", new InputInsertTextRequest - { - Text = charText, - }); + public abstract Task SendCharacterAsync(string charText); /// - public async Task TypeAsync(string text, TypeOptions options = null) - { - var delay = 0; - if (options?.Delay != null) - { - delay = (int)options.Delay; - } - - var textParts = StringInfo.GetTextElementEnumerator(text); - while (textParts.MoveNext()) - { - var letter = textParts.Current; - if (KeyDefinitions.ContainsKey(letter.ToString())) - { - await PressAsync(letter.ToString(), new PressOptions { Delay = delay }).ConfigureAwait(false); - } - else - { - if (delay > 0) - { - await Task.Delay(delay).ConfigureAwait(false); - } - - await SendCharacterAsync(letter.ToString()).ConfigureAwait(false); - } - } - } + public abstract Task TypeAsync(string text, TypeOptions options = null); /// - public async Task PressAsync(string key, PressOptions options = null) - { - await DownAsync(key, options).ConfigureAwait(false); - if (options?.Delay > 0) - { - await Task.Delay((int)options.Delay).ConfigureAwait(false); - } - - await UpAsync(key).ConfigureAwait(false); - } - - internal void UpdateClient(CDPSession newSession) => _client = newSession; - - private int ModifierBit(string key) - { - if (key == "Alt") - { - return 1; - } - - if (key == "Control") - { - return 2; - } - - if (key == "Meta") - { - return 4; - } - - if (key == "Shift") - { - return 8; - } - - return 0; - } - - private KeyDefinition KeyDescriptionForString(string keyString) - { - var shift = Modifiers & 8; - var description = new KeyDefinition - { - Key = string.Empty, - KeyCode = 0, - Code = string.Empty, - Text = string.Empty, - Location = 0, - }; - - var definition = KeyDefinitions.Get(keyString); - - if (!string.IsNullOrEmpty(definition.Key)) - { - description.Key = definition.Key; - } - - if (shift > 0 && !string.IsNullOrEmpty(definition.ShiftKey)) - { - description.Key = definition.ShiftKey; - } - - if (definition.KeyCode > 0) - { - description.KeyCode = definition.KeyCode; - } - - if (shift > 0 && definition.ShiftKeyCode != null) - { - description.KeyCode = (int)definition.ShiftKeyCode; - } - - if (!string.IsNullOrEmpty(definition.Code)) - { - description.Code = definition.Code; - } - - if (definition.Location != 0) - { - description.Location = definition.Location; - } - - if (description.Key.Length == 1) - { - description.Text = description.Key; - } - - if (!string.IsNullOrEmpty(definition.Text)) - { - description.Text = definition.Text; - } - - if (shift > 0 && !string.IsNullOrEmpty(definition.ShiftText)) - { - description.Text = definition.ShiftText; - } - - // if any modifiers besides shift are pressed, no text should be sent - if ((Modifiers & ~8) > 0) - { - description.Text = string.Empty; - } - - return description; - } + public abstract Task PressAsync(string key, PressOptions options = null); } } diff --git a/lib/PuppeteerSharp/Input/Mouse.cs b/lib/PuppeteerSharp/Input/Mouse.cs index 7d8810faa..e608c80a3 100644 --- a/lib/PuppeteerSharp/Input/Mouse.cs +++ b/lib/PuppeteerSharp/Input/Mouse.cs @@ -11,285 +11,40 @@ due to the differences in the threading model. namespace PuppeteerSharp.Input { /// - public class Mouse : IMouse + public abstract class Mouse : IMouse { - private readonly Keyboard _keyboard; - private readonly MouseState _mouseState = new(); - private readonly TaskQueue _actionsQueue = new(); - private readonly TaskQueue _multipleActionsQueue = new(); - private MouseTransaction.TransactionData _inFlightTransaction = null; - private CDPSession _client; - - /// - public Mouse(CDPSession client, Keyboard keyboard) - { - _client = client; - _keyboard = keyboard; - } - /// - public async Task MoveAsync(decimal x, decimal y, MoveOptions options = null) - { - options ??= new MoveOptions(); - - var position = GetState().Position; - var fromX = position.X; - var fromY = position.Y; - var steps = options.Steps; - - for (var i = 1; i <= steps; i++) - { - await WithTransactionAsync(async (updateState) => - { - updateState(new MouseTransaction.TransactionData - { - Position = new Point - { - X = fromX + ((x - fromX) * ((decimal)i / steps)), - Y = fromY + ((y - fromY) * ((decimal)i / steps)), - }, - }); - - var state = GetState(); - - await _client.SendAsync("Input.dispatchMouseEvent", new InputDispatchMouseEventRequest - { - Type = MouseEventType.MouseMoved, - Modifiers = _keyboard.Modifiers, - Buttons = (int)state.Buttons, - Button = GetButtonFromPressedButtons(state.Buttons), - X = state.Position.X, - Y = state.Position.Y, - }).ConfigureAwait(false); - }).ConfigureAwait(false); - } - } + public abstract Task MoveAsync(decimal x, decimal y, MoveOptions options = null); /// - public Task ClickAsync(decimal x, decimal y, ClickOptions options = null) - { - options ??= new ClickOptions(); - - return _multipleActionsQueue.Enqueue(async () => - { - if (options.Delay > 0) - { - await Task.WhenAll( - MoveAsync(x, y), - DownAsync(options)).ConfigureAwait(false); - - await Task.Delay(options.Delay).ConfigureAwait(false); - await UpAsync(options).ConfigureAwait(false); - } - else - { - await Task.WhenAll( - MoveAsync(x, y), - DownAsync(options), - UpAsync(options)).ConfigureAwait(false); - } - }); - } + public abstract Task ClickAsync(decimal x, decimal y, ClickOptions options = null); /// - public Task DownAsync(ClickOptions options = null) - { - return WithTransactionAsync((updateState) => - { - options ??= new ClickOptions(); - - if (GetState().Buttons.HasFlag(options.Button)) - { - throw new PuppeteerException($"{options.Button} is already pressed"); - } - - updateState(new MouseTransaction.TransactionData - { - Buttons = GetState().Buttons | options.Button, - }); - - var state = GetState(); - return _client.SendAsync("Input.dispatchMouseEvent", new InputDispatchMouseEventRequest - { - Type = MouseEventType.MousePressed, - Modifiers = _keyboard.Modifiers, - ClickCount = options.ClickCount, - Buttons = (int)state.Buttons, - Button = options.Button, - X = state.Position.X, - Y = state.Position.Y, - }); - }); - } + public abstract Task DownAsync(ClickOptions options = null); /// - public Task UpAsync(ClickOptions options = null) - { - return WithTransactionAsync((updateState) => - { - options ??= new ClickOptions(); - - if (!GetState().Buttons.HasFlag(options.Button)) - { - throw new PuppeteerException($"{options.Button} is not pressed"); - } - - updateState(new MouseTransaction.TransactionData - { - Buttons = GetState().Buttons & ~options.Button, - }); - - var state = GetState(); - return _client.SendAsync("Input.dispatchMouseEvent", new InputDispatchMouseEventRequest - { - Type = MouseEventType.MouseReleased, - Modifiers = _keyboard.Modifiers, - ClickCount = options.ClickCount, - Buttons = (int)state.Buttons, - Button = options.Button, - X = state.Position.X, - Y = state.Position.Y, - }); - }); - } + public abstract Task UpAsync(ClickOptions options = null); /// - public Task WheelAsync(decimal deltaX, decimal deltaY) - { - var state = GetState(); - - return _client.SendAsync( - "Input.dispatchMouseEvent", - new InputDispatchMouseEventRequest - { - Type = MouseEventType.MouseWheel, - DeltaX = deltaX, - DeltaY = deltaY, - X = state.Position.X, - Y = state.Position.Y, - Modifiers = _keyboard.Modifiers, - PointerType = PointerType.Mouse, - }); - } + public abstract Task WheelAsync(decimal deltaX, decimal deltaY); /// - public Task DragAsync(decimal startX, decimal startY, decimal endX, decimal endY) - { - return _multipleActionsQueue.Enqueue(async () => - { - var result = new TaskCompletionSource(); - - void DragIntercepted(object sender, MessageEventArgs e) - { - if (e.MessageID == "Input.dragIntercepted") - { - result.TrySetResult(e.MessageData.SelectToken("data").ToObject()); - _client.MessageReceived -= DragIntercepted; - } - } - - _client.MessageReceived += DragIntercepted; - await MoveAsync(startX, startY).ConfigureAwait(false); - await DownAsync().ConfigureAwait(false); - await MoveAsync(endX, endY).ConfigureAwait(false); - - return await result.Task.ConfigureAwait(false); - }); - } + public abstract Task DragAsync(decimal startX, decimal startY, decimal endX, decimal endY); /// - public Task DragEnterAsync(decimal x, decimal y, DragData data) - => _client.SendAsync( - "Input.dispatchDragEvent", - new InputDispatchDragEventRequest - { - Type = DragEventType.DragEnter, - X = x, - Y = y, - Modifiers = _keyboard.Modifiers, - Data = data, - }); + public abstract Task DragEnterAsync(decimal x, decimal y, DragData data); /// - public Task DragOverAsync(decimal x, decimal y, DragData data) - => _client.SendAsync( - "Input.dispatchDragEvent", - new InputDispatchDragEventRequest - { - Type = DragEventType.DragOver, - X = x, - Y = y, - Modifiers = _keyboard.Modifiers, - Data = data, - }); + public abstract Task DragOverAsync(decimal x, decimal y, DragData data); /// - public Task DropAsync(decimal x, decimal y, DragData data) - => _client.SendAsync( - "Input.dispatchDragEvent", - new InputDispatchDragEventRequest - { - Type = DragEventType.Drop, - X = x, - Y = y, - Modifiers = _keyboard.Modifiers, - Data = data, - }); + public abstract Task DropAsync(decimal x, decimal y, DragData data); /// - public async Task DragAndDropAsync(decimal startX, decimal startY, decimal endX, decimal endY, int delay = 0) - { - // DragAsync is already using _multipleActionsQueue - var data = await DragAsync(startX, startY, endX, endY).ConfigureAwait(false); - await _multipleActionsQueue.Enqueue(async () => - { - await DragEnterAsync(endX, endY, data).ConfigureAwait(false); - await DragOverAsync(endX, endY, data).ConfigureAwait(false); - - if (delay > 0) - { - await Task.Delay(delay).ConfigureAwait(false); - } - - await DropAsync(endX, endY, data).ConfigureAwait(false); - await UpAsync().ConfigureAwait(false); - }).ConfigureAwait(false); - } + public abstract Task DragAndDropAsync(decimal startX, decimal startY, decimal endX, decimal endY, int delay = 0); /// - public Task ResetAsync() - { - return _multipleActionsQueue.Enqueue(() => - { - var actions = new List(); - var state = GetState(); - - foreach (var button in new[] - { - MouseButton.Left, - MouseButton.Middle, - MouseButton.Right, - MouseButton.Back, - MouseButton.Forward, - }) - { - if (state.Buttons.HasFlag(button)) - { - actions.Add(UpAsync(new() - { - Button = button, - })); - } - } - - if (state.Position.X != 0 || state.Position.Y != 0) - { - actions.Add(MoveAsync(0, 0)); - } - - return Task.WhenAll(actions); - }); - } + public abstract Task ResetAsync(); /// public void Dispose() @@ -298,120 +53,7 @@ public void Dispose() GC.SuppressFinalize(this); } - internal void UpdateClient(CDPSession newSession) => _client = newSession; - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _actionsQueue.Dispose(); - _multipleActionsQueue.Dispose(); - } - } - - private MouseTransaction CreateTransaction() - { - _inFlightTransaction = new MouseTransaction.TransactionData(); - - return new MouseTransaction() - { - Update = updates => - { - if (updates.Position.HasValue) - { - _inFlightTransaction.Position = updates.Position.Value; - } - - if (updates.Buttons.HasValue) - { - _inFlightTransaction.Buttons = updates.Buttons.Value; - } - }, - Commit = () => - { - _mouseState.Position = _inFlightTransaction.Position ?? _mouseState.Position; - _mouseState.Buttons = _inFlightTransaction.Buttons ?? _mouseState.Buttons; - _inFlightTransaction = null; - }, - Rollback = () => _inFlightTransaction = null, - }; - } - - private Task WithTransactionAsync(Func, Task> action) - { - return _actionsQueue.Enqueue(async () => - { - var transaction = CreateTransaction(); - try - { - await action(transaction.Update).ConfigureAwait(false); - transaction.Commit(); - } - catch (Exception ex) - { - transaction.Rollback(); - throw new PuppeteerException("Failed to perform mouse action", ex); - } - }); - } - - private MouseButton GetButtonFromPressedButtons(MouseButton buttons) - { - if (buttons.HasFlag(MouseButton.Left)) - { - return MouseButton.Left; - } - - if (buttons.HasFlag(MouseButton.Right)) - { - return MouseButton.Right; - } - - if (buttons.HasFlag(MouseButton.Middle)) - { - return MouseButton.Middle; - } - - if (buttons.HasFlag(MouseButton.Back)) - { - return MouseButton.Back; - } - - if (buttons.HasFlag(MouseButton.Forward)) - { - return MouseButton.Forward; - } - - return MouseButton.None; - } - - private MouseState GetState() - { - var state = new MouseTransaction.TransactionData() - { - Position = _mouseState.Position, - Buttons = _mouseState.Buttons, - }; - - if (_inFlightTransaction != null) - { - if (_inFlightTransaction.Position.HasValue) - { - state.Position = _inFlightTransaction.Position.Value; - } - - if (_inFlightTransaction.Buttons.HasValue) - { - state.Buttons = _inFlightTransaction.Buttons.Value; - } - } - - return new MouseState - { - Position = state.Position.Value, - Buttons = state.Buttons.Value, - }; - } + protected abstract void Dispose(bool disposing); } } diff --git a/lib/PuppeteerSharp/Input/Touchscreen.cs b/lib/PuppeteerSharp/Input/Touchscreen.cs index e5fcde7f7..191d6ed73 100755 --- a/lib/PuppeteerSharp/Input/Touchscreen.cs +++ b/lib/PuppeteerSharp/Input/Touchscreen.cs @@ -1,22 +1,10 @@ -using System; using System.Threading.Tasks; -using PuppeteerSharp.Cdp.Messaging; namespace PuppeteerSharp.Input { /// - public class Touchscreen : ITouchscreen + public abstract class Touchscreen : ITouchscreen { - private readonly Keyboard _keyboard; - private CDPSession _client; - - /// - public Touchscreen(CDPSession client, Keyboard keyboard) - { - _client = client; - _keyboard = keyboard; - } - /// public async Task TapAsync(decimal x, decimal y) { @@ -25,68 +13,12 @@ public async Task TapAsync(decimal x, decimal y) } /// - public Task TouchStartAsync(decimal x, decimal y) - { - var touchPoints = new[] - { - new TouchPoint - { - X = Math.Round(x, MidpointRounding.AwayFromZero), - Y = Math.Round(y, MidpointRounding.AwayFromZero), - RadiusX = 0.5m, - RadiusY = 0.5m, - Force = 0.5m, - }, - }; - - return _client.SendAsync( - "Input.dispatchTouchEvent", - new InputDispatchTouchEventRequest - { - Type = "touchStart", - TouchPoints = touchPoints, - Modifiers = _keyboard.Modifiers, - }); - } + public abstract Task TouchStartAsync(decimal x, decimal y); /// - public Task TouchMoveAsync(decimal x, decimal y) - { - var touchPoints = new[] - { - new TouchPoint - { - X = Math.Round(x, MidpointRounding.AwayFromZero), - Y = Math.Round(y, MidpointRounding.AwayFromZero), - RadiusX = 0.5m, - RadiusY = 0.5m, - Force = 0.5m, - }, - }; - - return _client.SendAsync( - "Input.dispatchTouchEvent", - new InputDispatchTouchEventRequest - { - Type = "touchStart", - TouchPoints = touchPoints, - Modifiers = _keyboard.Modifiers, - }); - } + public abstract Task TouchMoveAsync(decimal x, decimal y); /// - public Task TouchEndAsync() - { - return _client.SendAsync( - "Input.dispatchTouchEvent", - new InputDispatchTouchEventRequest - { - Type = "touchEnd", - TouchPoints = [], - Modifiers = _keyboard.Modifiers, - }); - } - - internal void UpdateClient(CDPSession newSession) => _client = newSession; + public abstract Task TouchEndAsync(); } }