Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate modifier event handling #52

Merged
merged 3 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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