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 @@ -183,6 +183,26 @@ private extension KeyboardVisualizer {
}
}

private func displayFunctionKey(isPressed: Bool, modifierFlags: NSEvent.ModifierFlags) {
self.prepareForNextContentEvent()

let keycap = KeycapItemFactory.functionItem(
isPressed: isPressed,
palette: self.visualizerSettings.palette
)
let group = self.eventCoordinator.handleTrackedKey(
keyCode: KeyboardKeyCode.function.rawValue,
isKeyDown: isPressed,
items: [keycap],
appendGroup: { self.visualizerWindow.appendGroup(with: $0, defersMaxCount: self.visualizerSettings.collapseRepeatedGroups) },
updateGroup: { group, items in self.visualizerWindow.updateGroup(group, with: items) }
)
self.collapseActiveRepeatIfNeeded(group)
if !isPressed, modifierFlags.intersection(Self.trackedModifierFlags).isEmpty {
self.finalizeGroupIfNeeded(group)
}
}

private func displayModifierPreview(_ modifierFlags: NSEvent.ModifierFlags) {
let currentTrackedFlags = modifierFlags.intersection(Self.trackedModifierFlags)
let previousTrackedFlags = self.lastModifierFlags.intersection(Self.trackedModifierFlags)
Expand All @@ -208,6 +228,14 @@ private extension KeyboardVisualizer {
self.finalizeGroupIfNeeded(group)
}

// `fn` is not a modifier: it gets its own keycap, tracked from key down to key up
// so a press and its release update one group instead of rendering twice.
let functionNow = modifierFlags.contains(.function)
let functionWas = self.lastModifierFlags.contains(.function)
if self.visualizerSettings.showSpecialKeys, functionNow != functionWas {
self.displayFunctionKey(isPressed: functionNow, modifierFlags: modifierFlags)
}

self.lastModifierFlags = modifierFlags

let items = KeycapItemFactory.modifierItems(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extension KeycapItemFactory {
palette: KeycapThemePalette
) -> [KeycapItem] {
var result = Self.modifierItems(
currentFlags: modifierFlags.subtracting(.function),
currentFlags: modifierFlags,
releasedFlags: [],
palette: palette
)
Expand All @@ -48,6 +48,21 @@ extension KeycapItemFactory {
return result
}

/// The `fn` keycap. `fn` only ever reaches us as a modifier flag, never as a key
/// event, so it is built here instead of going through the keystroke path.
static func functionItem(
isPressed: Bool,
palette: KeycapThemePalette
) -> KeycapItem {
let identity = KeycapIdentity.keyCode(KeyboardKeyCode.function.rawValue)
return KeycapItem(
identity: identity,
legend: .function,
state: KeycapState(isPressed: isPressed),
appearance: palette.appearance(for: identity)
)
}

// Legend for a key, with the two keys whose keycap styling differs from
// what the resolved legend alone describes.
private static func keycapLegend(for keyCode: UInt16, legend: EventLegend) -> KeycapLegend {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ extension KeycapItemFactory {
}
}

if currentFlags.contains(.function) || releasedFlags.contains(.function) {
items.append(Self.functionItem(isPressed: currentFlags.contains(.function), palette: palette))
}
return items
}

Expand Down Expand Up @@ -76,17 +73,4 @@ extension KeycapItemFactory {
.map { KeyboardModifierKey(modifier, location: $0) }
.filter { keys.contains($0) }
}

private static func functionItem(
isPressed: Bool,
palette: KeycapThemePalette
) -> KeycapItem {
let identity = KeycapIdentity.keyCode(KeyboardKeyCode.function.rawValue)
return KeycapItem(
identity: identity,
legend: .function,
state: KeycapState(isPressed: isPressed),
appearance: palette.appearance(for: identity)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ enum KeycapCategory: CaseIterable {
case .mouse:
self = .mouse
case let .keyCode(code):
// `fn` is emitted as a keyCode from the modifier path and is shown beside the
// modifiers, so it follows the modifier theme rather than the special theme.
if code == KeyboardKeyCode.function.rawValue {
self = .modifier
} else if KeyboardSpecialKeyResolver.specialKey(for: code) != nil {
if KeyboardSpecialKeyResolver.specialKey(for: code) != nil {
self = .special
} else {
self = .regular
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ final class KeyboardVisualizerTests: XCTestCase {
XCTAssertTrue(self.settings.isEnabled)
}

func testFunctionKeyPressAndReleaseRenderOneGroup() {
self.settings.showSpecialKeys = true
self.visualizer.isPresentationActive = true

self.visualizer.display(.modifierStateChanged([.function]))

XCTAssertEqual(self.visualizer.visibleGroupCount, 1)

self.visualizer.display(.modifierStateChanged([]))

XCTAssertEqual(self.visualizer.visibleGroupCount, 1)
}

func testFunctionKeyIsHiddenWhenSpecialKeysAreHidden() {
self.settings.showSpecialKeys = false
self.visualizer.isPresentationActive = true

self.visualizer.display(.modifierStateChanged([.function]))
self.visualizer.display(.modifierStateChanged([]))

XCTAssertEqual(self.visualizer.visibleGroupCount, 0)
}

func testOnlyShowModifiedKeystrokesStillShowsStandaloneSpecialKeys() {
self.settings.onlyShowModifiedKeystrokes = true
self.settings.showSpecialKeys = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,25 @@ final class KeycapItemFactoryTests: XCTestCase {
XCTAssertEqual(KeyboardModifierKey.keys(in: flags), [.leftCommand, .rightCommand])
}

func testModifierItemsIncludeFunctionKey() {
func testModifierItemsExcludeFunctionKey() {
let items = KeycapItemFactory.modifierItems(
currentFlags: [.function],
releasedFlags: [],
releasedFlags: [.function],
palette: Self.makePalette()
)

XCTAssertEqual(items.count, 1)
XCTAssertEqual(items.first?.identity, .keyCode(KeyboardKeyCode.function.rawValue))
XCTAssertEqual(items.first?.label, "fn")
XCTAssertEqual(items.first?.sfSymbolName, "globe")
XCTAssertEqual(items.first?.isPressed, true)
XCTAssertTrue(items.isEmpty)
}

func testFunctionItemIsAStandaloneKeycap() {
let item = KeycapItemFactory.functionItem(isPressed: true, palette: Self.makePalette())

XCTAssertEqual(item.identity, .keyCode(KeyboardKeyCode.function.rawValue))
XCTAssertEqual(item.label, "fn")
XCTAssertEqual(item.sfSymbolName, "globe")
XCTAssertEqual(item.isPressed, true)
XCTAssertFalse(item.identity.isModifier)
XCTAssertEqual(KeycapCategory(identity: item.identity), .special)
}

func testModifierItemsUseLocationSpecificModifierKeys() {
Expand Down
Loading