Skip to content

Commit

Permalink
Merge pull request #2 from jordanbaird/improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
jordanbaird authored May 2, 2022
2 parents a0afaa6 + 514d3ad commit c72e89f
Show file tree
Hide file tree
Showing 189 changed files with 435 additions and 200 deletions.
18 changes: 3 additions & 15 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,8 @@ MIT License

Copyright (c) 2022 Jordan Baird

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ let package = Package(

## Usage

Start by creating an instance of `KeyEvent` You then use it to initialize a `KeyRecorder` instance,
which will update the event's value whenever a new key combination is recorded. You can also observe
the event, and perform actions on both key-down and key-up.
Start by creating an instance of `KeyEvent`. Then, use it to initialize a `KeyRecorder` instance.
The recorder will stay synchronized with the key event, so that when it records a new key combination
the key event will update in accordance to the new value. You can also observe the event and perform
actions on both key-down and key-up.

```swift
let event = KeyEvent(name: "SomeEvent")
Expand Down
60 changes: 60 additions & 0 deletions Sources/SwiftKeys/Documentation.docc/Documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# ``SwiftKeys``

A straightforward global hotkey API for macOS.

## Overview

``SwiftKeys`` allows you to create, observe, and record global hotkeys in the form of the
``KeyEvent`` type.

Start by creating an instance of ``KeyEvent``. Then, use it to initialize a ``KeyRecorder``
instance. The recorder will stay synchronized with the key event, so that when it records a
new key combination the key event will update in accordance to the new value. You can also
observe the event and perform actions on both key-down and key-up.

```swift
let event = KeyEvent(name: "SomeEvent")
let recorder = KeyRecorder(keyEvent: event)

event.observe(.keyDown) {
print("DOWN")
}
event.observe(.keyUp) {
print("UP")
}
```
For improved type safety, you can create hard-coded key event names that can be referenced
across your app.

```swift
extension KeyEvent.Name {
static let showPreferences = Self("ShowPreferences")
}
let event = KeyEvent(name: .showPreferences)
```

Key events are automatically stored in the `UserDefaults` system, using their names as keys.
You can provide a custom prefix that will be combined with each name to create the keys.

```swift
extension KeyEvent.Name.Prefix {
public override var sharedPrefix: Self {
Self("SK")
}
}
```

The `showPreferences` name from above would become "SKShowPreferences" when used as a
`UserDefaults` key.

## Topics

### Creating and Observing Key Events

- ``KeyEvent``
- ``KeyEvent/Name-swift.struct``
- ``KeyEvent/Name-swift.struct/Prefix-swift.class``

### Recording Key Events

- ``KeyRecorder``
9 changes: 3 additions & 6 deletions Sources/SwiftKeys/EventProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
import Carbon.HIToolbox

final class EventProxy {
static var all = [UInt32: EventProxy]()

private static var eventHandlerRef: EventHandlerRef?
private static let eventTypes = [
EventTypeSpec(
Expand Down Expand Up @@ -107,7 +105,7 @@ final class EventProxy {
// with our signature), and that we have a stored proxy for the event.
guard
identifier.signature == EventProxy.signature,
let proxy = EventProxy.all[identifier.id]
let proxy = ProxyStorage.proxy(with: identifier.id)
else {
return OSStatus(eventNotHandledErr)
}
Expand Down Expand Up @@ -157,10 +155,10 @@ final class EventProxy {

// We need to retain a reference to each proxy instance. The C function
// inside of the `install()` method can't deal with objects, so we can't
// inject or reference `self`. We _do_ have a way to access the event's
// inject or reference `self`. We _do_ have a way to access the proxy's
// identifier, so we can use that to store the proxy, then access the
// storage from inside the C function.
Self.all[identifier.id] = self
ProxyStorage.store(self)
if status != noErr {
logError(.registrationFailed(code: status))
}
Expand All @@ -183,7 +181,6 @@ final class EventProxy {
return
}
let status = UnregisterEventHotKey(hotKeyRef)
Self.all.removeValue(forKey: identifier.id)
hotKeyRef = nil
if status != noErr {
logError(.unregistrationFailed(code: status))
Expand Down
48 changes: 45 additions & 3 deletions Sources/SwiftKeys/KeyEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,26 @@ public struct KeyEvent {
}

static var keyEventStorage = [Name: KeyEvent]()
static var proxyStorage = [Name: EventProxy]()
// static var proxyStorage = [Name: EventProxy]()

/// The name that is be used to store this key event.
public let name: Name

/// A Boolean value that indicates whether the key event is currently
/// enabled and active.
///
/// When enabled, the event's handlers will be executed whenever the
/// event is triggered.
///
/// - Note: If the event does not have a key or modifiers, it will not be
/// possible to enable it, even when calling the `enable()` method. If you
/// have created an event without these, and wish to enable it, you can
/// create a new event with the same name, and it will take the place of
/// the old event.
public var isEnabled: Bool {
proxy.isRegistered
}

/// The key associated with this key event.
public var key: Key? {
proxy.key
Expand All @@ -99,11 +114,11 @@ public struct KeyEvent {
}

var proxy: EventProxy {
if let proxy = Self.proxyStorage[name] {
if let proxy = ProxyStorage.proxy(with: name) {
return proxy
} else {
let proxy = EventProxy(name: name)
Self.proxyStorage[name] = proxy
ProxyStorage.store(proxy)
return proxy
}
}
Expand Down Expand Up @@ -170,6 +185,33 @@ public struct KeyEvent {
proxy.observations.append((type: type, handler: handler))
proxy.register()
}

/// Enables the key event.
///
/// When enabled, the key event's observation handlers become active, and will
/// execute whenever the event is triggered.
public func enable() {
proxy.register()
}

/// Disables the key event.
///
/// When disabled, the key event's observation handlers become dormant, but are
/// still retained, so that the event can be re-enabled later. If you wish to
/// completely remove the event and its handlers, use the `remove()` method instead.
public func disable() {
proxy.unregister()
}

/// Completely removes the key event and its handlers.
///
/// Once this method has been called, the key event should be considered invalid.
/// The `enable()` method will have no effect. If you wish to re-enable the event,
/// you will need to call `observe(_:handler:)` and provide a new handler.
public func remove() {
proxy.unregister()
ProxyStorage.remove(proxy)
}
}

extension KeyEvent: Codable { }
Expand Down
58 changes: 58 additions & 0 deletions Sources/SwiftKeys/ProxyStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//===----------------------------------------------------------------------===//
//
// ProxyStorage.swift
//
// Created: 2022. Author: Jordan Baird.
//
//===----------------------------------------------------------------------===//

struct ProxyStorage: Hashable {
private static var all = Set<Self>()

private let proxy: EventProxy

private var identifier: UInt32 {
proxy.identifier.id
}

private var name: KeyEvent.Name {
proxy.name
}

private init(_ proxy: EventProxy) {
self.proxy = proxy
}

static func proxy(with identifier: UInt32) -> EventProxy? {
all.first { $0.identifier == identifier }?.proxy
}

static func proxy(with name: KeyEvent.Name) -> EventProxy? {
all.first { $0.name == name }?.proxy
}

static func store(_ proxy: EventProxy) {
all.update(with: .init(proxy))
}

static func remove(_ proxy: EventProxy) {
if let storage = all.first(where: { $0 ~= proxy }) {
all.remove(storage)
}
}

func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
hasher.combine(name)
}

static func == (lhs: Self, rhs: Self) -> Bool {
lhs.identifier == rhs.identifier &&
lhs.name == rhs.name
}

static func ~= (lhs: Self, rhs: EventProxy) -> Bool {
lhs.identifier == rhs.identifier.id &&
lhs.name == rhs.name
}
}
18 changes: 18 additions & 0 deletions Tests/SwiftKeysTests/KeyEventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,22 @@ final class KeyEventTests: XCTestCase {
XCTAssert(event1.proxy.modifiers == [.option])
XCTAssertEqual(event1, event2)
}

func testEnable() {
let event = KeyEvent(name: "Soup", key: .a, modifiers: .option, .shift)

XCTAssert(!event.isEnabled)
event.observe(.keyDown) { }
XCTAssert(event.isEnabled)
event.disable()
XCTAssert(!event.isEnabled)

XCTAssertNotNil(event.key)
XCTAssert(!event.modifiers.isEmpty)

event.remove()

XCTAssertNil(event.key)
XCTAssert(event.modifiers.isEmpty)
}
}
23 changes: 23 additions & 0 deletions Tests/SwiftKeysTests/PrefixTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,29 @@ final class PrefixTests: XCTestCase {

XCTAssert(event2.key == .return)
XCTAssert(event2.modifiers == [.command, .shift, .option])

let stringLiteralPrefix: KeyEvent.Name.Prefix = "Prefix2"
XCTAssertEqual(stringLiteralPrefix.sharedPrefix, stringLiteralPrefix)
}

func testEqual() {
let prefix1 = KeyEvent.Name.Prefix("Hello")
let prefix2: KeyEvent.Name.Prefix = "Hello"
XCTAssertEqual(prefix1, prefix2)
}

func testNotEqual() {
let prefix1 = KeyEvent.Name.Prefix("Hello")
let prefix2 = KeyEvent.Name.Prefix("Goodbye")
XCTAssertNotEqual(prefix1, prefix2)
}

func testHashValue() {
let prefix1 = KeyEvent.Name.Prefix("Hello")
let prefix2: KeyEvent.Name.Prefix = "Hello"
let prefix3 = KeyEvent.Name.Prefix("Goodbye")
XCTAssertEqual(prefix1.hashValue, prefix2.hashValue)
XCTAssertNotEqual(prefix2.hashValue, prefix3.hashValue)
}
}

Expand Down
Loading

0 comments on commit c72e89f

Please sign in to comment.