Skip to content

Commit

Permalink
feat: add mouse focus command
Browse files Browse the repository at this point in the history
  • Loading branch information
socsieng committed Jan 20, 2021
1 parent 8e41509 commit 1bbab2d
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 0 deletions.
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,33 @@ 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 focus

The mouse focus command can be used to draw attention to an area of the screen by moving the cursor in a circular
pattern. The mouse focus command uses the following markup:
`<mf:centerX,centerY:radiusX[,radiusY]:angleFrom,angleTo:duration>`

- `centerX` is required and represents the center x coordinate of the circular path.
- `centerY` is required and represents the center y coordinate of the circular path.
- `radiusX` is required and represents the size of the radius along the x axis of the circular path.
- `radiusY` is optional and represents the size of the radius along the y axis of the circular path. If omitted,
`radiusX` will be used indicating that the circular path will be a regular circle. An elipse can be achieved by having
different values for `radiusX` and `radiusY`.
- `angleFrom` is required and represents the start angle/position of the circular path. Angle is defined using degrees
where `0` represents 12 o'clock on an analog clock, and positive are applied in a clockwize direction. (e.g. 90
degrees is 3 o'clock).
- `angleTo` is required and represents the end angle/position of the circular path.
- `duration` is required and determines the number of seconds (supports partial seconds) used to complete the animation
between `angleFrom` to `angleTo`.

Example usage:

- `<mf:1000,200:50,20:180,900:2>`: Draws attention to position 1000, 200 by moving the mouse along an eliptical 50
pixels wide by 20 pixels high starting at the bottom (180 degrees) to 900 degrees (delta of 720 degrees) over a period
of 2 seconds.

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

#### 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/CommandFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class CommandFactory {
MouseScrollCommand.self,
MouseDownCommand.self,
MouseUpCommand.self,
MouseFocusCommand.self,
DefaultCommand.self,
]

Expand Down
66 changes: 66 additions & 0 deletions Sources/SendKeysLib/Commands/MouseFocusCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Foundation

public class MouseFocusCommand: MouseClickCommand {
public override class var commandType: CommandType { return .mouseScroll }

// <mf:centerX,centerY:radiusX[,radiusY]:angleFrom,angleTo:duration>
private static let _expression = try! NSRegularExpression(
pattern: "\\<mf:(-?\\d+),(-?\\d+):(\\d+)(,(\\d+))?:(-?[.\\d]+),(-?[.\\d]+):([.\\d]+)\\>")
public override class var expression: NSRegularExpression { return _expression }

var x: Int
var y: Int
var rx: Int
var ry: Int
var from: Double
var to: Double
var duration: TimeInterval

public init(x: Int, y: Int, rx: Int, ry: Int, angleFrom: Double, angleTo: Double, duration: TimeInterval) {
self.x = x
self.y = y
self.rx = rx
self.ry = ry
self.from = angleFrom
self.to = angleTo
self.duration = duration

super.init()
}

required public init(arguments: [String?]) {
self.x = Int(arguments[1]!)!
self.y = Int(arguments[2]!)!
self.rx = Int(arguments[3]!)!
self.ry = Int(arguments[5] ?? arguments[3]!)!
self.from = Double(arguments[6]!)!
self.to = Double(arguments[7]!)!
self.duration = TimeInterval(arguments[8]!)!

super.init()
}

public override func execute() throws {
mouseController!.circle(CGPoint(x: x, y: y), CGPoint(x: rx, y: ry), from, to, duration)
}

public override func describeMembers() -> String {
return "x: \(x), y: \(y), rx: \(rx), ry: \(ry), from: \(from), to: \(to), duration: \(duration)"
}

public override func equals(_ comparison: Command) -> Bool {
return super.equals(comparison)
&& {
if let command = comparison as? MouseFocusCommand {
return x == command.x
&& y == command.y
&& rx == command.rx
&& ry == command.ry
&& from == command.from
&& to == command.to
&& duration == command.duration
}
return false
}()
}
}
21 changes: 21 additions & 0 deletions Sources/SendKeysLib/MouseController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,27 @@ class MouseController {
animator.animate()
}

func circle(_ center: CGPoint, _ radius: CGPoint, _ fromAngle: Double, _ toAngle: Double, _ duration: TimeInterval)
{
let eventSource = CGEventSource(event: nil)
let ANGLE_OFFSET: Double = -90
let button = downButtons.first
let moveType = getEventType(.move, button)

let animator = Animator(
duration, animationRefreshInterval,
{ progress in
let angle = (toAngle - fromAngle) * progress + fromAngle + ANGLE_OFFSET
let location = CGPoint(
x: cos(angle * Double.pi / 180) * Double(radius.x) + Double(center.x),
y: sin(angle * Double.pi / 180) * Double(radius.y) + Double(center.y)
)
self.setLocation(location, eventSource: eventSource, moveType: moveType, button: .left, flags: [])
})

animator.animate()
}

func scrollBy(_ amount: Int, _ axis: ScrollAxis, eventSource: CGEventSource?, flags: CGEventFlags) {
if #available(OSX 10.13, *) {
let event = CGEvent(
Expand Down
36 changes: 36 additions & 0 deletions Tests/SendKeysTests/CommandIteratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,42 @@ final class CommandIteratorTests: XCTestCase {
])
}

func testParsesMouseFocus() throws {
let commands = getCommands(CommandsIterator("<mf:0,0:100,50:0,360:1>"))
XCTAssertEqual(
commands,
[
MouseFocusCommand(x: 0, y: 0, rx: 100, ry: 50, angleFrom: 0, angleTo: 360, duration: 1)
])
}

func testParsesMouseFocusWithSingleRadius() throws {
let commands = getCommands(CommandsIterator("<mf:0,0:100:0,360:0.1>"))
XCTAssertEqual(
commands,
[
MouseFocusCommand(x: 0, y: 0, rx: 100, ry: 100, angleFrom: 0, angleTo: 360, duration: 0.1)
])
}

func testParsesMouseFocusWithNegativeCoordinates() throws {
let commands = getCommands(CommandsIterator("<mf:-10,-20:100,50:0,360:1.5>"))
XCTAssertEqual(
commands,
[
MouseFocusCommand(x: -10, y: -20, rx: 100, ry: 50, angleFrom: 0, angleTo: 360, duration: 1.5)
])
}

func testParsesMouseFocusWithNegativeAngles() throws {
let commands = getCommands(CommandsIterator("<mf:-10,-20:100,50:100,-360:1.5>"))
XCTAssertEqual(
commands,
[
MouseFocusCommand(x: -10, y: -20, rx: 100, ry: 50, angleFrom: 100, angleTo: -360, duration: 1.5)
])
}

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

Expand Down
Binary file added docs/images/mouse-focus.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 1bbab2d

Please # to comment.