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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Prefer not to use Homebrew? Download `ClickLight.zip` from [GitHub Releases](htt
- Click highlights across macOS apps
- Separate visuals for press, release, right-click, and drag
- Optional laser pointer mode with fading freehand strokes while dragging
- Optional live keyboard shortcut display beside the pointer
- Optional live keyboard shortcut display pinned to the bottom of the screen by default, with pointer-following placement and adjustable badge size available
- Local daily click activity chart with a resettable seven-day history
- Optional daily click count in the menu bar
- Dedicated settings window with sliders + presets for size, duration, intensity, and color
Expand Down
48 changes: 46 additions & 2 deletions Sources/ClickLight/ClickLightSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,12 +441,28 @@ struct ClickLightSettingsView: View {
}
Divider().padding(.vertical, 6)
ModernRow(title: "Show Live Keyboard Shortcuts",
subtitle: "Display shortcut combinations beside the pointer while you use them.") {
subtitle: "Display shortcut combinations while you use them.") {
Toggle("", isOn: binding(\.showLiveKeyboardShortcuts))
.toggleStyle(.switch)
.labelsHidden()
.accessibilityLabel("Show Live Keyboard Shortcuts")
}
if viewModel.settings.showLiveKeyboardShortcuts {
Divider().padding(.vertical, 6)
VStack(alignment: .leading, spacing: 14) {
shortcutDisplayPicker(
title: "Position",
selection: binding(\.liveShortcutPosition),
options: LiveShortcutPosition.allCases
)
shortcutDisplayPicker(
title: "Size",
selection: binding(\.liveShortcutSize),
options: LiveShortcutSize.allCases
)
}
.padding(.vertical, 6)
}
Divider().padding(.vertical, 6)
ModernRow(title: "Show Press",
subtitle: "Highlight when the mouse button goes down.") {
Expand Down Expand Up @@ -650,6 +666,27 @@ struct ClickLightSettingsView: View {
)
}

@ViewBuilder
private func shortcutDisplayPicker<Option: Hashable & Equatable>(
title: String,
selection: Binding<Option>,
options: [Option]
) -> some View where Option: ShortcutDisplayOption {
HStack(spacing: 16) {
Text(title)
.font(.callout.weight(.medium))
.frame(width: 62, alignment: .leading)
Picker(title, selection: selection) {
ForEach(options, id: \.self) { option in
Text(option.title).tag(option)
}
}
.labelsHidden()
.pickerStyle(.segmented)
.accessibilityLabel("Live Shortcut \(title)")
}
}

@ViewBuilder
private func presetSegmented(
label: String,
Expand Down Expand Up @@ -706,6 +743,13 @@ struct ClickLightSettingsView: View {

}

private protocol ShortcutDisplayOption {
var title: String { get }
}

extension LiveShortcutPosition: ShortcutDisplayOption {}
extension LiveShortcutSize: ShortcutDisplayOption {}

// MARK: - Reusable Components

private struct SettingsCard<Content: View>: View {
Expand Down Expand Up @@ -985,7 +1029,7 @@ private enum SettingsPane: String, CaseIterable, Hashable {
case .shortcuts:
return "Set global shortcuts."
case .events:
return "Choose which mouse interactions trigger a pulse."
return "Choose which interactions and shortcut overlays appear."
case .activity:
return "A local daily view of your clicking."
}
Expand Down
26 changes: 18 additions & 8 deletions Sources/ClickLight/ClickOverlayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,24 +200,34 @@ final class ClickOverlayView: NSView {
guard settings.showLiveKeyboardShortcuts, let label = liveShortcutLabel else { return }

let alpha = label.alpha(at: now)
let font = NSFont.monospacedSystemFont(ofSize: 18, weight: .semibold)
let style = settings.liveShortcutSize
let font = NSFont.monospacedSystemFont(ofSize: style.fontSize, weight: .semibold)
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: NSColor.white.withAlphaComponent(alpha)
]
let textSize = (label.text as NSString).size(withAttributes: attributes)
let padding = CGSize(width: 13, height: 8)
let padding = style.padding
let size = CGSize(width: textSize.width + padding.width * 2, height: textSize.height + padding.height * 2)
let proposedOrigin = CGPoint(x: label.point.x + 18, y: label.point.y + 18)
let origin = CGPoint(
x: min(max(10, proposedOrigin.x), bounds.width - size.width - 10),
y: min(max(10, proposedOrigin.y), bounds.height - size.height - 10)
)
let origin: CGPoint
switch settings.liveShortcutPosition {
case .nearPointer:
let proposedOrigin = CGPoint(x: label.point.x + 18, y: label.point.y + 18)
origin = CGPoint(
x: min(max(10, proposedOrigin.x), bounds.width - size.width - 10),
y: min(max(10, proposedOrigin.y), bounds.height - size.height - 10)
)
case .bottomCenter:
origin = CGPoint(
x: max(10, (bounds.width - size.width) / 2),
y: 34
)
}
let rect = CGRect(origin: origin, size: size)

context.saveGState()
context.setFillColor(NSColor(calibratedWhite: 0.06, alpha: 0.88 * alpha).cgColor)
context.addPath(CGPath(roundedRect: rect, cornerWidth: 9, cornerHeight: 9, transform: nil))
context.addPath(CGPath(roundedRect: rect, cornerWidth: style.cornerRadius, cornerHeight: style.cornerRadius, transform: nil))
context.fillPath()
context.restoreGState()

Expand Down
69 changes: 69 additions & 0 deletions Sources/ClickLight/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ struct ClickSettings: Equatable {
var showDrag: Bool
var showLaserPointer: Bool
var showLiveKeyboardShortcuts: Bool
var liveShortcutPosition: LiveShortcutPosition
var liveShortcutSize: LiveShortcutSize
var showMenuBarText: Bool
var showMenuBarClickCount: Bool
var size: CGFloat
Expand Down Expand Up @@ -93,6 +95,8 @@ struct ClickSettings: Equatable {
showDrag: true,
showLaserPointer: false,
showLiveKeyboardShortcuts: false,
liveShortcutPosition: .bottomCenter,
liveShortcutSize: .medium,
showMenuBarText: false,
showMenuBarClickCount: false,
size: 64,
Expand Down Expand Up @@ -201,6 +205,63 @@ enum CustomClickColorMode: String, CaseIterable, Equatable {
}
}

enum LiveShortcutPosition: String, CaseIterable, Equatable {
case nearPointer
case bottomCenter

var title: String {
switch self {
case .nearPointer:
return "Near Pointer"
case .bottomCenter:
return "Bottom Center"
}
}
}

enum LiveShortcutSize: String, CaseIterable, Equatable {
case small
case medium
case large

var title: String {
rawValue.capitalized
}

var fontSize: CGFloat {
switch self {
case .small:
return 15
case .medium:
return 18
case .large:
return 25
}
}

var padding: CGSize {
switch self {
case .small:
return CGSize(width: 11, height: 6)
case .medium:
return CGSize(width: 13, height: 8)
case .large:
return CGSize(width: 18, height: 11)
}
}

var cornerRadius: CGFloat {
switch self {
case .small:
return 8
case .medium:
return 9
case .large:
return 12
}
}
}

enum CustomClickColorTarget {
case left
case right
Expand Down Expand Up @@ -279,6 +340,8 @@ final class SettingsStore {
static let showDrag = "showDrag"
static let showLaserPointer = "showLaserPointer"
static let showLiveKeyboardShortcuts = "showLiveKeyboardShortcuts"
static let liveShortcutPosition = "liveShortcutPosition"
static let liveShortcutSize = "liveShortcutSize"
static let showMenuBarText = "showMenuBarText"
static let showMenuBarClickCount = "showMenuBarClickCount"
static let size = "size"
Expand Down Expand Up @@ -342,6 +405,8 @@ final class SettingsStore {
showDrag: defaults.bool(forKey: Key.showDrag),
showLaserPointer: defaults.bool(forKey: Key.showLaserPointer),
showLiveKeyboardShortcuts: defaults.bool(forKey: Key.showLiveKeyboardShortcuts),
liveShortcutPosition: LiveShortcutPosition(rawValue: defaults.string(forKey: Key.liveShortcutPosition) ?? "") ?? .bottomCenter,
liveShortcutSize: LiveShortcutSize(rawValue: defaults.string(forKey: Key.liveShortcutSize) ?? "") ?? .medium,
showMenuBarText: defaults.bool(forKey: Key.showMenuBarText),
showMenuBarClickCount: defaults.bool(forKey: Key.showMenuBarClickCount),
size: CGFloat(defaults.double(forKey: Key.size)),
Expand Down Expand Up @@ -410,6 +475,8 @@ final class SettingsStore {
defaults.set(newValue.showDrag, forKey: Key.showDrag)
defaults.set(newValue.showLaserPointer, forKey: Key.showLaserPointer)
defaults.set(newValue.showLiveKeyboardShortcuts, forKey: Key.showLiveKeyboardShortcuts)
defaults.set(newValue.liveShortcutPosition.rawValue, forKey: Key.liveShortcutPosition)
defaults.set(newValue.liveShortcutSize.rawValue, forKey: Key.liveShortcutSize)
defaults.set(newValue.showMenuBarText, forKey: Key.showMenuBarText)
defaults.set(newValue.showMenuBarClickCount, forKey: Key.showMenuBarClickCount)
defaults.set(Double(newValue.size), forKey: Key.size)
Expand Down Expand Up @@ -475,6 +542,8 @@ final class SettingsStore {
Key.showDrag: defaults.showDrag,
Key.showLaserPointer: defaults.showLaserPointer,
Key.showLiveKeyboardShortcuts: defaults.showLiveKeyboardShortcuts,
Key.liveShortcutPosition: defaults.liveShortcutPosition.rawValue,
Key.liveShortcutSize: defaults.liveShortcutSize.rawValue,
Key.showMenuBarText: defaults.showMenuBarText,
Key.showMenuBarClickCount: defaults.showMenuBarClickCount,
Key.size: Double(defaults.size),
Expand Down
Loading