Skip to content

Commit

Permalink
feat: add mouse path command
Browse files Browse the repository at this point in the history
  • Loading branch information
socsieng committed Aug 31, 2021
1 parent a04fdf4 commit 6c5c0b9
Show file tree
Hide file tree
Showing 12 changed files with 1,036 additions and 8 deletions.
19 changes: 19 additions & 0 deletions README.md
Expand Up @@ -203,6 +203,25 @@ Example usage:

![mouse focus example](https://github.com/socsieng/sendkeys/raw/main/docs/images/mouse-focus.gif)

#### Mouse path

The mouse path command can be used move the mouse cursor along a path. The mouse path command uses the following markup:
`<mpath:path[:ofssetX,offsetY[,scaleX,scaleY]]:duration>`

- `path` is required and defines path for the mouse cursor to follow. The path is described using
[SVG Path data](https://www.w3.org/TR/SVG/paths.html#PathData)
- `ofssetX` and `offsetY` are optional and can be used to offset path coordinates by their respective `x` and `y`
values.
- `scaleX` and `scaleY` are also optional and can be used to scale path coordinates by their respective `x` and `y`
values.
- `duration` is required and determines the number of seconds (supports partial seconds) used to complete the animation
along the `path`.

Example usage:

- `<mpath:c0,40 200,40 200,0:2>`: Moves the mouse from its current position along a cubic bezier path with control
points `0,40` and `200,40` to the final position of `200,1`.

#### Mouse down and up

Mouse down and up events can be used to manually initiate a drag event or multiple mouse move commands while the mouse
Expand Down
1 change: 1 addition & 0 deletions Sources/SendKeysLib/Commands/Command.swift
Expand Up @@ -8,6 +8,7 @@ public enum CommandType {
case pause
case stickyPause
case mouseMove
case mousePath
case mouseClick
case mouseDrag
case mouseScroll
Expand Down
1 change: 1 addition & 0 deletions Sources/SendKeysLib/Commands/CommandFactory.swift
Expand Up @@ -8,6 +8,7 @@ public class CommandFactory {
ContinuationCommand.self,
NewlineCommand.self,
MouseMoveCommand.self,
MousePathCommand.self,
MouseClickCommand.self,
MouseDragCommand.self,
MouseScrollCommand.self,
Expand Down
74 changes: 74 additions & 0 deletions Sources/SendKeysLib/Commands/MousePathCommand.swift
@@ -0,0 +1,74 @@
import Foundation

public class MousePathCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mousePath }

private static let _expression = try! NSRegularExpression(
pattern: "\\<mpath:([^:\\>]+)(:(-?[\\d.]+),(-?[\\d.]+)(,(-?[\\d.]+),(-?[\\d.]+))?)?:([\\d.]+)(:([a-z,]+))?\\>")
public override class var expression: NSRegularExpression { return _expression }

var path: String
var offsetX: Double = 0
var offsetY: Double = 0
var scaleX: Double = 1
var scaleY: Double = 1
var duration: TimeInterval = 0

public init(
path: String, offsetX: Double, offsetY: Double, scaleX: Double, scaleY: Double, duration: TimeInterval,
modifiers: [String]
) {
self.path = path
self.offsetX = offsetX
self.offsetY = offsetY
self.scaleX = scaleX
self.scaleY = scaleY
self.duration = duration

super.init()
self.modifiers = modifiers
}

required public init(arguments: [String?]) {
self.path = arguments[1]!
self.offsetX = Double(arguments[3] ?? "0")!
self.offsetY = Double(arguments[4] ?? "0")!
self.scaleX = Double(arguments[6] ?? "1")!
self.scaleY = Double(arguments[7] ?? "1")!
self.duration = TimeInterval(arguments[8] ?? "0")!

super.init()
self.modifiers = arguments[9]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}

public override func execute() throws {
mouseController!.move(
start: nil,
path: path,
offset: CGPoint(x: offsetX, y: offsetY),
scale: CGPoint(x: scaleX, y: scaleY),
duration: duration,
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}

public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? MousePathCommand {
return path == command.path
&& offsetX == command.offsetX
&& offsetY == command.offsetY
&& scaleX == command.scaleX
&& scaleY == command.scaleY
&& duration == command.duration
}
return false
}()
}

public override func describeMembers() -> String {
return
"path: \(path), offsetX: \(offsetX), offsetY: \(offsetY), scaleX: \(scaleX), scaleY: \(scaleY), duration: \(duration)"
}
}
29 changes: 21 additions & 8 deletions Sources/SendKeysLib/MouseController.swift
Expand Up @@ -30,10 +30,26 @@ class MouseController {
let animator = Animator(
duration, animationRefreshInterval,
{ progress in
let location = CGPoint(
x: (Double(end.x - resolvedStart.x) * progress) + Double(resolvedStart.x),
y: (Double(end.y - resolvedStart.y) * progress) + Double(resolvedStart.y)
)
let location = CGFloat(progress) * (end - resolvedStart) + resolvedStart
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: button, flags: flags)
})

animator.animate()
}

func move(
start: CGPoint?, path: String, offset: CGPoint, scale: CGPoint, duration: TimeInterval, flags: CGEventFlags
) {
let resolvedStart = start ?? getLocation()!
let eventSource = CGEventSource(event: nil)
let button = downButtons.first
let moveType = getEventType(.move, button)
let pathData = PathData(path, resolvedStart)

let animator = Animator(
duration, animationRefreshInterval,
{ progress in
let location = offset + (pathData.getPointAtInterval(progress) * scale)
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: button, flags: flags)
})

Expand Down Expand Up @@ -91,10 +107,7 @@ class MouseController {
let animator = Animator(
duration, animationRefreshInterval,
{ progress in
let location = CGPoint(
x: (Double(end.x - resolvedStart.x) * progress) + Double(resolvedStart.x),
y: (Double(end.y - resolvedStart.y) * progress) + Double(resolvedStart.y)
)
let location = CGFloat(progress) * (end - resolvedStart) + resolvedStart
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: button, flags: flags)
})

Expand Down
27 changes: 27 additions & 0 deletions Sources/SendKeysLib/Path/Extensions.swift
@@ -0,0 +1,27 @@
import AppKit
import Foundation

extension CGPoint {
// Vector math
public static func + (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x + right.x, y: left.y + right.y)
}

public static func - (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x - right.x, y: left.y - right.y)
}

public static func * (left: CGFloat, right: CGPoint) -> CGPoint {
return CGPoint(x: left * right.x, y: left * right.y)
}

public static func * (left: CGPoint, right: CGPoint) -> CGPoint {
return CGPoint(x: left.x * right.x, y: left.y * right.y)
}

public func distance(from: CGPoint) -> Double {
let delta = self - from
let sqr = delta * delta
return sqrt(Double(sqr.x + sqr.y))
}
}

0 comments on commit 6c5c0b9

Please sign in to comment.