Skip to content

Commit

Permalink
feat: add support for scrolling
Browse files Browse the repository at this point in the history
  • Loading branch information
socsieng committed Dec 31, 2020
1 parent 2861bf9 commit b438e00
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 26 deletions.
28 changes: 21 additions & 7 deletions README.md
Expand Up @@ -85,9 +85,9 @@ The mouse cursor can be moved using the following markup: `<m:[x1,y1,]x2,y2[:dur

Example usage:

- `<m:400,400:0.5>`: Move mouse cursor from current position to 400, 400 over 0.5 seconds
- `<m:400,400,0,0:2>`: Move mouse cursor from 400, 400 position to 0, 0 over 2 seconds
- `<m:400,400>`: Move mouse cursor to 400, 400 instantly
- `<m:400,400:0.5>`: Move mouse cursor from current position to 400, 400 over 0.5 seconds.
- `<m:400,400,0,0:2>`: Move mouse cursor from 400, 400 position to 0, 0 over 2 seconds.
- `<m:400,400>`: Move mouse cursor to 400, 400 instantly.

#### Mouse click

Expand All @@ -98,8 +98,8 @@ A mouse click can be activated using the following markup: `<m:button[:clicks]>`

Example usage:

- `<m:right>`: Right mouse click at the current mouse location
- `<m:left:2>`: Double click the left button at the current mouse location
- `<m:right>`: Right mouse click at the current mouse location.
- `<m:left:2>`: Double click the left button at the current mouse location.

#### Mouse drag

Expand All @@ -114,8 +114,22 @@ The structure argument structure is similar to moving the mouse cursor.

Example usage:

- `<d:400,400:0.5>`: Drag the mouse using the left mouse button from current position to 400, 400 over 0.5 seconds
- `<d:400,400,0,0:2:right>`: Drag the mouse using the right mouse button from 400, 400 position to 0, 0 over 2 seconds
- `<d:400,400:0.5>`: Drag the mouse using the left mouse button from current position to 400, 400 over 0.5 seconds.
- `<d:400,400,0,0:2:right>`: Drag the mouse using the right mouse button from 400, 400 position to 0, 0 over 2 seconds.

#### Mouse scrolling

A mouse scroll can be initiated with: `<s:x,y[:duration]>`

- `x` is required and controls horizontal scrolling. Positive values scroll to the right, while negative values scroll to the left.
- `y` is required and controls vertical scrolling. Positive values scroll down, while negative values scroll up.
- `duration` is optional and determines the number of seconds (supports partial seconds) that should be used to drag the mouse (larger number means slower movement). Defaults to `0`.

Example usage:

- `<s:0,400:0.5>`: Scrolls down 400 pixels over 0.5 seconds.
- `<s:0,-100:0.2>`: Scrolls up 400 pixels over 0.2 seconds.
- `<s:100,0>`: Scrolls 100 pixel to the right instantly.

### Pauses

Expand Down
5 changes: 3 additions & 2 deletions Sources/SendKeysLib/Commands/Command.swift
Expand Up @@ -5,14 +5,15 @@ public enum CommandType {
case mouseMove
case mouseClick
case mouseDrag
case mouseScroll
case continuation
}

public struct Command: Equatable {
let type: CommandType
let arguments: [String]
let arguments: [String?]

public init(_ type: CommandType, _ arguments: [String]) {
public init(_ type: CommandType, _ arguments: [String?]) {
self.type = type
self.arguments = arguments
}
Expand Down
45 changes: 29 additions & 16 deletions Sources/SendKeysLib/Commands/CommandExecutor.swift
Expand Up @@ -20,6 +20,8 @@ public class CommandExecutor: CommandExecutorProtocol {
executeMouseClick(command)
case .mouseDrag:
executeMouseDrag(command)
case .mouseScroll:
executeMouseScroll(command)
default:
fatalError("Unrecognized command type \(command.type)")
}
Expand All @@ -29,22 +31,22 @@ public class CommandExecutor: CommandExecutorProtocol {
var modifiers: [String] = []

if command.arguments.count > 1 {
modifiers = command.arguments[1].components(separatedBy: ",")
modifiers = command.arguments[1]!.components(separatedBy: ",")
}

try! keyPresser.pressKey(key: command.arguments[0], modifiers: modifiers)
try! keyPresser.pressKey(key: command.arguments[0]!, modifiers: modifiers)
}

private func executePause(_ command: Command) {
Sleeper.sleep(seconds: Double(command.arguments[0])!)
Sleeper.sleep(seconds: Double(command.arguments[0]!)!)
}

private func executeMouseMove(_ command: Command) {
let x1 = Double(command.arguments[0])!
let y1 = Double(command.arguments[1])!
let x2 = Double(command.arguments[2])!
let y2 = Double(command.arguments[3])!
let duration: TimeInterval = Double(command.arguments[4])!
let x1 = Double(command.arguments[0]!)!
let y1 = Double(command.arguments[1]!)!
let x2 = Double(command.arguments[2]!)!
let y2 = Double(command.arguments[3]!)!
let duration: TimeInterval = Double(command.arguments[4]!)!

mouseController.move(
start: CGPoint(x: x1, y: y1),
Expand All @@ -54,23 +56,34 @@ public class CommandExecutor: CommandExecutorProtocol {
}

private func executeMouseClick(_ command: Command) {
let button = command.arguments[0]
let clicks = Int(command.arguments[1])!
let button = command.arguments[0]!
let clicks = Int(command.arguments[1]!)!

try! mouseController.click(
CGPoint(x: -1, y: -1),
button: getMouseButton(button: button),
clickCount: clicks
)
}

private func executeMouseScroll(_ command: Command) {
let x = Int(command.arguments[0]!) ?? 0
let y = Int(command.arguments[1]!) ?? 0
let duration = Double(command.arguments[2] ?? "0") ?? 0

mouseController.scroll(
CGPoint(x: x, y: y),
duration
)
}

private func executeMouseDrag(_ command: Command) {
let x1 = Double(command.arguments[0])!
let y1 = Double(command.arguments[1])!
let x2 = Double(command.arguments[2])!
let y2 = Double(command.arguments[3])!
let duration: TimeInterval = Double(command.arguments[4])!
let button = command.arguments[5]
let x1 = Double(command.arguments[0]!)!
let y1 = Double(command.arguments[1]!)!
let x2 = Double(command.arguments[2]!)!
let y2 = Double(command.arguments[3]!)!
let duration: TimeInterval = Double(command.arguments[4]!)!
let button = command.arguments[5]!

try! mouseController.drag(
start: CGPoint(x: x1, y: y1),
Expand Down
1 change: 1 addition & 0 deletions Sources/SendKeysLib/Commands/CommandsIterator.swift
Expand Up @@ -12,6 +12,7 @@ public class CommandsIterator: IteratorProtocol {
MouseMoveCommandMatcher(),
MouseClickCommandMatcher(),
MouseDragCommandMatcher(),
MouseScrollCommandMatcher(),
DefaultCommandMatcher()
]

Expand Down
2 changes: 1 addition & 1 deletion Sources/SendKeysLib/Commands/CommandsProcessor.swift
Expand Up @@ -38,7 +38,7 @@ public class CommandsProcessor {
shouldDefaultPause = false
} else if command.type == .stickyPause {
shouldDefaultPause = false
defaultPause = Double(command.arguments[0])!
defaultPause = Double(command.arguments[0]!)!
} else if shouldDefaultPause {
executeCommand(getDefaultPauseCommand())
shouldDefaultPause = true
Expand Down
19 changes: 19 additions & 0 deletions Sources/SendKeysLib/Commands/MouseScrollCommandMatcher.swift
@@ -0,0 +1,19 @@
import Foundation

public class MouseScrollCommandMatcher: CommandMatcher {
public init() {
super.init(try! NSRegularExpression(pattern: "\\<s:(-?\\d+),(-?\\d+)(:([.\\d]+))?\\>"))
}

override public func createCommand(_ arguments: [String?]) -> Command {
let x = arguments[1]
let y = arguments[2]
let duration = arguments[4]

return Command(.mouseScroll, [
x,
y,
duration
])
}
}
39 changes: 39 additions & 0 deletions Sources/SendKeysLib/MouseController.swift
@@ -1,6 +1,11 @@
import Foundation

class MouseController {
enum ScrollAxis {
case horizontal
case vertical
}

let animationRefreshInterval: TimeInterval = 0.01

func move(start: CGPoint, end: CGPoint, duration: TimeInterval) {
Expand Down Expand Up @@ -62,6 +67,40 @@ class MouseController {
upEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}

func scroll(_ delta: CGPoint, _ duration: TimeInterval) {
var scrolledX: Int = 0;
var scrolledY: Int = 0;

let animator = Animator(duration, animationRefreshInterval, { progress in
if delta.x != 0 {
let amount = Int((Double(delta.x) * progress) - Double(scrolledX))
scrolledX += amount

self.scrollBy(amount, .horizontal)
}
if delta.y != 0 {
let amount = Int((Double(delta.y) * progress) - Double(scrolledY))
scrolledY += amount

self.scrollBy(amount, .vertical)
}
})

animator.animate()
}

func scrollBy(_ amount: Int, _ axis: ScrollAxis) {
if #available(OSX 10.13, *) {
let event = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 1, wheel1: 0, wheel2: 0, wheel3: 0)
let field = axis == .vertical ? CGEventField.scrollWheelEventPointDeltaAxis1 : CGEventField.scrollWheelEventPointDeltaAxis2

event?.setIntegerValueField(field, value: Int64(amount * -1))
event?.post(tap: CGEventTapLocation.cghidEventTap)
} else {
fatalError("Scrolling is only available on 10.13 or later")
}
}

func getLocation() -> CGPoint? {
let event = CGEvent(source: nil);
return event?.location
Expand Down
36 changes: 36 additions & 0 deletions Tests/SendKeysTests/CommandIteratorTests.swift
Expand Up @@ -217,6 +217,42 @@ final class CommandIteratorTests: XCTestCase {
])
}

func testParsesMouseScroll() throws {
let commands = getCommands(CommandsIterator("<s:0,10>"))
XCTAssertEqual(
commands,
[
Command(CommandType.mouseScroll, ["0", "10", nil])
])
}

func testParsesMouseScrollWithNegativeAmount() throws {
let commands = getCommands(CommandsIterator("<s:-100,10>"))
XCTAssertEqual(
commands,
[
Command(CommandType.mouseScroll, ["-100", "10", nil])
])
}

func testParsesMouseScrollWithDuration() throws {
let commands = getCommands(CommandsIterator("<s:0,10:0.5>"))
XCTAssertEqual(
commands,
[
Command(CommandType.mouseScroll, ["0", "10", "0.5"])
])
}

func testParsesMouseScrollWithNegativeAmountAndDuration() throws {
let commands = getCommands(CommandsIterator("<s:0,-10:0.5>"))
XCTAssertEqual(
commands,
[
Command(CommandType.mouseScroll, ["0", "-10", "0.5"])
])
}

private func getCommands(_ iterator: CommandsIterator) -> [Command] {
var commands: [Command] = []

Expand Down

0 comments on commit b438e00

Please sign in to comment.