Skip to content

Commit

Permalink
feat: add support for triggering mouse up and down events independently
Browse files Browse the repository at this point in the history
  • Loading branch information
socsieng committed Jan 16, 2021
1 parent d984261 commit bee0fbe
Show file tree
Hide file tree
Showing 7 changed files with 213 additions and 37 deletions.
12 changes: 11 additions & 1 deletion README.md
Expand Up @@ -80,7 +80,7 @@ Example key combinations:
- `command` + `a`: `<c:a:command>`
- `option` + `shift` + `left arrow`: `<c:left:option,shift>`

#### Key up and down
#### Key down and up

Some applications expect modifier keys to be pressed explicitly before invoking actions like mouse click. An example of
this is Pixelmator which expect the `option` key to be pressed before executing the alternate click action. This can be
Expand Down Expand Up @@ -173,6 +173,16 @@ Example usage:
- `<s:0,-100:0.2>`: Scrolls up 400 pixels over 0.2 seconds.
- `<s:100,0>`: Scrolls 100 pixel to the right instantly.

#### 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
button is down. This can be achieved with mouse down `<md:button[:modifiers]>` and mouse up `<mu:button[:modifiers]>`.

Note that the drag command is recommended for basic drag functionality..

An example of how include multiple mouse movements while the mouse button is down:
`<md:left><m:0,0,100,0:1><m:100,100:1><mu:left>`.

### Pauses

The default time between keystrokes and instructions is determined by the `--delay`/`-d` argument (default value is
Expand Down
2 changes: 2 additions & 0 deletions Sources/SendKeysLib/Commands/Command.swift
Expand Up @@ -11,6 +11,8 @@ public enum CommandType {
case mouseClick
case mouseDrag
case mouseScroll
case mouseDown
case mouseUp
case continuation
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/SendKeysLib/Commands/CommandFactory.swift
Expand Up @@ -11,6 +11,8 @@ public class CommandFactory {
MouseClickCommand.self,
MouseDragCommand.self,
MouseScrollCommand.self,
MouseDownCommand.self,
MouseUpCommand.self,
DefaultCommand.self,
]

Expand Down
34 changes: 34 additions & 0 deletions Sources/SendKeysLib/Commands/MouseDownCommand.swift
@@ -0,0 +1,34 @@
import Foundation

public class MouseDownCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mouseDown }

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

override init() {
super.init()
}

public init(button: String?, modifiers: [String]) {
super.init()

self.button = button
self.modifiers = modifiers
}

required public init(arguments: [String?]) {
super.init()

self.button = arguments[1]!
self.modifiers = arguments[3]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}

public override func execute() throws {
try! mouseController!.down(
nil,
button: getMouseButton(button: button!),
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}
}
34 changes: 34 additions & 0 deletions Sources/SendKeysLib/Commands/MouseUpCommand.swift
@@ -0,0 +1,34 @@
import Foundation

public class MouseUpCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mouseUp }

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

override init() {
super.init()
}

public init(button: String?, modifiers: [String]) {
super.init()

self.button = button
self.modifiers = modifiers
}

required public init(arguments: [String?]) {
super.init()

self.button = arguments[1]!
self.modifiers = arguments[3]?.components(separatedBy: ",").filter({ !$0.isEmpty }) ?? []
}

public override func execute() throws {
try! mouseController!.up(
nil,
button: getMouseButton(button: button!),
flags: try! KeyPresser.getModifierFlags(modifiers)
)
}
}
130 changes: 94 additions & 36 deletions Sources/SendKeysLib/MouseController.swift
Expand Up @@ -6,8 +6,16 @@ class MouseController {
case vertical
}

enum mouseEventType {
case up
case down
case move
case drag
}

let animationRefreshInterval: TimeInterval
let keyPresser = KeyPresser()
var downButtons = Set<CGMouseButton>()

