Skip to content
Open
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
44 changes: 44 additions & 0 deletions Leader Key.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@
427C18502BD6652500955B98 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427C184F2BD6652500955B98 /* Util.swift */; };
427C18542BD6E59300955B98 /* NSWindow+Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 427C18532BD6E59300955B98 /* NSWindow+Animations.swift */; };
4284834C2E813212009D7EEF /* KeyboardLayoutTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4284834B2E813212009D7EEF /* KeyboardLayoutTests.swift */; };
4284834E2E813214009D7EEF /* KeyboardCheatsheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4284834D2E813213009D7EEF /* KeyboardCheatsheetView.swift */; };
428483502E813216009D7EEF /* KeyboardLayoutModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4284834F2E813215009D7EEF /* KeyboardLayoutModel.swift */; };
428483522E813218009D7EEF /* KeyboardLayoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428483512E813217009D7EEF /* KeyboardLayoutView.swift */; };
428483542E81321A009D7EEF /* KeyboardPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428483532E813219009D7EEF /* KeyboardPane.swift */; };
428483562E81321C009D7EEF /* KeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428483572E81321D009D7EEF /* KeyView.swift */; };
428483582E81321E009D7EEF /* KeyBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 428483592E81321F009D7EEF /* KeyBadge.swift */; };
4284835A2E813220009D7EEF /* CheatsheetRows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4284835B2E813221009D7EEF /* CheatsheetRows.swift */; };
42B21FBC2D67566100F4A2C7 /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42B21FBB2D67566100F4A2C7 /* Alerts.swift */; };
42CCB5A32DAD257700356FC0 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = FBCA04D82D9F02F700271163 /* Kingfisher */; };
42DFCD722D5B7D48002EA111 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DFCD712D5B7D46002EA111 /* Events.swift */; };
Expand Down Expand Up @@ -102,6 +109,13 @@
427C184F2BD6652500955B98 /* Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Util.swift; sourceTree = "<group>"; };
427C18532BD6E59300955B98 /* NSWindow+Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Animations.swift"; sourceTree = "<group>"; };
4284834B2E813212009D7EEF /* KeyboardLayoutTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayoutTests.swift; sourceTree = "<group>"; };
4284834D2E813213009D7EEF /* KeyboardCheatsheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardCheatsheetView.swift; sourceTree = "<group>"; };
4284834F2E813215009D7EEF /* KeyboardLayoutModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayoutModel.swift; sourceTree = "<group>"; };
428483512E813217009D7EEF /* KeyboardLayoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayoutView.swift; sourceTree = "<group>"; };
428483532E813219009D7EEF /* KeyboardPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPane.swift; sourceTree = "<group>"; };
428483572E81321D009D7EEF /* KeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyView.swift; sourceTree = "<group>"; };
428483592E81321F009D7EEF /* KeyBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBadge.swift; sourceTree = "<group>"; };
4284835B2E813221009D7EEF /* CheatsheetRows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheatsheetRows.swift; sourceTree = "<group>"; };
42B21FBB2D67566100F4A2C7 /* Alerts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alerts.swift; sourceTree = "<group>"; };
42DFCD712D5B7D46002EA111 /* Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Events.swift; sourceTree = "<group>"; };
42F4CDC82D458FF700D0DD76 /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -158,10 +172,31 @@
children = (
427C18242BD31E2E00955B98 /* GeneralPane.swift */,
42FDC3192D51687B004F5C5C /* AdvancedPane.swift */,
428483532E813219009D7EEF /* KeyboardPane.swift */,
);
path = Settings;
sourceTree = "<group>";
};
428483552E81321B009D7EEF /* KeyboardLayout */ = {
isa = PBXGroup;
children = (
4284834D2E813213009D7EEF /* KeyboardCheatsheetView.swift */,
4284834F2E813215009D7EEF /* KeyboardLayoutModel.swift */,
428483512E813217009D7EEF /* KeyboardLayoutView.swift */,
428483572E81321D009D7EEF /* KeyView.swift */,
);
path = KeyboardLayout;
sourceTree = "<group>";
};
4284835C2E813222009D7EEF /* Cheatsheet */ = {
isa = PBXGroup;
children = (
428483592E81321F009D7EEF /* KeyBadge.swift */,
4284835B2E813221009D7EEF /* CheatsheetRows.swift */,
);
path = Cheatsheet;
sourceTree = "<group>";
};
427C17DE2BD311B400955B98 = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -205,6 +240,8 @@
427C182E2BD3206200955B98 /* UserState.swift */,
427C184F2BD6652500955B98 /* Util.swift */,
427C17EE2BD311B500955B98 /* Assets.xcassets */,
4284835C2E813222009D7EEF /* Cheatsheet */,
428483552E81321B009D7EEF /* KeyboardLayout */,
423632242D68CC5D00878D92 /* Settings */,
427C18362BD3243C00955B98 /* Support */,
423632232D68CB0F00878D92 /* Themes */,
Expand Down Expand Up @@ -443,6 +480,13 @@
42F4CDCD2D45B13600D0DD76 /* KeyButton.swift in Sources */,
606C56EF2DAB875A00198B9F /* Cheater.swift in Sources */,
427C181C2BD314B500955B98 /* Constants.swift in Sources */,
4284834E2E813214009D7EEF /* KeyboardCheatsheetView.swift in Sources */,
428483502E813216009D7EEF /* KeyboardLayoutModel.swift in Sources */,
428483522E813218009D7EEF /* KeyboardLayoutView.swift in Sources */,
428483542E81321A009D7EEF /* KeyboardPane.swift in Sources */,
428483562E81321C009D7EEF /* KeyView.swift in Sources */,
428483582E81321E009D7EEF /* KeyBadge.swift in Sources */,
4284835A2E813220009D7EEF /* CheatsheetRows.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
7 changes: 7 additions & 0 deletions Leader Key/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ class AppDelegate: NSObject, NSApplicationDelegate,
contentView: {
AdvancedPane().environmentObject(self.config)
}),
Settings.Pane(
identifier: .keyboard, title: "Keyboard",
toolbarIcon: NSImage(
systemSymbolName: "keyboard", accessibilityDescription: "Keyboard")!,
contentView: {
KeyboardPane().environmentObject(self.config)
}),
],
style: .segmentedControl,
)
Expand Down
243 changes: 81 additions & 162 deletions Leader Key/Cheatsheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,195 +3,114 @@ import Kingfisher
import SwiftUI

enum Cheatsheet {
private static let iconSize = NSSize(width: 24, height: 24)
static func createWindow(for userState: UserState) -> NSWindow {
let view = CheatsheetView().environmentObject(userState)
let controller = NSHostingController(rootView: view)
controller.sizingOptions = .preferredContentSize
let cheatsheet = PanelWindow(
contentRect: NSRect(x: 0, y: 0, width: 700, height: 640)
)
cheatsheet.contentViewController = controller
return cheatsheet
}
}

struct KeyBadge: SwiftUI.View {
let key: String
struct CheatsheetView: SwiftUI.View {
@EnvironmentObject var userState: UserState
@Default(.cheatsheetStyle) var cheatsheetStyle
@State private var contentHeight: CGFloat = 0

var body: some SwiftUI.View {
Text(KeyMaps.glyph(for: key) ?? key)
.font(.system(.body, design: .rounded))
.multilineTextAlignment(.center)
.fontWeight(.bold)
.padding(.vertical, 4)
.frame(width: 24)
.background(.white.opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 5.0, style: .continuous))
var maxHeight: CGFloat {
if let screen = NSScreen.main {
return screen.visibleFrame.height - 40
}
return 640
}

struct ActionRow: SwiftUI.View {
let action: Action
let indent: Int
@Default(.showDetailsInCheatsheet) var showDetails
@Default(.showAppIconsInCheatsheet) var showIcons

var body: some SwiftUI.View {
HStack {
HStack {
ForEach(0..<indent, id: \.self) { _ in
Text(" ")
}
KeyBadge(key: action.key ?? "●")

if showIcons {
actionIcon(item: ActionOrGroup.action(action), iconSize: iconSize)
}

Text(action.displayName)
.lineLimit(1)
.truncationMode(.middle)
}
Spacer()
if showDetails {
Text(action.value)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
}
// Constrain to edge of screen
static var preferredWidth: CGFloat {
if let screen = NSScreen.main {
let screenHalf = screen.visibleFrame.width / 2
let desiredWidth: CGFloat = 700
let margin: CGFloat = 20
return desiredWidth > screenHalf ? screenHalf - margin : desiredWidth
}
return 700
}

struct GroupRow: SwiftUI.View {
@Default(.expandGroupsInCheatsheet) var expand
@Default(.showDetailsInCheatsheet) var showDetails
@Default(.showAppIconsInCheatsheet) var showIcons
var actions: [ActionOrGroup] {
(userState.currentGroup != nil)
? userState.currentGroup!.actions : userState.userConfig.root.actions
}

let group: Group
let indent: Int
var body: some SwiftUI.View {
switch cheatsheetStyle {
case .list:
listView
case .keyboard:
KeyboardCheatsheetView()
.fixedSize()
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
}

var body: some SwiftUI.View {
var listView: some View {
ScrollView {
VStack(alignment: .leading, spacing: 4) {
HStack {
ForEach(0..<indent, id: \.self) { _ in
Text(" ")
}
KeyBadge(key: group.key ?? "")

if showIcons {
actionIcon(item: ActionOrGroup.group(group), iconSize: iconSize)
}

Image(systemName: "chevron.right")
.foregroundStyle(.secondary)

Text(group.displayName)

Spacer()
if showDetails {
Text("\(group.actions.count.description) item(s)")
if let group = userState.currentGroup {
HStack {
KeyBadge(key: group.key ?? "•")
Text(group.key == nil ? "Leader Key" : group.displayName)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
}
if expand {
ForEach(Array(group.actions.enumerated()), id: \.offset) { _, item in
switch item {
case .action(let action):
Cheatsheet.ActionRow(action: action, indent: indent + 1)
case .group(let group):
Cheatsheet.GroupRow(group: group, indent: indent + 1)
}
}
}
}
}
}

struct CheatsheetView: SwiftUI.View {
@EnvironmentObject var userState: UserState
@State private var contentHeight: CGFloat = 0

var maxHeight: CGFloat {
if let screen = NSScreen.main {
return screen.visibleFrame.height - 40 // Leave some margin
}
return 640
}

// Constrain to edge of screen
static var preferredWidth: CGFloat {
if let screen = NSScreen.main {
let screenHalf = screen.visibleFrame.width / 2
let desiredWidth: CGFloat = 580
let margin: CGFloat = 20
return desiredWidth > screenHalf ? screenHalf - margin : desiredWidth
}
return 580
}

var actions: [ActionOrGroup] {
(userState.currentGroup != nil)
? userState.currentGroup!.actions : userState.userConfig.root.actions
}

var body: some SwiftUI.View {
ScrollView {
SwiftUI.VStack(alignment: .leading, spacing: 4) {
if let group = userState.currentGroup {
HStack {
KeyBadge(key: group.key ?? "•")
Text(group.key == nil ? "Leader Key" : group.displayName)
.foregroundStyle(.secondary)
}
.padding(.bottom, 8)
Divider()
.padding(.bottom, 8)
Divider()
.padding(.bottom, 8)
}
}

ForEach(Array(actions.enumerated()), id: \.offset) { _, item in
switch item {
case .action(let action):
Cheatsheet.ActionRow(action: action, indent: 0)
case .group(let group):
Cheatsheet.GroupRow(group: group, indent: 0)
}
ForEach(Array(actions.enumerated()), id: \.offset) { _, item in
switch item {
case .action(let action):
ActionRow(action: action, indent: 0)
case .group(let group):
GroupRow(group: group, indent: 0)
}
}
.padding()
.overlay(
GeometryReader { geo in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geo.size.height
)
}
)
}
.frame(width: Cheatsheet.CheatsheetView.preferredWidth)
.frame(height: min(contentHeight, maxHeight))
.background(
VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
.padding()
.overlay(
GeometryReader { geo in
Color.clear.preference(
key: HeightPreferenceKey.self,
value: geo.size.height
)
}
)
.onPreferenceChange(HeightPreferenceKey.self) { height in
self.contentHeight = height
}
.frame(width: CheatsheetView.preferredWidth)
.frame(height: min(contentHeight, maxHeight))
.background(
ZStack {
VisualEffectView(material: .popover, blendingMode: .behindWindow)
Color(NSColor.windowBackgroundColor).opacity(0.7)
}
)
.onPreferenceChange(HeightPreferenceKey.self) { height in
self.contentHeight = height
}
}
}

struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}

static func createWindow(for userState: UserState) -> NSWindow {
let view = CheatsheetView().environmentObject(userState)
let controller = NSHostingController(rootView: view)
let cheatsheet = PanelWindow(
contentRect: NSRect(x: 0, y: 0, width: 580, height: 640)
)
cheatsheet.contentViewController = controller
return cheatsheet
struct HeightPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}

struct CheatsheetView_Previews: PreviewProvider {
static var previews: some View {
Cheatsheet.CheatsheetView()
CheatsheetView()
.environmentObject(UserState(userConfig: UserConfig()))
}
}
}
Loading