Skip to content

Commit

Permalink
Separate modifier event handling (#52)
Browse files Browse the repository at this point in the history
* Separate modifier event handling

* Add modifier event handling tests

* swiftlint
  • Loading branch information
Econa77 committed May 14, 2020
1 parent 3320d51 commit 5e7a11c
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 68 deletions.
8 changes: 8 additions & 0 deletions Lib/Magnet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
091D85EC245A917500930473 /* KeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E8245A917500930473 /* KeyExtension.swift */; };
091D85ED245A917500930473 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E9245A917500930473 /* CollectionExtension.swift */; };
099F29FA246CFA9500992925 /* v2_0_0KeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F29F9246CFA9500992925 /* v2_0_0KeyCombo.swift */; };
099F2A02246D091400992925 /* ModifierEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F29FF246D091400992925 /* ModifierEventHandler.swift */; };
099F2A07246D094500992925 /* ModifierEventHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F2A04246D094500992925 /* ModifierEventHandlerTests.swift */; };
09DEE124245B128C00169BEC /* Sauce.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 091D85CF2459553A00930473 /* Sauce.framework */; };
FA3AA2162315A6A3007EAA1F /* CollectionExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3AA2152315A6A3007EAA1F /* CollectionExtensionTests.swift */; };
FAEC34B31C9059DF004177E2 /* Magnet.h in Headers */ = {isa = PBXBuildFile; fileRef = FAEC34B21C9059DF004177E2 /* Magnet.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand Down Expand Up @@ -54,6 +56,8 @@
091D85E8245A917500930473 /* KeyExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyExtension.swift; sourceTree = "<group>"; };
091D85E9245A917500930473 /* CollectionExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = "<group>"; };
099F29F9246CFA9500992925 /* v2_0_0KeyCombo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = v2_0_0KeyCombo.swift; sourceTree = "<group>"; };
099F29FF246D091400992925 /* ModifierEventHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifierEventHandler.swift; sourceTree = "<group>"; };
099F2A04246D094500992925 /* ModifierEventHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifierEventHandlerTests.swift; sourceTree = "<group>"; };
FA3AA2152315A6A3007EAA1F /* CollectionExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensionTests.swift; sourceTree = "<group>"; };
FAEC34AF1C9059DF004177E2 /* Magnet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Magnet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
FAEC34B21C9059DF004177E2 /* Magnet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Magnet.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -147,6 +151,7 @@
FAEC34D71C905B4D004177E2 /* HotKey.swift */,
FAEC34D91C905B5A004177E2 /* KeyCombo.swift */,
FAEC34DD1C905B7C004177E2 /* HotKeyCenter.swift */,
099F29FF246D091400992925 /* ModifierEventHandler.swift */,
FAEC34B21C9059DF004177E2 /* Magnet.h */,
FAEC34B41C9059DF004177E2 /* Info.plist */,
);
Expand All @@ -159,6 +164,7 @@
099F29F8246CFA8700992925 /* Fixtures */,
FA3AA2152315A6A3007EAA1F /* CollectionExtensionTests.swift */,
090077D3245D449F0099B20A /* KeyComboTests.swift */,
099F2A04246D094500992925 /* ModifierEventHandlerTests.swift */,
FAEC34C01C9059DF004177E2 /* Info.plist */,
);
path = MagnetTests;
Expand Down Expand Up @@ -302,6 +308,7 @@
FAEC34DE1C905B7C004177E2 /* HotKeyCenter.swift in Sources */,
091D85EA245A917500930473 /* NSEventExtension.swift in Sources */,
FAEC34D81C905B4D004177E2 /* HotKey.swift in Sources */,
099F2A02246D091400992925 /* ModifierEventHandler.swift in Sources */,
091D85EC245A917500930473 /* KeyExtension.swift in Sources */,
091D85ED245A917500930473 /* CollectionExtension.swift in Sources */,
091D85EB245A917500930473 /* IntExtension.swift in Sources */,
Expand All @@ -312,6 +319,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
099F2A07246D094500992925 /* ModifierEventHandlerTests.swift in Sources */,
090077D4245D449F0099B20A /* KeyComboTests.swift in Sources */,
099F29FA246CFA9500992925 /* v2_0_0KeyCombo.swift in Sources */,
FA3AA2162315A6A3007EAA1F /* CollectionExtensionTests.swift in Sources */,
Expand Down
84 changes: 17 additions & 67 deletions Lib/Magnet/HotKeyCenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ public final class HotKeyCenter {

private var hotKeys = [String: HotKey]()
private var hotKeyCount: UInt32 = 0
private var tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0)
private var multiModifiers = false
private var lastHandledEventTimeStamp: TimeInterval?
private let modifierEventHandler: ModifierEventHandler
private let notificationCenter: NotificationCenter