init(animationRefreshInterval: TimeInterval) {
self.animationRefreshInterval = animationRefreshInterval
Expand All @@ -16,6 +24,8 @@ class MouseController {
func move(start: CGPoint?, end: CGPoint, duration: TimeInterval, flags: CGEventFlags) {
let resolvedStart = start ?? getLocation()!
let eventSource = CGEventSource(event: nil)
let button = downButtons.first
let moveType = getEventType(.move, button)

let animator = Animator(
duration, animationRefreshInterval,
Expand All @@ -24,25 +34,16 @@ class MouseController {
x: (Double(end.x - resolvedStart.x) * progress) + Double(resolvedStart.x),
y: (Double(end.y - resolvedStart.y) * progress) + Double(resolvedStart.y)
)
self.setLocation(location, eventSource: eventSource, flags: flags)
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: button, flags: flags)
})

animator.animate()
}

func click(_ location: CGPoint?, button: CGMouseButton, flags: CGEventFlags, clickCount: Int) {
let resolvedLocation = location ?? getLocation()!

var downMouseType = CGEventType.leftMouseDown
var upMouseType = CGEventType.leftMouseUp

if button == .right {
downMouseType = CGEventType.rightMouseDown
upMouseType = CGEventType.rightMouseUp
} else if button != .left {
downMouseType = CGEventType.otherMouseDown
upMouseType = CGEventType.otherMouseUp
}
let downMouseType = getEventType(.down, button)
let upMouseType = getEventType(.up, button)

let downEvent = CGEvent(
mouseEventSource: nil, mouseType: downMouseType, mouseCursorPosition: resolvedLocation, mouseButton: button)
Expand All @@ -57,14 +58,36 @@ class MouseController {
upEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}

func down(_ location: CGPoint?, button: CGMouseButton, flags: CGEventFlags) {
let resolvedLocation = location ?? getLocation()!
let downMouseType = getEventType(.down, button)

let downEvent = CGEvent(
mouseEventSource: nil, mouseType: downMouseType, mouseCursorPosition: resolvedLocation, mouseButton: button)
downEvent?.flags = flags
downEvent?.post(tap: CGEventTapLocation.cghidEventTap)

downButtons.insert(button)
}

func up(_ location: CGPoint?, button: CGMouseButton, flags: CGEventFlags) {
let resolvedLocation = location ?? getLocation()!
let upMouseType = getEventType(.up, button)

let upEvent = CGEvent(
mouseEventSource: nil, mouseType: upMouseType, mouseCursorPosition: resolvedLocation,
mouseButton: button)
upEvent?.post(tap: CGEventTapLocation.cghidEventTap)
downButtons.remove(button)
}

func drag(start: CGPoint?, end: CGPoint, duration: TimeInterval, button: CGMouseButton, flags: CGEventFlags) {
let resolvedStart = start ?? getLocation()!
let downMouseType = getEventType(.down, button)
let upMouseType = getEventType(.up, button)
let moveType = getEventType(.drag, button)
var eventSource: CGEventSource?

var downMouseType = CGEventType.leftMouseDown
var upMouseType = CGEventType.leftMouseUp
var moveType = CGEventType.leftMouseDragged

let animator = Animator(
duration, animationRefreshInterval,
{ progress in
Expand All @@ -75,27 +98,22 @@ class MouseController {
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: button, flags: flags)
})

if button == .right {
downMouseType = CGEventType.rightMouseDown
upMouseType = CGEventType.rightMouseUp
moveType = CGEventType.rightMouseDragged
} else if button != .left {
downMouseType = CGEventType.otherMouseDown
upMouseType = CGEventType.otherMouseUp
moveType = CGEventType.otherMouseDragged
if !downButtons.contains(button) {
let downEvent = CGEvent(
mouseEventSource: nil, mouseType: downMouseType, mouseCursorPosition: resolvedStart, mouseButton: button
)
downEvent?.flags = flags
downEvent?.post(tap: CGEventTapLocation.cghidEventTap)
eventSource = CGEventSource(event: downEvent)
}

let downEvent = CGEvent(
mouseEventSource: nil, mouseType: downMouseType, mouseCursorPosition: resolvedStart, mouseButton: button)
downEvent?.flags = flags
downEvent?.post(tap: CGEventTapLocation.cghidEventTap)
eventSource = CGEventSource(event: downEvent)

animator.animate()

let upEvent = CGEvent(
mouseEventSource: eventSource, mouseType: upMouseType, mouseCursorPosition: end, mouseButton: button)
upEvent?.post(tap: CGEventTapLocation.cghidEventTap)
if !downButtons.contains(button) {
let upEvent = CGEvent(
mouseEventSource: eventSource, mouseType: upMouseType, mouseCursorPosition: end, mouseButton: button)
upEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}
}

func scroll(_ delta: CGPoint, _ duration: TimeInterval, flags: CGEventFlags) {
Expand Down Expand Up @@ -147,15 +165,55 @@ class MouseController {

private func setLocation(
_ location: CGPoint, eventSource: CGEventSource?, moveType: CGEventType = CGEventType.mouseMoved,
button: CGMouseButton = CGMouseButton.left, flags: CGEventFlags = []
button: CGMouseButton? = nil, flags: CGEventFlags = []
) {
let moveEvent = CGEvent(
mouseEventSource: eventSource, mouseType: moveType, mouseCursorPosition: location, mouseButton: button)
mouseEventSource: eventSource, mouseType: moveType, mouseCursorPosition: location,
mouseButton: button ?? CGMouseButton.left)
moveEvent?.flags = flags
moveEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}

func resolveLocation(_ location: CGPoint) -> CGPoint {
private func getEventType(_ mouseType: mouseEventType, _ button: CGMouseButton? = nil) -> CGEventType {
switch mouseType {
case .up:
if button == CGMouseButton.left {
return CGEventType.leftMouseUp
} else if button == CGMouseButton.right {
return CGEventType.rightMouseUp
} else {
return CGEventType.otherMouseUp
}
case .down:
if button == CGMouseButton.left {
return CGEventType.leftMouseDown
} else if button == CGMouseButton.right {
return CGEventType.rightMouseDown
} else {
return CGEventType.otherMouseDown
}
case .move:
if button == nil {
return CGEventType.mouseMoved
} else if button == CGMouseButton.left {
return CGEventType.leftMouseDragged
} else if button == CGMouseButton.right {
return CGEventType.rightMouseDragged
} else {
return CGEventType.otherMouseDragged
}
case .drag:
if button == CGMouseButton.left {
return CGEventType.leftMouseDragged
} else if button == CGMouseButton.right {
return CGEventType.rightMouseDragged
} else {
return CGEventType.otherMouseDragged
}
}
}

private func resolveLocation(_ location: CGPoint) -> CGPoint {
let currentLocation = getLocation()
return CGPoint(
x: location.x < 0 ? (currentLocation?.x ?? 0) : location.x,
Expand Down
36 changes: 36 additions & 0 deletions Tests/SendKeysTests/CommandIteratorTests.swift
Expand Up @@ -407,6 +407,42 @@ final class CommandIteratorTests: XCTestCase {
])
}

func testParsesMouseDown() throws {
let commands = getCommands(CommandsIterator("<md:right>"))
XCTAssertEqual(
commands,
[
MouseDownCommand(button: "right", modifiers: [])
])
}

func testParsesMouseDownWithModifiers() throws {
let commands = getCommands(CommandsIterator("<md:left:shift,command>"))
XCTAssertEqual(
commands,
[
MouseDownCommand(button: "left", modifiers: ["shift", "command"])
])
}

func testParsesMouseUp() throws {
let commands = getCommands(CommandsIterator("<mu:center>"))
XCTAssertEqual(
commands,
[
MouseUpCommand(button: "center", modifiers: [])
])
}

func testParsesMouseUpWithModifiers() throws {
let commands = getCommands(CommandsIterator("<mu:right:option,command>"))
XCTAssertEqual(
commands,
[
MouseUpCommand(button: "right", modifiers: ["option", "command"])
])
}

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

Expand Down

0 comments on commit bee0fbe

Please sign in to comment.