From bee0fbe5032a97fe63090fedf9cc7c9d537f60f6 Mon Sep 17 00:00:00 2001 From: Soc Sieng Date: Tue, 12 Jan 2021 17:58:10 -0800 Subject: [PATCH] feat: add support for triggering mouse up and down events independently --- README.md | 12 +- Sources/SendKeysLib/Commands/Command.swift | 2 + .../SendKeysLib/Commands/CommandFactory.swift | 2 + .../Commands/MouseDownCommand.swift | 34 +++++ .../SendKeysLib/Commands/MouseUpCommand.swift | 34 +++++ Sources/SendKeysLib/MouseController.swift | 130 +++++++++++++----- .../SendKeysTests/CommandIteratorTests.swift | 36 +++++ 7 files changed, 213 insertions(+), 37 deletions(-) create mode 100644 Sources/SendKeysLib/Commands/MouseDownCommand.swift create mode 100644 Sources/SendKeysLib/Commands/MouseUpCommand.swift diff --git a/README.md b/README.md index 94506a0..041a33b 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Example key combinations: - `command` + `a`: `` - `option` + `shift` + `left arrow`: `` -#### 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 @@ -173,6 +173,16 @@ Example usage: - ``: Scrolls up 400 pixels over 0.2 seconds. - ``: 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 `` and mouse up ``. + +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: +``. + ### Pauses The default time between keystrokes and instructions is determined by the `--delay`/`-d` argument (default value is diff --git a/Sources/SendKeysLib/Commands/Command.swift b/Sources/SendKeysLib/Commands/Command.swift index 67d0907..27f7e2d 100644 --- a/Sources/SendKeysLib/Commands/Command.swift +++ b/Sources/SendKeysLib/Commands/Command.swift @@ -11,6 +11,8 @@ public enum CommandType { case mouseClick case mouseDrag case mouseScroll + case mouseDown + case mouseUp case continuation } diff --git a/Sources/SendKeysLib/Commands/CommandFactory.swift b/Sources/SendKeysLib/Commands/CommandFactory.swift index 6a80f72..24d7bf7 100644 --- a/Sources/SendKeysLib/Commands/CommandFactory.swift +++ b/Sources/SendKeysLib/Commands/CommandFactory.swift @@ -11,6 +11,8 @@ public class CommandFactory { MouseClickCommand.self, MouseDragCommand.self, MouseScrollCommand.self, + MouseDownCommand.self, + MouseUpCommand.self, DefaultCommand.self, ] diff --git a/Sources/SendKeysLib/Commands/MouseDownCommand.swift b/Sources/SendKeysLib/Commands/MouseDownCommand.swift new file mode 100644 index 0000000..84cc9bc --- /dev/null +++ b/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: "\\") + 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) + ) + } +} diff --git a/Sources/SendKeysLib/Commands/MouseUpCommand.swift b/Sources/SendKeysLib/Commands/MouseUpCommand.swift new file mode 100644 index 0000000..febeeb4 --- /dev/null +++ b/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: "\\") + 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) + ) + } +} diff --git a/Sources/SendKeysLib/MouseController.swift b/Sources/SendKeysLib/MouseController.swift index 95357db..f5c8624 100644 --- a/Sources/SendKeysLib/MouseController.swift +++ b/Sources/SendKeysLib/MouseController.swift @@ -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() init(animationRefreshInterval: TimeInterval) { self.animationRefreshInterval = animationRefreshInterval @@ -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, @@ -24,7 +34,7 @@ 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() @@ -32,17 +42,8 @@ class MouseController { 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) @@ -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 @@ -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) { @@ -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, diff --git a/Tests/SendKeysTests/CommandIteratorTests.swift b/Tests/SendKeysTests/CommandIteratorTests.swift index ef19ac6..3b29a2c 100644 --- a/Tests/SendKeysTests/CommandIteratorTests.swift +++ b/Tests/SendKeysTests/CommandIteratorTests.swift @@ -407,6 +407,42 @@ final class CommandIteratorTests: XCTestCase { ]) } + func testParsesMouseDown() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + MouseDownCommand(button: "right", modifiers: []) + ]) + } + + func testParsesMouseDownWithModifiers() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + MouseDownCommand(button: "left", modifiers: ["shift", "command"]) + ]) + } + + func testParsesMouseUp() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + MouseUpCommand(button: "center", modifiers: []) + ]) + } + + func testParsesMouseUpWithModifiers() throws { + let commands = getCommands(CommandsIterator("")) + XCTAssertEqual( + commands, + [ + MouseUpCommand(button: "right", modifiers: ["option", "command"]) + ]) + } + private func getCommands(_ iterator: CommandsIterator) -> [Command] { var commands: [Command] = []