mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
735 lines
25 KiB
Swift
735 lines
25 KiB
Swift
//
|
|
// extensions.swift
|
|
// Kit
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 10/04/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import Carbon
|
|
|
|
extension String: @retroactive LocalizedError {
|
|
public var errorDescription: String? { return self }
|
|
|
|
public var nilIfEmpty: String? { self.isEmpty ? nil : self }
|
|
|
|
public var digits: String {
|
|
return components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
|
|
}
|
|
|
|
public func widthOfString(usingFont font: NSFont) -> CGFloat {
|
|
let fontAttributes = [NSAttributedString.Key.font: font]
|
|
let size = self.size(withAttributes: fontAttributes)
|
|
return size.width
|
|
}
|
|
|
|
public func condenseWhitespace() -> String {
|
|
let components = self.components(separatedBy: .whitespacesAndNewlines)
|
|
return components.filter { !$0.isEmpty }.joined(separator: " ")
|
|
}
|
|
|
|
public func findAndCrop(pattern: String) -> (cropped: String, remain: String) {
|
|
do {
|
|
let regex = try NSRegularExpression(pattern: pattern)
|
|
let range = NSRange(self.startIndex..., in: self)
|
|
|
|
if let match = regex.firstMatch(in: self, options: [], range: range) {
|
|
if let range = Range(match.range, in: self) {
|
|
let cropped = String(self[range]).trimmingCharacters(in: .whitespaces)
|
|
let remaining = self.replacingOccurrences(of: cropped, with: "", options: .regularExpression).trimmingCharacters(in: .whitespaces)
|
|
return (cropped, remaining)
|
|
}
|
|
}
|
|
} catch {
|
|
print("Error creating regex: \(error.localizedDescription)")
|
|
}
|
|
|
|
return ("", self)
|
|
}
|
|
|
|
public func find(pattern: String) -> String {
|
|
do {
|
|
let regex = try NSRegularExpression(pattern: pattern)
|
|
let stringRange = NSRange(location: 0, length: self.utf16.count)
|
|
|
|
if let searchRange = regex.firstMatch(in: self, options: [], range: stringRange) {
|
|
let start = self.index(self.startIndex, offsetBy: searchRange.range.lowerBound)
|
|
let end = self.index(self.startIndex, offsetBy: searchRange.range.upperBound)
|
|
let value = String(self[start..<end]).trimmingCharacters(in: .whitespaces)
|
|
return value.trimmingCharacters(in: .whitespaces)
|
|
}
|
|
} catch {}
|
|
|
|
return ""
|
|
}
|
|
|
|
public var trimmed: String {
|
|
var buf = [UInt8]()
|
|
var trimming = true
|
|
for c in self.utf8 {
|
|
if trimming && c < 33 { continue }
|
|
trimming = false
|
|
buf.append(c)
|
|
}
|
|
|
|
while let last = buf.last, last < 33 {
|
|
buf.removeLast()
|
|
}
|
|
|
|
buf.append(0)
|
|
return String(cString: buf)
|
|
}
|
|
|
|
public func matches(_ regex: String) -> Bool {
|
|
return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil
|
|
}
|
|
|
|
public func removedRegexMatches(pattern: String, replaceWith: String = "") -> String {
|
|
do {
|
|
let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive)
|
|
let range = NSRange(location: 0, length: self.count)
|
|
return regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith)
|
|
} catch {
|
|
return self
|
|
}
|
|
}
|
|
|
|
func removingWhitespaces() -> String {
|
|
return components(separatedBy: .whitespaces).joined()
|
|
}
|
|
}
|
|
|
|
public extension DispatchSource.MemoryPressureEvent {
|
|
func pressureColor() -> NSColor {
|
|
switch self {
|
|
case .normal:
|
|
return NSColor.systemGreen
|
|
case .warning:
|
|
return NSColor.systemYellow
|
|
case .critical:
|
|
return NSColor.systemRed
|
|
default:
|
|
return .controlAccentColor
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension Double {
|
|
func roundTo(decimalPlaces: Int) -> String {
|
|
return NSString(format: "%.\(decimalPlaces)f" as NSString, self) as String
|
|
}
|
|
|
|
func rounded(toPlaces places: Int) -> Double {
|
|
let divisor = pow(10.0, Double(places))
|
|
return (self * divisor).rounded() / divisor
|
|
}
|
|
|
|
func usageColor(zones: colorZones = (0.6, 0.8), reversed: Bool = false) -> NSColor {
|
|
let firstColor: NSColor = NSColor.systemBlue
|
|
let secondColor: NSColor = NSColor.orange
|
|
let thirdColor: NSColor = NSColor.red
|
|
|
|
if reversed {
|
|
switch self {
|
|
case 0...zones.orange:
|
|
return thirdColor
|
|
case zones.orange...zones.red:
|
|
return secondColor
|
|
default:
|
|
return firstColor
|
|
}
|
|
} else {
|
|
switch self {
|
|
case 0...zones.orange:
|
|
return firstColor
|
|
case zones.orange...zones.red:
|
|
return secondColor
|
|
default:
|
|
return thirdColor
|
|
}
|
|
}
|
|
}
|
|
|
|
func batteryColor(color: Bool = false, lowPowerMode: Bool? = nil) -> NSColor {
|
|
if let mode = lowPowerMode, mode {
|
|
return NSColor.systemOrange
|
|
}
|
|
|
|
switch self {
|
|
case 0.2...0.4:
|
|
if !color {
|
|
return NSColor.textColor
|
|
}
|
|
return NSColor.systemOrange
|
|
case 0.4...1:
|
|
if self == 1 {
|
|
return NSColor.textColor
|
|
}
|
|
if !color {
|
|
return NSColor.textColor
|
|
}
|
|
return NSColor.systemGreen
|
|
default:
|
|
return NSColor.systemRed
|
|
}
|
|
}
|
|
|
|
func secondsToHoursMinutesSeconds() -> (Int, Int) {
|
|
let mins = (self.truncatingRemainder(dividingBy: 3600)) / 60
|
|
return (Int(self / 3600), Int(mins))
|
|
}
|
|
|
|
func printSecondsToHoursMinutesSeconds(short: Bool = false) -> String {
|
|
let (h, m) = self.secondsToHoursMinutesSeconds()
|
|
|
|
if self == 0 || h < 0 || m < 0 {
|
|
return "n/a"
|
|
}
|
|
|
|
let minutes = m > 9 ? "\(m)" : "0\(m)"
|
|
|
|
if short {
|
|
return "\(h):\(minutes)"
|
|
}
|
|
|
|
if h == 0 {
|
|
return "\(minutes)min"
|
|
} else if m == 0 {
|
|
return "\(h)h"
|
|
}
|
|
|
|
return "\(h)h \(minutes)min"
|
|
}
|
|
|
|
func power(_ unit: String) -> Double {
|
|
switch unit {
|
|
case "mJ":
|
|
return self / 1e3
|
|
case "uJ":
|
|
return self / 1e6
|
|
case "nJ":
|
|
return self / 1e9
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension NSView {
|
|
var isDarkMode: Bool {
|
|
switch effectiveAppearance.name {
|
|
case .darkAqua, .vibrantDark, .accessibilityHighContrastDarkAqua, .accessibilityHighContrastVibrantDark:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func toggleSettingRow(title: String, action: Selector, state: Bool) -> NSView {
|
|
let view: NSStackView = NSStackView()
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.heightAnchor.constraint(equalToConstant: Constants.Settings.row).isActive = true
|
|
view.orientation = .horizontal
|
|
view.alignment = .centerY
|
|
view.distribution = .fill
|
|
view.spacing = 0
|
|
|
|
let titleField: NSTextField = LabelField(frame: NSRect(x: 0, y: 0, width: 0, height: 0), title)
|
|
titleField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
|
titleField.textColor = .textColor
|
|
|
|
let state: NSControl.StateValue = state ? .on : .off
|
|
var toggle: NSControl = NSControl()
|
|
if #available(OSX 10.15, *) {
|
|
let switchButton = NSSwitch()
|
|
switchButton.state = state
|
|
switchButton.action = action
|
|
switchButton.target = self
|
|
|
|
toggle = switchButton
|
|
} else {
|
|
let button: NSButton = NSButton()
|
|
button.setButtonType(.switch)
|
|
button.state = state
|
|
button.title = ""
|
|
button.action = action
|
|
button.isBordered = false
|
|
button.isTransparent = false
|
|
button.target = self
|
|
button.wantsLayer = true
|
|
|
|
toggle = button
|
|
}
|
|
|
|
view.addArrangedSubview(titleField)
|
|
view.addArrangedSubview(NSView())
|
|
view.addArrangedSubview(toggle)
|
|
|
|
return view
|
|
}
|
|
|
|
func selectSettingsRow(title: String, action: Selector, items: [KeyValue_p], selected: String) -> NSView {
|
|
let view = NSStackView()
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
view.heightAnchor.constraint(equalToConstant: Constants.Settings.row).isActive = true
|
|
view.orientation = .horizontal
|
|
view.alignment = .centerY
|
|
view.distribution = .fill
|
|
view.spacing = 0
|
|
|
|
let titleField: NSTextField = LabelField(frame: NSRect(x: 0, y: 0, width: 0, height: 0), title)
|
|
titleField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
|
titleField.textColor = .textColor
|
|
|
|
let select: NSPopUpButton = selectView(action: action, items: items, selected: selected)
|
|
select.sizeToFit()
|
|
|
|
view.addArrangedSubview(titleField)
|
|
view.addArrangedSubview(NSView())
|
|
view.addArrangedSubview(select)
|
|
|
|
return view
|
|
}
|
|
|
|
func selectView(action: Selector, items: [KeyValue_p], selected: String) -> NSPopUpButton {
|
|
let select: NSPopUpButton = NSPopUpButton(frame: NSRect(x: 0, y: 4, width: 50, height: 28))
|
|
select.target = self
|
|
select.action = action
|
|
|
|
let menu = NSMenu()
|
|
items.forEach { (item) in
|
|
if item.key.contains("separator") {
|
|
menu.addItem(NSMenuItem.separator())
|
|
} else {
|
|
let interfaceMenu = NSMenuItem(title: localizedString(item.value), action: nil, keyEquivalent: "")
|
|
interfaceMenu.representedObject = item.key
|
|
menu.addItem(interfaceMenu)
|
|
if selected == item.key {
|
|
interfaceMenu.state = .on
|
|
}
|
|
}
|
|
}
|
|
select.menu = menu
|
|
|
|
return select
|
|
}
|
|
|
|
func switchView(action: Selector, state: Bool) -> NSSwitch {
|
|
let s = NSSwitch()
|
|
s.heightAnchor.constraint(equalToConstant: 25).isActive = true
|
|
s.controlSize = .mini
|
|
s.state = state ? .on : .off
|
|
s.action = action
|
|
s.target = self
|
|
return s
|
|
}
|
|
|
|
func buttonView(_ action: Selector, text: String) -> NSButton {
|
|
let button = NSButton()
|
|
button.title = text
|
|
button.contentTintColor = .labelColor
|
|
button.action = action
|
|
button.target = self
|
|
return button
|
|
}
|
|
|
|
func buttonIconView(_ action: Selector, icon: NSImage, height: CGFloat = 22) -> NSButton {
|
|
let button = NSButton()
|
|
button.heightAnchor.constraint(equalToConstant: height).isActive = true
|
|
button.bezelStyle = .regularSquare
|
|
button.translatesAutoresizingMaskIntoConstraints = false
|
|
button.imageScaling = .scaleNone
|
|
button.image = icon
|
|
button.contentTintColor = .labelColor
|
|
button.isBordered = false
|
|
button.action = action
|
|
button.target = self
|
|
button.focusRingType = .none
|
|
return button
|
|
}
|
|
|
|
func textView(_ value: String, alignment: NSTextAlignment = .left) -> NSTextField {
|
|
let field: NSTextField = TextView()
|
|
field.font = NSFont.systemFont(ofSize: 13, weight: .regular)
|
|
field.stringValue = value
|
|
field.isSelectable = true
|
|
field.alignment = alignment
|
|
return field
|
|
}
|
|
|
|
func sliderView(action: Selector, value: Int, initialValue: String, min: Double = 1, max: Double = 100, valueWidth: CGFloat = 40) -> NSView {
|
|
let view: NSStackView = NSStackView()
|
|
view.orientation = .horizontal
|
|
view.widthAnchor.constraint(equalToConstant: 195).isActive = true
|
|
|
|
let valueField: NSTextField = LabelField(initialValue)
|
|
valueField.font = NSFont.systemFont(ofSize: 12, weight: .regular)
|
|
valueField.textColor = .textColor
|
|
valueField.alignment = .center
|
|
valueField.widthAnchor.constraint(equalToConstant: valueWidth).isActive = true
|
|
|
|
let slider = NSSlider()
|
|
slider.controlSize = .small
|
|
slider.minValue = min
|
|
slider.maxValue = max
|
|
slider.intValue = Int32(value)
|
|
slider.target = self
|
|
slider.isContinuous = true
|
|
slider.action = action
|
|
slider.sizeToFit()
|
|
|
|
view.addArrangedSubview(slider)
|
|
view.addArrangedSubview(valueField)
|
|
|
|
return view
|
|
}
|
|
}
|
|
|
|
public class NSButtonWithPadding: NSButton {
|
|
public var horizontalPadding: CGFloat = 0
|
|
public var verticalPadding: CGFloat = 0
|
|
|
|
public override var intrinsicContentSize: NSSize {
|
|
var size = super.intrinsicContentSize
|
|
size.width += self.horizontalPadding
|
|
size.height += self.verticalPadding
|
|
return size
|
|
}
|
|
}
|
|
|
|
public class TextView: NSTextField {
|
|
public override init(frame: NSRect = .zero) {
|
|
super.init(frame: frame)
|
|
|
|
self.isEditable = false
|
|
self.isSelectable = false
|
|
self.isBezeled = false
|
|
self.wantsLayer = true
|
|
self.textColor = .labelColor
|
|
self.backgroundColor = .clear
|
|
self.canDrawSubviewsIntoLayer = true
|
|
self.alignment = .natural
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
public extension OperatingSystemVersion {
|
|
func getFullVersion(separator: String = ".") -> String {
|
|
return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
|
|
}
|
|
}
|
|
|
|
extension URL {
|
|
func checkFileExist() -> Bool {
|
|
return FileManager.default.fileExists(atPath: self.path)
|
|
}
|
|
}
|
|
|
|
public extension NSColor {
|
|
func grayscaled() -> NSColor {
|
|
guard let space = CGColorSpace(name: CGColorSpace.extendedGray),
|
|
let cg = self.cgColor.converted(to: space, intent: .perceptual, options: nil),
|
|
let color = NSColor.init(cgColor: cg) else {
|
|
return self
|
|
}
|
|
return color
|
|
}
|
|
}
|
|
|
|
public class FlippedStackView: NSStackView {
|
|
public override var isFlipped: Bool { return true }
|
|
}
|
|
|
|
open class ScrollableStackView: NSView {
|
|
public var stackView: NSStackView = FlippedStackView()
|
|
|
|
private let clipView: NSClipView = NSClipView()
|
|
private let scrollView: NSScrollView = NSScrollView()
|
|
|
|
public var scrollWidth: CGFloat? {
|
|
self.scrollView.verticalScroller?.frame.size.width
|
|
}
|
|
|
|
public init(frame: NSRect = NSRect.zero, orientation: NSUserInterfaceLayoutOrientation = .vertical) {
|
|
super.init(frame: frame)
|
|
|
|
self.clipView.drawsBackground = false
|
|
|
|
self.stackView.orientation = orientation
|
|
self.stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
self.scrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
if orientation == .vertical {
|
|
self.scrollView.hasVerticalScroller = true
|
|
self.scrollView.hasHorizontalScroller = false
|
|
self.scrollView.autohidesScrollers = true
|
|
self.scrollView.horizontalScrollElasticity = .none
|
|
} else {
|
|
self.scrollView.hasVerticalScroller = false
|
|
self.scrollView.hasHorizontalScroller = true
|
|
self.scrollView.autohidesScrollers = true
|
|
self.scrollView.verticalScrollElasticity = .none
|
|
}
|
|
self.scrollView.drawsBackground = false
|
|
self.scrollView.contentView = self.clipView
|
|
self.scrollView.documentView = self.stackView
|
|
|
|
self.addSubview(self.scrollView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
self.scrollView.leftAnchor.constraint(equalTo: self.leftAnchor),
|
|
self.scrollView.rightAnchor.constraint(equalTo: self.rightAnchor),
|
|
self.scrollView.topAnchor.constraint(equalTo: self.topAnchor),
|
|
self.scrollView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
|
|
|
self.stackView.leftAnchor.constraint(equalTo: self.clipView.leftAnchor),
|
|
self.stackView.topAnchor.constraint(equalTo: self.clipView.topAnchor)
|
|
])
|
|
|
|
if orientation == .vertical {
|
|
self.stackView.rightAnchor.constraint(equalTo: self.clipView.rightAnchor).isActive = true
|
|
} else {
|
|
self.stackView.bottomAnchor.constraint(equalTo: self.clipView.bottomAnchor).isActive = true
|
|
}
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|
|
|
|
// https://stackoverflow.com/a/54492165
|
|
extension NSTextView {
|
|
override open func performKeyEquivalent(with event: NSEvent) -> Bool {
|
|
let commandKey = NSEvent.ModifierFlags.command.rawValue
|
|
let commandShiftKey = NSEvent.ModifierFlags.command.rawValue | NSEvent.ModifierFlags.shift.rawValue
|
|
if event.type == NSEvent.EventType.keyDown {
|
|
if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandKey {
|
|
switch event.charactersIgnoringModifiers! {
|
|
case "x":
|
|
if NSApp.sendAction(#selector(NSText.cut(_:)), to: nil, from: self) { return true }
|
|
case "c":
|
|
if NSApp.sendAction(#selector(NSText.copy(_:)), to: nil, from: self) { return true }
|
|
case "v":
|
|
if NSApp.sendAction(#selector(NSText.paste(_:)), to: nil, from: self) { return true }
|
|
case "z":
|
|
if NSApp.sendAction(Selector(("undo:")), to: nil, from: self) { return true }
|
|
case "a":
|
|
if NSApp.sendAction(#selector(NSResponder.selectAll(_:)), to: nil, from: self) { return true }
|
|
default:
|
|
break
|
|
}
|
|
} else if (event.modifierFlags.rawValue & NSEvent.ModifierFlags.deviceIndependentFlagsMask.rawValue) == commandShiftKey {
|
|
if event.charactersIgnoringModifiers == "Z" {
|
|
if NSApp.sendAction(Selector(("redo:")), to: nil, from: self) { return true }
|
|
}
|
|
}
|
|
}
|
|
return super.performKeyEquivalent(with: event)
|
|
}
|
|
}
|
|
|
|
public extension Data {
|
|
var socketAddress: sockaddr {
|
|
return withUnsafeBytes { $0.load(as: sockaddr.self) }
|
|
}
|
|
}
|
|
|
|
public extension Date {
|
|
func convertToTimeZone(_ timeZone: TimeZone) -> Date {
|
|
return addingTimeInterval(TimeInterval(timeZone.secondsFromGMT(for: self) - TimeZone.current.secondsFromGMT(for: self)))
|
|
}
|
|
|
|
func currentTimeSeconds() -> Int {
|
|
return Int(self.timeIntervalSince1970)
|
|
}
|
|
}
|
|
|
|
public extension TimeZone {
|
|
init(from: String) {
|
|
if let tz = TimeZone(identifier: from) {
|
|
self = tz
|
|
return
|
|
}
|
|
|
|
if from == "local" {
|
|
self = TimeZone.current
|
|
return
|
|
}
|
|
|
|
let arr = from.split(separator: ":")
|
|
guard !arr.isEmpty else {
|
|
self = TimeZone.current
|
|
return
|
|
}
|
|
|
|
var secondsFromGMT = 0
|
|
if arr.indices.contains(0), let h = Int(arr[0]) {
|
|
secondsFromGMT += h*3600
|
|
}
|
|
if arr.indices.contains(1), let m = Int(arr[1]) {
|
|
if secondsFromGMT < 0 {
|
|
secondsFromGMT -= m*60
|
|
} else {
|
|
secondsFromGMT += m*60
|
|
}
|
|
}
|
|
|
|
if let tz = TimeZone(secondsFromGMT: secondsFromGMT) {
|
|
self = tz
|
|
} else {
|
|
self = TimeZone.current
|
|
}
|
|
}
|
|
}
|
|
|
|
extension CGFloat {
|
|
func roundedUpToNearestTen() -> CGFloat {
|
|
return ceil(self / 10) * 10
|
|
}
|
|
}
|
|
|
|
public class KeyboardShartcutView: NSStackView {
|
|
private let callback: (_ value: [UInt16]) -> Void
|
|
|
|
private var startIcon: NSImage { iconFromSymbol(name: "record.circle", scale: .large) }
|
|
private var stopIcon: NSImage { iconFromSymbol(name: "stop.circle.fill", scale: .large) }
|
|
|
|
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)
|
|
}
|
|
}
|