Skip to content

Commit

Permalink
feat: added feature to set keyboard shortcut to open/close popup wind…
Browse files Browse the repository at this point in the history
…ow (#1976)
  • Loading branch information
exelban committed Jan 20, 2025
1 parent b4c835e commit 58ad6c5
Show file tree
Hide file tree
Showing 25 changed files with 346 additions and 39 deletions.
152 changes: 150 additions & 2 deletions Kit/extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//

import Cocoa
import Carbon

extension String: @retroactive LocalizedError {
public var errorDescription: String? { return self }
Expand Down Expand Up @@ -312,9 +313,9 @@ public extension NSView {
return s
}

func buttonIconView(_ action: Selector, icon: NSImage) -> NSButton {
func buttonIconView(_ action: Selector, icon: NSImage, height: CGFloat = 22) -> NSButton {
let button = NSButton()
button.heightAnchor.constraint(equalToConstant: 22).isActive = true
button.heightAnchor.constraint(equalToConstant: height).isActive = true
button.bezelStyle = .regularSquare
button.translatesAutoresizingMaskIntoConstraints = false
button.imageScaling = .scaleNone
Expand Down Expand Up @@ -564,3 +565,150 @@ extension CGFloat {
return ceil(self / 10) * 10
}
}

public class KeyboardShartcutView: NSStackView {
private let callback: (_ value: [UInt16]) -> Void

private var startIcon: NSImage {
if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "record.circle", scale: .large) {
return icon
}
return NSImage(named: NSImage.Name("record"))!
}
private var stopIcon: NSImage {
if #available(macOS 12.0, *), let icon = iconFromSymbol(name: "stop.circle.fill", scale: .large) {
return icon
}
return NSImage(named: NSImage.Name("stop"))!
}

private var valueField: NSTextField? = nil
private var startButton: NSButton? = nil
private var stopButton: NSButton? = nil

private var recording: Bool = false
private var keyCodes: [UInt16] = []
private var value: [UInt16] = []
private var interaction: Bool = false

public init(callback: @escaping (_ value: [UInt16]) -> Void, value: [UInt16]) {
self.callback = callback
self.value = value

super.init(frame: NSRect.zero)
self.orientation = .horizontal

let stringValue = value.isEmpty ? localizedString("Disabled") : self.parseValue(value)
let valueField: NSTextField = LabelField(stringValue)
valueField.font = NSFont.systemFont(ofSize: 13, weight: .regular)
valueField.textColor = .textColor
valueField.alignment = .center

let startButton = buttonIconView(#selector(self.startListening), icon: self.startIcon, height: 15)
let stopButton = buttonIconView(#selector(self.stopListening), icon: self.stopIcon, height: 15)

self.addArrangedSubview(valueField)
self.addArrangedSubview(startButton)

self.valueField = valueField
self.startButton = startButton
self.stopButton = stopButton

NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in
self?.handleKeyEvent(event)
return event
}
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

@objc private func startListening() {
guard AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary) else { return }
if let btn = self.stopButton {
self.startButton?.removeFromSuperview()
self.addArrangedSubview(btn)
}
self.valueField?.stringValue = localizedString("Listening...")
self.keyCodes = []
self.recording = true
}

@objc private func stopListening() {
if let btn = self.startButton {
self.stopButton?.removeFromSuperview()
self.addArrangedSubview(btn)
}

if self.keyCodes.isEmpty && !self.interaction {
self.value = []
self.valueField?.stringValue = localizedString("Disabled")
}

self.recording = false
self.interaction = false
self.callback(self.value)
}

private func handleKeyEvent(_ event: NSEvent) {
guard self.recording else { return }
self.interaction = true

if event.type == .flagsChanged {
self.keyCodes = []
if event.modifierFlags.contains(.control) { self.keyCodes.append(59) }
if event.modifierFlags.contains(.shift) { self.keyCodes.append(60) }
if event.modifierFlags.contains(.command) { self.keyCodes.append(55) }
if event.modifierFlags.contains(.option) { self.keyCodes.append(58) }
} else if event.type == .keyDown {
self.keyCodes.append(event.keyCode)
self.value = self.keyCodes
}

let list = self.keyCodes.isEmpty ? self.value : self.keyCodes
self.valueField?.stringValue = self.parseValue(list)
}

private func parseValue(_ list: [UInt16]) -> String {
return list.compactMap { self.keyName(virtualKeyCode: $0) }.joined(separator: " + ")
}

private func keyName(virtualKeyCode: UInt16) -> String? {
if virtualKeyCode == 59 {
return "Control"
} else if virtualKeyCode == 60 {
return "Shift"
} else if virtualKeyCode == 55 {
return "Command"
} else if virtualKeyCode == 58 {
return "Option"
}

let maxNameLength = 4
var nameBuffer = [UniChar](repeating: 0, count: maxNameLength)
var nameLength = 0

let modifierKeys = UInt32(alphaLock >> 8) & 0xFF // Caps Lock
var deadKeys: UInt32 = 0
let keyboardType = UInt32(LMGetKbdType())

let source = TISCopyCurrentKeyboardLayoutInputSource().takeRetainedValue()
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
NSLog("Could not get keyboard layout data")
return nil
}
let layoutData = Unmanaged<CFData>.fromOpaque(ptr).takeUnretainedValue() as Data
let osStatus = layoutData.withUnsafeBytes {
UCKeyTranslate($0.bindMemory(to: UCKeyboardLayout.self).baseAddress, virtualKeyCode, UInt16(kUCKeyActionDown),
modifierKeys, keyboardType, UInt32(kUCKeyTranslateNoDeadKeysMask),
&deadKeys, maxNameLength, &nameLength, &nameBuffer)
}
guard osStatus == noErr else {
NSLog("Code: 0x%04X Status: %+i", virtualKeyCode, osStatus)
return nil
}

return String(utf16CodeUnits: nameBuffer, count: nameLength)
}
}
6 changes: 5 additions & 1 deletion Kit/module/module.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,14 @@ open class Module {
}
public var combinedPosition: Int {
get { Store.shared.int(key: "\(self.name)_position", defaultValue: 0) }
set { Store.shared.set(key: "\(self.name)_position", value: newValue) }
set { Store.shared.set(key: "\(self.name)_position", value: newValue) }
}
public var userDefaults: UserDefaults? = UserDefaults(suiteName: "\(Bundle.main.object(forInfoDictionaryKey: "TeamId") as! String).eu.exelban.Stats.widgets")

public var popupKeyboardShortcut: [UInt16] {
return self.popupView?.keyboardShortcut ?? []
}

private var moduleType: ModuleType

private var settingsView: Settings_v? = nil
Expand Down
22 changes: 22 additions & 0 deletions Kit/module/popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,40 @@
import Cocoa

public protocol Popup_p: NSView {
var keyboardShortcut: [UInt16] { get }
var sizeCallback: ((NSSize) -> Void)? { get set }

func settings() -> NSView?

func appear()
func disappear()
func setKeyboardShortcut(_ binding: [UInt16])
}

open class PopupWrapper: NSStackView, Popup_p {
public var title: String
public var keyboardShortcut: [UInt16] = []
open var sizeCallback: ((NSSize) -> Void)? = nil

public init(_ typ: ModuleType, frame: NSRect) {
self.title = typ.rawValue
self.keyboardShortcut = Store.shared.array(key: "\(typ.rawValue)_popup_keyboardShortcut", defaultValue: []) as? [UInt16] ?? []

super.init(frame: frame)
}

required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

open func settings() -> NSView? { return nil }
open func appear() {}
open func disappear() {}

open func setKeyboardShortcut(_ binding: [UInt16]) {
self.keyboardShortcut = binding
Store.shared.set(key: "\(self.title)_popup_keyboardShortcut", value: binding)
}
}

public class PopupWindow: NSWindow, NSWindowDelegate {
Expand Down
8 changes: 8 additions & 0 deletions Kit/plugins/Store.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public class Store {
return (!self.exist(key: key) ? value : defaults.integer(forKey: key))
}

public func array(key: String, defaultValue value: [Any]) -> [Any] {
return (!self.exist(key: key) ? value : defaults.array(forKey: key)!)
}

public func data(key: String) -> Data? {
return defaults.data(forKey: key)
}
Expand All @@ -57,6 +61,10 @@ public class Store {
self.defaults.set(value, forKey: key)
}

public func set(key: String, value: [Any]) {
self.defaults.set(value, forKey: key)
}

public func reset() {
self.defaults.dictionaryRepresentation().keys.forEach { key in
self.defaults.removeObject(forKey: key)
Expand Down
13 changes: 8 additions & 5 deletions Modules/Battery/popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import Cocoa
import Kit

internal class Popup: PopupWrapper {
private var title: String

private var grid: NSGridView? = nil

private let dashboardHeight: CGFloat = 90
Expand Down Expand Up @@ -65,9 +63,7 @@ internal class Popup: PopupWrapper {
}

public init(_ module: ModuleType) {
self.title = module.rawValue

super.init(frame: NSRect(
super.init(module, frame: NSRect(
x: 0,
y: 0,
width: Constants.Popup.width,
Expand Down Expand Up @@ -337,6 +333,13 @@ internal class Popup: PopupWrapper {
public override func settings() -> NSView? {
let view = SettingsContainerView()

view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))

view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Colorize battery"), component: switchView(
action: #selector(self.toggleColor),
Expand Down
2 changes: 1 addition & 1 deletion Modules/Bluetooth/popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal class Popup: PopupWrapper {
private let emptyView: EmptyView = EmptyView(height: 30, isHidden: false, msg: localizedString("No Bluetooth devices are available"))

public init() {
super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 30))
super.init(ModuleType.bluetooth, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 30))

self.orientation = .vertical
self.spacing = Constants.Popup.margins
Expand Down
15 changes: 8 additions & 7 deletions Modules/CPU/popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import Cocoa
import Kit

internal class Popup: PopupWrapper {
private var title: String

private let dashboardHeight: CGFloat = 90
private let chartHeight: CGFloat = 120 + Constants.Popup.separatorHeight
private var detailsHeight: CGFloat {
Expand Down Expand Up @@ -125,14 +123,10 @@ internal class Popup: PopupWrapper {
}

public init(_ module: ModuleType) {
self.title = module.rawValue

super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))

self.spacing = 0
self.orientation = .vertical
// self.setAccessibilityElement(true)
// self.toolTip = self.title

self.systemColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_systemColor", defaultValue: self.systemColorState.key))
self.userColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_userColor", defaultValue: self.userColorState.key))
Expand Down Expand Up @@ -546,6 +540,13 @@ internal class Popup: PopupWrapper {
public override func settings() -> NSView? {
let view = SettingsContainerView()

view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))

view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("System color"), component: selectView(
action: #selector(self.toggleSystemColor),
Expand Down
13 changes: 8 additions & 5 deletions Modules/Clock/popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,14 @@ import Cocoa
import Kit

internal class Popup: PopupWrapper {
private var title: String

private let orderTableView: OrderTableView = OrderTableView()
private var list: [Clock_t] = []

private var calendarView: CalendarView? = nil
private var calendarState: Bool = true

public init(_ module: ModuleType) {
self.title = module.rawValue

super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))

self.orientation = .vertical
self.spacing = Constants.Popup.margins
Expand Down Expand Up @@ -87,6 +83,13 @@ internal class Popup: PopupWrapper {
public override func settings() -> NSView? {
let view = SettingsContainerView()

view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))

view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Calendar"), component: switchView(
action: #selector(self.toggleCalendarState),
Expand Down
13 changes: 8 additions & 5 deletions Modules/Disk/popup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import Cocoa
import Kit

internal class Popup: PopupWrapper {
private var title: String

private var readColorState: SColor = .secondBlue
private var readColor: NSColor { self.readColorState.additional as? NSColor ?? NSColor.systemRed }
private var writeColorState: SColor = .secondRed
Expand Down Expand Up @@ -43,9 +41,7 @@ internal class Popup: PopupWrapper {
private var lastList: [String] = []

public init(_ module: ModuleType) {
self.title = module.rawValue

super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))
super.init(module, frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0))

self.readColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_readColor", defaultValue: self.readColorState.key))
self.writeColorState = SColor.fromString(Store.shared.string(key: "\(self.title)_writeColor", defaultValue: self.writeColorState.key))
Expand Down Expand Up @@ -193,6 +189,13 @@ internal class Popup: PopupWrapper {
public override func settings() -> NSView? {
let view = SettingsContainerView()

view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Keyboard shortcut"), component: KeyboardShartcutView(
callback: self.setKeyboardShortcut,
value: self.keyboardShortcut
))
]))

view.addArrangedSubview(PreferencesSection([
PreferencesRow(localizedString("Write color"), component: selectView(
action: #selector(self.toggleWriteColor),
Expand Down
Loading

0 comments on commit 58ad6c5

Please sign in to comment.