// MARK: - Initialize
init(notificationCenter: NotificationCenter = .default) {
init(modifierEventHandler: ModifierEventHandler = .init(), notificationCenter: NotificationCenter = .default) {
self.modifierEventHandler = modifierEventHandler
self.notificationCenter = notificationCenter
installHotKeyPressedEventHandler()
installModifiersChangedEventHandlerIfNeeded()
Expand All @@ -46,8 +45,9 @@ public extension HotKeyCenter {

hotKeys[hotKey.identifier] = hotKey
guard !hotKey.keyCombo.doubledModifiers else { return true }
// Normal macOS shortcut
/*
* Normal macOS shortcut
*
* Discussion:
* When registering a hotkey, a KeyCode that conforms to the
* keyboard layout at the time of registration is registered.
Expand All @@ -67,7 +67,7 @@ public extension HotKeyCenter {
GetEventDispatcherTarget(),
0,
&carbonHotKey)
if error != 0 {
guard error == noErr else {
unregister(with: hotKey)
return false
}
Expand Down Expand Up @@ -120,11 +120,11 @@ private extension HotKeyCenter {
pressedEventType.eventClass = OSType(kEventClassKeyboard)
pressedEventType.eventKind = OSType(kEventHotKeyPressed)
InstallEventHandler(GetEventDispatcherTarget(), { _, inEvent, _ -> OSStatus in
return HotKeyCenter.shared.sendCarbonEvent(inEvent!)
return HotKeyCenter.shared.sendPressedKeyboardEvent(inEvent!)
}, 1, &pressedEventType, nil, nil)
}

func sendCarbonEvent(_ event: EventRef) -> OSStatus {
func sendPressedKeyboardEvent(_ event: EventRef) -> OSStatus {
assert(Int(GetEventClass(event)) == kEventClassKeyboard, "Unknown event class")

var hotKeyId = EventHotKeyID()
Expand All @@ -136,85 +136,35 @@ private extension HotKeyCenter {
nil,
&hotKeyId)

if error != 0 { return error }

guard error == noErr else { return error }
assert(hotKeyId.signature == UTGetOSTypeFromString("Magnet" as CFString), "Invalid hot key id")

let hotKey = hotKeys.values.first(where: { $0.hotKeyId == hotKeyId.id })
switch GetEventKind(event) {
case EventParamName(kEventHotKeyPressed):
hotKeyDown(hotKey)
hotKey?.invoke()
default:
assert(false, "Unknown event kind")
}
return noErr
}

func hotKeyDown(_ hotKey: HotKey?) {
guard let hotKey = hotKey else { return }
hotKey.invoke()
}
}

// MARK: - Double Tap Modifier Event
private extension HotKeyCenter {
func installModifiersChangedEventHandlerIfNeeded() {
NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in
self?.sendModifiersChangeEvent(event)
self?.modifierEventHandler.handleModifiersEvent(with: event.modifierFlags, timestamp: event.timestamp)
}
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event -> NSEvent? in
self?.sendModifiersChangeEvent(event)
self?.modifierEventHandler.handleModifiersEvent(with: event.modifierFlags, timestamp: event.timestamp)
return event
}
}

func sendModifiersChangeEvent(_ event: NSEvent) {
guard lastHandledEventTimeStamp != event.timestamp else { return }
lastHandledEventTimeStamp = event.timestamp

let modifierFlags = event.modifierFlags
let commandTapped = modifierFlags.contains(.command)
let shiftTapped = modifierFlags.contains(.shift)
let controlTapped = modifierFlags.contains(.control)
let optionTapped = modifierFlags.contains(.option)
let modifiersCount = [commandTapped, optionTapped, shiftTapped, controlTapped].trueCount
guard modifiersCount != 0 else { return }
guard modifiersCount == 1 else {
multiModifiers = true
return
modifierEventHandler.doubleTapped = { [weak self] tappedModifierFlags in
self?.hotKeys.values
.filter { $0.keyCombo.doubledModifiers }
.filter { $0.keyCombo.modifiers == tappedModifierFlags.carbonModifiers() }
.forEach { $0.invoke() }
}
guard !multiModifiers else {
multiModifiers = false
return
}
if (tappedModifierKey.contains(.command) && commandTapped) ||
(tappedModifierKey.contains(.shift) && shiftTapped) ||
(tappedModifierKey.contains(.control) && controlTapped) ||
(tappedModifierKey.contains(.option) && optionTapped) {
doubleTapped(with: tappedModifierKey.carbonModifiers())
tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0)
} else {
if commandTapped {
tappedModifierKey = .command
} else if shiftTapped {
tappedModifierKey = .shift
} else if controlTapped {
tappedModifierKey = .control
} else if optionTapped {
tappedModifierKey = .option
} else {
tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0)
}
}
// Clean Flag
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: { [weak self] in
self?.tappedModifierKey = NSEvent.ModifierFlags(rawValue: 0)
})
}

func doubleTapped(with key: Int) {
hotKeys.values
.filter { $0.keyCombo.doubledModifiers && $0.keyCombo.modifiers == key }
.forEach { $0.invoke() }
}
}
3 changes: 2 additions & 1 deletion Lib/Magnet/KeyCombo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,10 @@ public final class KeyCombo: NSObject, NSCopying, NSCoding, Codable {
}

public init?(doubledCocoaModifiers modifiers: NSEvent.ModifierFlags) {
let filterdCocoaModifiers = modifiers.filterUnsupportModifiers()
guard modifiers.isSingleFlags else { return nil }
self.key = .a
self.modifiers = modifiers.carbonModifiers()
self.modifiers = filterdCocoaModifiers.carbonModifiers()
self.doubledModifiers = true
}

Expand Down
72 changes: 72 additions & 0 deletions Lib/Magnet/ModifierEventHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// ModifierEventHandler.swift
//
// Magnet
// GitHub: https://github.com/clipy
// HP: https://clipy-app.com
//
// Copyright © 2015-2020 Clipy Project.
//

import Cocoa

public final class ModifierEventHandler {

// MARK: - Properties
public var doubleTapped: ((NSEvent.ModifierFlags) -> Void)?

private var tappingModifierFlags = NSEvent.ModifierFlags()
private var isTappingMultiModifiers = false
private var lastHandledEventTimestamp: TimeInterval?
private let cleanTimeInterval: DispatchTimeInterval
private let cleanQueue: DispatchQueue

// MARK: - Initialize
init(cleanTimeInterval: DispatchTimeInterval = .milliseconds(300), cleanQueue: DispatchQueue = .main) {
self.cleanTimeInterval = cleanTimeInterval
self.cleanQueue = cleanQueue
}

}

// MARK: - Handling
public extension ModifierEventHandler {
func handleModifiersEvent(with modifierFlags: NSEvent.ModifierFlags, timestamp: TimeInterval) {
guard lastHandledEventTimestamp != timestamp else { return }
lastHandledEventTimestamp = timestamp

handleDoubleTapModifierEvent(modifierFlags: modifierFlags)
}

private func handleDoubleTapModifierEvent(modifierFlags: NSEvent.ModifierFlags) {
let tappedModifierFlags = modifierFlags.filterUnsupportModifiers()
let commandTapped = tappedModifierFlags.contains(.command)
let shiftTapped = tappedModifierFlags.contains(.shift)
let controlTapped = tappedModifierFlags.contains(.control)
let optionTapped = tappedModifierFlags.contains(.option)
let tappedModifierCount = [commandTapped, shiftTapped, controlTapped, optionTapped].trueCount
guard tappedModifierCount != 0 else { return }
guard tappedModifierCount == 1 else {
isTappingMultiModifiers = true
return
}
guard !isTappingMultiModifiers else {
isTappingMultiModifiers = false
return
}
if (tappingModifierFlags.contains(.command) && commandTapped) ||
(tappingModifierFlags.contains(.shift) && shiftTapped) ||
(tappingModifierFlags.contains(.control) && controlTapped) ||
(tappingModifierFlags.contains(.option) && optionTapped) {
doubleTapped?(tappingModifierFlags)
tappingModifierFlags = NSEvent.ModifierFlags()
} else {
tappingModifierFlags = tappedModifierFlags
}

// After a certain amount of time, the tapped modifier will be reset.
cleanQueue.asyncAfter(deadline: .now() + cleanTimeInterval) { [weak self] in
self?.tappingModifierFlags = NSEvent.ModifierFlags()
}
}
}
Loading

0 comments on commit 5e7a11c

Please sign in to comment.