Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ public struct StandardKeyEvent {
}

public var displayString: String {
EventTransformer.shared.transform(inputEvent)
}

public var isCommand: Bool {
!self.modifierFlags.intersection([.control, .command]).isEmpty
EventTransformer.shared.transform(self.inputEvent)
}

public var isModified: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ enum KeyboardGlyphCatalog {
static let control = KeyboardModifierKey.Kind.control.glyph

static let tab = UnicodeToken.tab.string
static let backTab = UnicodeToken.backTab.string

/// Glyphs that can prefix a chord in display strings.
static let modifierSymbols: [String] = KeyboardModifierKey.Kind.allCases.map(\.glyph)
Expand Down
33 changes: 20 additions & 13 deletions Apps/Keyty/Sources/Keyty/Domain/Keyboard/KeyboardModifierKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ extension KeyboardModifierKey {
}

static func keys(in flags: NSEvent.ModifierFlags) -> Set<KeyboardModifierKey> {
let rawValue = flags.rawValue
return Set(Self.deviceModifierKeys.compactMap { mask, key in
rawValue & mask == 0 ? nil : key
})
Set(Self.all.filter { flags.rawValue & $0.deviceMask != 0 })
}
}

Expand All @@ -71,14 +68,24 @@ extension KeyboardModifierKey {
static let leftControl = KeyboardModifierKey(.control, location: .left)
static let rightControl = KeyboardModifierKey(.control, location: .right)

private static let deviceModifierKeys: [(mask: UInt, key: KeyboardModifierKey)] = [
(UInt(NX_DEVICELCMDKEYMASK), .leftCommand),
(UInt(NX_DEVICERCMDKEYMASK), .rightCommand),
(UInt(NX_DEVICELSHIFTKEYMASK), .leftShift),
(UInt(NX_DEVICERSHIFTKEYMASK), .rightShift),
(UInt(NX_DEVICELALTKEYMASK), .leftOption),
(UInt(NX_DEVICERALTKEYMASK), .rightOption),
(UInt(NX_DEVICELCTLKEYMASK), .leftControl),
(UInt(NX_DEVICERCTLKEYMASK), .rightControl),
static let all: [KeyboardModifierKey] = [
.leftCommand, .rightCommand,
.leftShift, .rightShift,
.leftOption, .rightOption,
.leftControl, .rightControl
]

// The device-dependent bit macOS sets for this specific physical key.
var deviceMask: UInt {
switch (self.kind, self.location) {
case (.command, .left): return UInt(NX_DEVICELCMDKEYMASK)
case (.command, .right): return UInt(NX_DEVICERCMDKEYMASK)
case (.shift, .left): return UInt(NX_DEVICELSHIFTKEYMASK)
case (.shift, .right): return UInt(NX_DEVICERSHIFTKEYMASK)
case (.option, .left): return UInt(NX_DEVICELALTKEYMASK)
case (.option, .right): return UInt(NX_DEVICERALTKEYMASK)
case (.control, .left): return UInt(NX_DEVICELCTLKEYMASK)
case (.control, .right): return UInt(NX_DEVICERCTLKEYMASK)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ enum UnicodeToken {
static let control: Unicode.Scalar = "\u{2303}"

static let tab: Unicode.Scalar = "\u{21E5}"
static let backTab: Unicode.Scalar = "\u{21E4}"
static let escape: Unicode.Scalar = "\u{238B}"
static let delete: Unicode.Scalar = "\u{232B}"
static let keypadClear: Unicode.Scalar = "\u{2327}"
Expand Down
101 changes: 27 additions & 74 deletions Apps/Keyty/Sources/Keyty/Services/EventPipeline/EventTransformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,97 +26,32 @@ public final class EventTransformer {
public func transform(_ event: InputEvent) -> String {
switch event {
case .keystroke(let keystroke):
return transform(keystroke)
return self.transform(keystroke)
case .mouse(let mouseEvent):
return transform(mouseEvent)
return self.transform(mouseEvent)
case .mediaKey(let mediaKey):
return transform(mediaKey)
return self.transform(mediaKey)
}
}

private func shouldReturnOriginalCharacters(keyCode: UInt16, characters: String?) -> Bool {
keyCode == KeyboardKeyCode.minus.rawValue && characters == "ß"
}

private func modifierPrefix(
for modifiers: NSEvent.ModifierFlags,
includesDeferredShift: Bool = true
) -> String {
let hasOptionModifier = modifiers.contains(.option)
let hasShiftModifier = modifiers.contains(.shift)
let usesShortcutStyle = !modifiers.intersection([.control, .command]).isEmpty
var needsShiftGlyph = false
var response = ""

if modifiers.contains(.control) {
response += KeyboardGlyphCatalog.control
}

if hasOptionModifier {
response += KeyboardGlyphCatalog.option
}

if hasShiftModifier {
if usesShortcutStyle || hasOptionModifier {
response += KeyboardGlyphCatalog.shift
} else {
needsShiftGlyph = true
}
}

if modifiers.contains(.command) {
if needsShiftGlyph {
response += KeyboardGlyphCatalog.shift
needsShiftGlyph = false
}
response += KeyboardGlyphCatalog.command
}

if needsShiftGlyph && includesDeferredShift {
response += KeyboardGlyphCatalog.shift
}

return response
}
}

// MARK: - Event Transforms
private extension EventTransformer {
func transform(_ keystroke: StandardKeyEvent) -> String {
if let glyph = InputEventGlyphMapper.glyph(for: keystroke.inputEvent) {
return glyph
}

let modifiers = keystroke.modifierFlags
let hasOptionModifier = modifiers.contains(.option)
let hasShiftModifier = modifiers.contains(.shift)
let isCommand = !modifiers.intersection([.control, .command]).isEmpty
var response = self.modifierPrefix(for: modifiers, includesDeferredShift: false)

if hasShiftModifier && !keystroke.isCommand && !hasOptionModifier && keystroke.keyCode == KeyboardKeyCode.tab.rawValue {
response += KeyboardGlyphCatalog.backTab
return response
}

if hasShiftModifier && !isCommand && !hasOptionModifier {
response += KeyboardGlyphCatalog.shift
}
var response = self.modifierPrefix(for: keystroke.modifierFlags)

if let specialKeyString = KeyboardSpecialKeyResolver.displayText(for: keystroke) {
response += specialKeyString
return response
return response + specialKeyString
}

if isCommand,
shouldReturnOriginalCharacters(keyCode: keystroke.keyCode, characters: keystroke.characters) {
response += keystroke.characters ?? ""
} else {
response += uchrData.translatedKeyCode(keystroke.keyCode)
}
response += self.uchrData.translatedKeyCode(keystroke.keyCode)

if isCommand || hasShiftModifier || hasOptionModifier {
if keystroke.keyCode != KeyboardKeyCode.minus.rawValue {
response = response.uppercased()
}
if keystroke.isModified {
response = self.legendCased(response)
}

return response
Expand All @@ -132,3 +67,21 @@ private extension EventTransformer {
InputEventGlyphMapper.mediaKeyGlyph(for: mediaKey)
}
}

// MARK: - Display Formatting
private extension EventTransformer {
// Uppercases a legend only when the result keeps its length.
private func legendCased(_ text: String) -> String {
let uppercased = text.uppercased()
return uppercased.count == text.count ? uppercased : text
}


// The glyphs for the held modifiers, in Apple's canonical display order.
private func modifierPrefix(for modifiers: NSEvent.ModifierFlags) -> String {
KeyboardModifierKey.Kind.canonicalDisplayOrder
.filter { modifiers.contains($0.flag) }
.map(\.glyph)
.joined()
}
}
23 changes: 9 additions & 14 deletions Apps/Keyty/Tests/KeytyTests/Domain/Events/MouseEventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,18 @@ import XCTest

final class MouseEventTests: XCTestCase {
func testKindClassifiesButtonsAndScrollDirections() {
XCTAssertEqual(TestMouseEvents.make(type: .leftMouseDown, buttonNumber: 0).kind, .leftButton)
XCTAssertEqual(TestMouseEvents.make(type: .rightMouseDown, buttonNumber: 1).kind, .rightButton)
XCTAssertEqual(TestMouseEvents.make(type: .otherMouseDown, buttonNumber: 2).kind, .middleButton)
XCTAssertEqual(TestMouseEvents.make(type: .otherMouseDown, buttonNumber: 3).kind, .otherButton(4))
XCTAssertEqual(makeScrollEvent(deltaX: 0, deltaY: 1).kind, .wheelUp)
XCTAssertEqual(makeScrollEvent(deltaX: 0, deltaY: -1).kind, .wheelDown)
XCTAssertEqual(makeScrollEvent(deltaX: -1, deltaY: 0).kind, .wheelLeft)
XCTAssertEqual(makeScrollEvent(deltaX: 1, deltaY: 0).kind, .wheelRight)
XCTAssertEqual(MouseEvent.stub(type: .leftMouseDown, buttonNumber: 0).kind, .leftButton)
XCTAssertEqual(MouseEvent.stub(type: .rightMouseDown, buttonNumber: 1).kind, .rightButton)
XCTAssertEqual(MouseEvent.stub(type: .otherMouseDown, buttonNumber: 2).kind, .middleButton)
XCTAssertEqual(MouseEvent.stub(type: .otherMouseDown, buttonNumber: 3).kind, .otherButton(4))
XCTAssertEqual(MouseEvent.scrollStub(deltaX: 0, deltaY: 1).kind, .wheelUp)
XCTAssertEqual(MouseEvent.scrollStub(deltaX: 0, deltaY: -1).kind, .wheelDown)
XCTAssertEqual(MouseEvent.scrollStub(deltaX: -1, deltaY: 0).kind, .wheelLeft)
XCTAssertEqual(MouseEvent.scrollStub(deltaX: 1, deltaY: 0).kind, .wheelRight)
}

func testKindTreatsZeroDeltaScrollEventAsGeneric() {
XCTAssertEqual(makeScrollEvent(deltaX: 0, deltaY: 0).kind, .generic)
XCTAssertEqual(MouseEvent.scrollStub(deltaX: 0, deltaY: 0).kind, .generic)
}

func testKindIsScrollRecognizesOnlyWheelCases() {
Expand Down Expand Up @@ -67,9 +67,4 @@ final class MouseEventTests: XCTestCase {
XCTAssertEqual(location.x, 1600)
XCTAssertEqual(location.y, 980)
}

private func makeScrollEvent(deltaX: CGFloat, deltaY: CGFloat) -> MouseEvent {
let cgEvent = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: Int32(deltaY), wheel2: Int32(deltaX), wheel3: 0)!
return MouseEvent(nsEvent: NSEvent(cgEvent: cgEvent)!)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,7 @@ final class KeyboardVisualizerGroupViewSnapshotTests: XCTestCase {

private extension KeyboardVisualizerGroupViewSnapshotTests {
static var commandShiftFlags: NSEvent.ModifierFlags {
NSEvent.ModifierFlags(
[.command, .shift],
deviceMasks: UInt(NX_DEVICELCMDKEYMASK),
UInt(NX_DEVICELSHIFTKEYMASK)
)
.recorded([.command, .shift])
}

func keycapItems(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ final class KeyboardVisualizerSpecialKeyFilteringTests: XCTestCase {
}

func testResolverClassifiesInsertFunctionKeyAsSpecial() {
let ch = TestKeyboardCharacters.functionKeyCharacter(NSInsertFunctionKey)
let event = TestKeystrokes.make(
keyCode: KeyboardKeyCode.help.rawValue,
let ch = String.functionKey(NSInsertFunctionKey)
let event = StandardKeyEvent.stub(
keyCode: .help,
characters: ch,
charactersIgnoringModifiers: ch
)
Expand All @@ -44,8 +44,8 @@ final class KeyboardVisualizerSpecialKeyFilteringTests: XCTestCase {
}

func testResolverClassifiesPrintableLetterAsNonSpecial() {
let event = TestKeystrokes.make(
keyCode: KeyboardKeyCode.a.rawValue,
let event = StandardKeyEvent.stub(
keyCode: .a,
characters: "a",
charactersIgnoringModifiers: "a"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ final class KeyboardVisualizerTests: XCTestCase {
self.settings.isEnabled = true
self.visualizer.isPresentationActive = true

let keystroke = TestKeystrokes.make(
keyCode: KeyboardKeyCode.k.rawValue,
let keystroke = StandardKeyEvent.stub(
keyCode: .k,
characters: "k",
charactersIgnoringModifiers: "k"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,11 @@ final class KeycapItemFactoryTests: XCTestCase {
let palette = Self.makePalette()

let downItem = KeycapItemFactory.mouseItem(
for: TestMouseEvents.make(type: .leftMouseDown),
for: MouseEvent.stub(type: .leftMouseDown),
palette: palette
)
let upItem = KeycapItemFactory.mouseItem(
for: TestMouseEvents.make(type: .leftMouseUp),
for: MouseEvent.stub(type: .leftMouseUp),
palette: palette
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ final class PointerIconVisualizerTests: XCTestCase {
XCTAssertEqual(view.displayedKind, .rightButton)
XCTAssertTrue(view.isTransientlyVisible)

view.handle(mouseEvent: try makeScrollEvent(deltaX: 0, deltaY: 0))
view.handle(mouseEvent: MouseEvent.scrollStub())

XCTAssertEqual(view.displayedKind, .rightButton)
XCTAssertTrue(view.isTransientlyVisible)
Expand Down Expand Up @@ -189,20 +189,6 @@ final class PointerIconVisualizerTests: XCTestCase {
return MouseEvent(nsEvent: nsEvent)
}

private func makeScrollEvent(deltaX: Int32 = 0, deltaY: Int32) throws -> MouseEvent {
guard let cgEvent = CGEvent(
scrollWheelEvent2Source: nil,
units: .pixel,
wheelCount: 2,
wheel1: deltaY,
wheel2: deltaX,
wheel3: 0
), let nsEvent = NSEvent(cgEvent: cgEvent) else {
throw TestError.eventCreationFailed
}
return MouseEvent(nsEvent: nsEvent)
}

private enum TestError: Error {
case eventCreationFailed
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ final class EventProcessorTests: XCTestCase {
var items: [DisplayEvent] = []
processor.onItemProduced = { items.append($0) }

processor.processMouseEvent(TestMouseEvents.make(type: .leftMouseUp, buttonNumber: 0, modifiers: [.command]))
processor.processMouseEvent(MouseEvent.stub(type: .leftMouseUp, buttonNumber: 0, modifiers: [.command]))

XCTAssertEqual(items.count, 2)
switch items[0] {
Expand All @@ -38,7 +38,7 @@ final class EventProcessorTests: XCTestCase {
var items: [DisplayEvent] = []
processor.onItemProduced = { items.append($0) }

processor.processMouseEvent(TestMouseEvents.make(type: .leftMouseUp, buttonNumber: 0, modifiers: []))
processor.processMouseEvent(MouseEvent.stub(type: .leftMouseUp, buttonNumber: 0, modifiers: []))

XCTAssertEqual(items.count, 2)
switch items[0] {
Expand Down
Loading
Loading