Add USB mouse/trackpad auto-detection, cleanup
This commit is contained in:
parent
4403fb8eca
commit
e7da1fac30
|
@ -11,6 +11,8 @@
|
||||||
D62D7D8326DE7A9F001DCC5F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D62D7D8226DE7A9F001DCC5F /* Assets.xcassets */; };
|
D62D7D8326DE7A9F001DCC5F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D62D7D8226DE7A9F001DCC5F /* Assets.xcassets */; };
|
||||||
D62D7D9226DF237A001DCC5F /* PreferencePanesSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62D7D9126DF237A001DCC5F /* PreferencePanesSupport.framework */; };
|
D62D7D9226DF237A001DCC5F /* PreferencePanesSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D62D7D9126DF237A001DCC5F /* PreferencePanesSupport.framework */; };
|
||||||
D6955CDC26DF287800EB0723 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6955CDB26DF287800EB0723 /* main.swift */; };
|
D6955CDC26DF287800EB0723 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6955CDB26DF287800EB0723 /* main.swift */; };
|
||||||
|
D6AB3C8A26EE52020019A4F7 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AB3C8926EE52010019A4F7 /* Preferences.swift */; };
|
||||||
|
D6AB3C8C26EE56540019A4F7 /* Direction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6AB3C8B26EE56540019A4F7 /* Direction.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -21,6 +23,8 @@
|
||||||
D62D7D8F26DF228D001DCC5F /* ScrollSwitcher-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ScrollSwitcher-Bridging-Header.h"; sourceTree = "<group>"; };
|
D62D7D8F26DF228D001DCC5F /* ScrollSwitcher-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ScrollSwitcher-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
D62D7D9126DF237A001DCC5F /* PreferencePanesSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanesSupport.framework; path = ../../../../System/Library/PrivateFrameworks/PreferencePanesSupport.framework; sourceTree = "<group>"; };
|
D62D7D9126DF237A001DCC5F /* PreferencePanesSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PreferencePanesSupport.framework; path = ../../../../System/Library/PrivateFrameworks/PreferencePanesSupport.framework; sourceTree = "<group>"; };
|
||||||
D6955CDB26DF287800EB0723 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
D6955CDB26DF287800EB0723 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||||
|
D6AB3C8926EE52010019A4F7 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||||
|
D6AB3C8B26EE56540019A4F7 /* Direction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Direction.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -58,6 +62,8 @@
|
||||||
D62D7D8C26DE7B36001DCC5F /* Info.plist */,
|
D62D7D8C26DE7B36001DCC5F /* Info.plist */,
|
||||||
D6955CDB26DF287800EB0723 /* main.swift */,
|
D6955CDB26DF287800EB0723 /* main.swift */,
|
||||||
D62D7D7E26DE7A9D001DCC5F /* AppDelegate.swift */,
|
D62D7D7E26DE7A9D001DCC5F /* AppDelegate.swift */,
|
||||||
|
D6AB3C8B26EE56540019A4F7 /* Direction.swift */,
|
||||||
|
D6AB3C8926EE52010019A4F7 /* Preferences.swift */,
|
||||||
D62D7D8F26DF228D001DCC5F /* ScrollSwitcher-Bridging-Header.h */,
|
D62D7D8F26DF228D001DCC5F /* ScrollSwitcher-Bridging-Header.h */,
|
||||||
D62D7D8226DE7A9F001DCC5F /* Assets.xcassets */,
|
D62D7D8226DE7A9F001DCC5F /* Assets.xcassets */,
|
||||||
);
|
);
|
||||||
|
@ -143,6 +149,8 @@
|
||||||
files = (
|
files = (
|
||||||
D6955CDC26DF287800EB0723 /* main.swift in Sources */,
|
D6955CDC26DF287800EB0723 /* main.swift in Sources */,
|
||||||
D62D7D7F26DE7A9D001DCC5F /* AppDelegate.swift in Sources */,
|
D62D7D7F26DE7A9D001DCC5F /* AppDelegate.swift in Sources */,
|
||||||
|
D6AB3C8A26EE52020019A4F7 /* Preferences.swift in Sources */,
|
||||||
|
D6AB3C8C26EE56540019A4F7 /* Direction.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -283,6 +291,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 11.3;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ScrollSwitcher;
|
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ScrollSwitcher;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
@ -315,6 +324,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
|
MACOSX_DEPLOYMENT_TARGET = 11.3;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ScrollSwitcher;
|
PRODUCT_BUNDLE_IDENTIFIER = net.shadowfacts.ScrollSwitcher;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|
|
@ -6,14 +6,24 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
import IOKit
|
||||||
|
import Combine
|
||||||
|
import OSLog
|
||||||
|
|
||||||
let defaultsKey = "com.apple.swipescrolldirection"
|
let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "main")
|
||||||
|
|
||||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
// things that need to be retained to keep them from disappearing
|
// things that need to be retained to keep them from disappearing
|
||||||
private var prefPanesSupport: Bundle!
|
private var prefPanesSupport: Bundle!
|
||||||
private var item: NSStatusItem!
|
private var item: NSStatusItem!
|
||||||
|
private var manager: IOHIDManager!
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
// internal not private because they need to be accessible from global IOHIDManager callbacks
|
||||||
|
var hidDevicesChangedSubject = PassthroughSubject<Void, Never>()
|
||||||
|
var trackpadCount = 0
|
||||||
|
var mouseCount = 0
|
||||||
|
|
||||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
||||||
|
@ -23,6 +33,28 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
// update the icon when system prefs changes
|
// update the icon when system prefs changes
|
||||||
DistributedNotificationCenter.default().addObserver(self, selector: #selector(updateIcon), name: .swipeScrollDirectionDidChangeNotification, object: nil)
|
DistributedNotificationCenter.default().addObserver(self, selector: #selector(updateIcon), name: .swipeScrollDirectionDidChangeNotification, object: nil)
|
||||||
|
|
||||||
|
// register for HID device addition/removal notifications
|
||||||
|
manager = IOHIDManagerCreate(kCFAllocatorDefault, 0 /* kIOHIDManagerOptionNone */)
|
||||||
|
|
||||||
|
var dict = IOServiceMatching(kIOHIDDeviceKey)! as! [String: Any]
|
||||||
|
dict[kIOHIDDeviceUsagePageKey] = kHIDPage_GenericDesktop
|
||||||
|
dict[kIOHIDDeviceUsageKey] = kHIDUsage_GD_Mouse
|
||||||
|
IOHIDManagerSetDeviceMatching(manager, dict as CFDictionary)
|
||||||
|
|
||||||
|
IOHIDManagerRegisterDeviceMatchingCallback(manager, hidDeviceAdded(context:result:sender:device:), nil)
|
||||||
|
IOHIDManagerRegisterDeviceRemovalCallback(manager, hidDeviceRemoved(context:result:sender:device:), nil)
|
||||||
|
|
||||||
|
IOHIDManagerScheduleWithRunLoop(manager, CFRunLoopGetCurrent(), CFRunLoopMode.commonModes.rawValue)
|
||||||
|
|
||||||
|
// handle HID device changes debounced, because IOKit sends a whole at initialization (and seemingly duplicates when devices are removed/connected)
|
||||||
|
hidDevicesChangedSubject
|
||||||
|
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
|
||||||
|
.sink { [unowned self] (_) in
|
||||||
|
logger.info("HID devices changed, trackpads: \(self.trackpadCount, privacy: .public), mice: \(self.mouseCount, privacy: .public)")
|
||||||
|
self.updateDirectionForAutoMode()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func updateIcon() {
|
@objc private func updateIcon() {
|
||||||
|
@ -43,11 +75,31 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
menu.delegate = self
|
menu.delegate = self
|
||||||
let state = Direction.current == .natural ? "On" : "Off"
|
let state = Direction.current == .natural ? "On" : "Off"
|
||||||
let status = NSMenuItem(title: "Natural Scrolling: \(state)", action: nil, keyEquivalent: "")
|
let status = NSMenuItem(title: "Natural Scrolling: \(state)", action: nil, keyEquivalent: "")
|
||||||
|
status.isEnabled = true
|
||||||
|
status.attributedTitle = NSAttributedString(string: "Natural Scrolling: \(state)", attributes: [
|
||||||
|
.foregroundColor: NSColor.white,
|
||||||
|
])
|
||||||
menu.addItem(status)
|
menu.addItem(status)
|
||||||
let verb = Direction.current == .natural ? "Disable" : "Enable"
|
let verb = Direction.current == .natural ? "Disable" : "Enable"
|
||||||
let toggleItem = NSMenuItem(title: "\(verb) Natural Scrolling", action: #selector(toggleScrollDirection), keyEquivalent: "")
|
let toggleItem = NSMenuItem(title: "\(verb) Natural Scrolling", action: #selector(toggleScrollDirection), keyEquivalent: "")
|
||||||
menu.addItem(toggleItem)
|
menu.addItem(toggleItem)
|
||||||
|
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
|
let autoItem = NSMenuItem(title: "Auto Switching Mode", action: nil, keyEquivalent: "")
|
||||||
|
let autoMenu = NSMenu()
|
||||||
|
for mode in Preferences.AutoMode.allCases {
|
||||||
|
let modeItem = NSMenuItem(title: mode.displayName, action: #selector(modeChanged(_:)), keyEquivalent: "")
|
||||||
|
modeItem.tag = mode.rawValue
|
||||||
|
modeItem.toolTip = mode.displayDescription
|
||||||
|
modeItem.state = Preferences.autoMode == mode ? .on : .off
|
||||||
|
autoMenu.addItem(modeItem)
|
||||||
|
}
|
||||||
|
autoItem.submenu = autoMenu
|
||||||
|
menu.addItem(autoItem)
|
||||||
|
|
||||||
|
menu.addItem(NSMenuItem.separator())
|
||||||
|
|
||||||
menu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
menu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
|
@ -56,31 +108,29 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
setDirection(.current == Direction.natural ? .normal : .natural)
|
setDirection(.current == Direction.natural ? .normal : .natural)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setDirection(_ new: Direction) {
|
@objc private func modeChanged(_ sender: NSMenuItem) {
|
||||||
// can't construct a UserDefaults with NSGlobalDomain, so set it via the command line
|
Preferences.autoMode = Preferences.AutoMode(rawValue: sender.tag) ?? .disabled
|
||||||
let proc = Process()
|
updateDirectionForAutoMode()
|
||||||
// let out = Pipe()
|
|
||||||
// let err = Pipe()
|
|
||||||
proc.launchPath = "/usr/bin/env"
|
|
||||||
let newVal = new == .normal ? "NO" : "YES"
|
|
||||||
proc.arguments = ["defaults", "write", "-g", "com.apple.swipescrolldirection", "-bool", newVal]
|
|
||||||
// proc.standardOutput = out
|
|
||||||
// proc.standardError = err
|
|
||||||
proc.launch()
|
|
||||||
proc.waitUntilExit()
|
|
||||||
|
|
||||||
// let outData = out.fileHandleForReading.readDataToEndOfFile()
|
|
||||||
// let errData = err.fileHandleForReading.readDataToEndOfFile()
|
|
||||||
if proc.terminationStatus != 0 {
|
|
||||||
fatalError("uh oh, exit code: \(proc.terminationStatus)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// makes system preferences update
|
private func setDirection(_ new: Direction) {
|
||||||
DistributedNotificationCenter.default().postNotificationName(.swipeScrollDirectionDidChangeNotification, object: nil, userInfo: nil, deliverImmediately: false)
|
logger.debug("Changing scroll direction to \(new == .normal ? "Normal" : "Natural", privacy: .public)")
|
||||||
// make the actual input behavior update
|
|
||||||
setSwipeScrollDirection(new == .natural)
|
setSwipeScrollDirection(new == .natural)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateDirectionForAutoMode() {
|
||||||
|
switch Preferences.autoMode {
|
||||||
|
case .disabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
case .normalWhenMousePresent:
|
||||||
|
setDirection(mouseCount > 0 ? .normal : .natural)
|
||||||
|
|
||||||
|
case .naturalWhenTrackpadPresent:
|
||||||
|
setDirection(trackpadCount > 0 ? .natural : .normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppDelegate: NSMenuDelegate {
|
extension AppDelegate: NSMenuDelegate {
|
||||||
|
@ -90,28 +140,81 @@ extension AppDelegate: NSMenuDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Direction: Equatable {
|
|
||||||
case normal, natural
|
|
||||||
|
|
||||||
static var current: Direction {
|
|
||||||
let value = UserDefaults.standard.bool(forKey: defaultsKey)
|
|
||||||
if value {
|
|
||||||
return .natural
|
|
||||||
} else {
|
|
||||||
return .normal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var image: NSImage {
|
|
||||||
switch self {
|
|
||||||
case .normal:
|
|
||||||
return NSImage(systemSymbolName: "scroll", accessibilityDescription: nil)!
|
|
||||||
case .natural:
|
|
||||||
return NSImage(systemSymbolName: "scroll.fill", accessibilityDescription: nil)!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Notification.Name {
|
extension Notification.Name {
|
||||||
static let swipeScrollDirectionDidChangeNotification = Notification.Name(rawValue: "SwipeScrollDirectionDidChangeNotification")
|
static let swipeScrollDirectionDidChangeNotification = Notification.Name(rawValue: "SwipeScrollDirectionDidChangeNotification")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hidDeviceAdded(context: UnsafeMutableRawPointer?, result: IOReturn, sender: UnsafeMutableRawPointer?, device: IOHIDDevice) {
|
||||||
|
// it is not clear to me why you can interpolate device here but not in the log message
|
||||||
|
let deviceDesc = "\(device)"
|
||||||
|
|
||||||
|
let name = IOHIDDeviceGetProperty(device, kIOHIDProductKey as CFString) as? String
|
||||||
|
guard let name = name else {
|
||||||
|
logger.warning("Could not get product name for \(deviceDesc, privacy: .public)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let usage = IOHIDDeviceGetProperty(device, kIOHIDPrimaryUsageKey as CFString) as? UInt32
|
||||||
|
// we get this callback for non kHIDUsage_GD_Mouse devices even though we specify that in the matching dict
|
||||||
|
// (specifically, something with kHIDUsage_GD_SystemControl), so filter ourselves
|
||||||
|
guard let usage = usage else {
|
||||||
|
logger.warning("Could not get usage for \(deviceDesc, privacy: .public)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard usage == kHIDUsage_GD_Mouse else {
|
||||||
|
logger.info("Unexpected usage 0x\(usage, format: .hex, privacy: .public) for device '\(name, privacy: .public)'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("HID device '\(name, privacy: .public)' added")
|
||||||
|
|
||||||
|
let delegate = NSApp.delegate as! AppDelegate
|
||||||
|
|
||||||
|
if deviceNameIsProbablyTrackpad(name) {
|
||||||
|
delegate.trackpadCount += 1
|
||||||
|
} else {
|
||||||
|
delegate.mouseCount += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate.hidDevicesChangedSubject.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hidDeviceRemoved(context: UnsafeMutableRawPointer?, result: IOReturn, sender: UnsafeMutableRawPointer?, device: IOHIDDevice) {
|
||||||
|
let deviceDesc = "\(device)"
|
||||||
|
|
||||||
|
let name = IOHIDDeviceGetProperty(device, kIOHIDProductKey as CFString) as? String
|
||||||
|
guard let name = name else {
|
||||||
|
logger.warning("Could not get product name for \(deviceDesc, privacy: .public)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let usage = IOHIDDeviceGetProperty(device, kIOHIDPrimaryUsageKey as CFString) as? UInt32
|
||||||
|
guard let usage = usage else {
|
||||||
|
logger.warning("Could not get usage for \(deviceDesc, privacy: .public)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard usage == kHIDUsage_GD_Mouse else {
|
||||||
|
logger.info("Unexpected usage 0x\(usage, format: .hex, privacy: .public) for device '\(name, privacy: .public)'")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("HID device '\(name, privacy: .public)' removed")
|
||||||
|
|
||||||
|
let delegate = NSApp.delegate as! AppDelegate
|
||||||
|
|
||||||
|
if deviceNameIsProbablyTrackpad(name) {
|
||||||
|
delegate.trackpadCount -= 1
|
||||||
|
} else {
|
||||||
|
delegate.mouseCount -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate.hidDevicesChangedSubject.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// dumb heuristics because USB HID doesn't differentiate between mice/trackpads
|
||||||
|
func deviceNameIsProbablyTrackpad(_ name: String) -> Bool {
|
||||||
|
if name.lowercased().contains("trackpad") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
//
|
||||||
|
// Direction.swift
|
||||||
|
// ScrollSwitcher
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/12/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
let defaultsKey = "com.apple.swipescrolldirection"
|
||||||
|
|
||||||
|
enum Direction: Equatable {
|
||||||
|
case normal, natural
|
||||||
|
|
||||||
|
static var current: Direction {
|
||||||
|
let value = UserDefaults.standard.bool(forKey: defaultsKey)
|
||||||
|
if value {
|
||||||
|
return .natural
|
||||||
|
} else {
|
||||||
|
return .normal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var image: NSImage {
|
||||||
|
switch self {
|
||||||
|
case .normal:
|
||||||
|
return NSImage(systemSymbolName: "scroll", accessibilityDescription: nil)!
|
||||||
|
case .natural:
|
||||||
|
return NSImage(systemSymbolName: "scroll.fill", accessibilityDescription: nil)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
//
|
||||||
|
// Preferences.swift
|
||||||
|
// ScrollSwitcher
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 9/12/21.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Preferences {
|
||||||
|
|
||||||
|
private static let autoModeKey = "autoMode"
|
||||||
|
static var autoMode: AutoMode {
|
||||||
|
get {
|
||||||
|
AutoMode(rawValue: UserDefaults.standard.integer(forKey: autoModeKey)) ?? .disabled
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
UserDefaults.standard.set(newValue.rawValue, forKey: autoModeKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Preferences {
|
||||||
|
enum AutoMode: Int, Codable, CaseIterable, Equatable {
|
||||||
|
case disabled = 0
|
||||||
|
case normalWhenMousePresent = 1
|
||||||
|
case naturalWhenTrackpadPresent = 2
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .disabled:
|
||||||
|
return "Disabled"
|
||||||
|
case .normalWhenMousePresent:
|
||||||
|
return "Normal when mouse present"
|
||||||
|
case .naturalWhenTrackpadPresent:
|
||||||
|
return "Natural when trackpad present"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .disabled:
|
||||||
|
return "Scroll direction is never changed automatically"
|
||||||
|
case .normalWhenMousePresent:
|
||||||
|
return "Scroll direction is changed to normal when at least 1 mouse is connected. Optimal for computers with built-in trackpads."
|
||||||
|
case .naturalWhenTrackpadPresent:
|
||||||
|
return "Scroll direction is changed to natural when at least 1 trackpad is connected. Optimal for computers with rarely-connected trackpads."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,8 +8,8 @@
|
||||||
#ifndef ScrollSwitcher_Bridging_Header_h
|
#ifndef ScrollSwitcher_Bridging_Header_h
|
||||||
#define ScrollSwitcher_Bridging_Header_h
|
#define ScrollSwitcher_Bridging_Header_h
|
||||||
|
|
||||||
#import <CoreFoundation/CoreFoundation.h>
|
#import <stdbool.h>
|
||||||
|
|
||||||
void setSwipeScrollDirection(BOOL natural);
|
extern void setSwipeScrollDirection(bool natural);
|
||||||
|
|
||||||
#endif /* ScrollSwitcher_Bridging_Header_h */
|
#endif /* ScrollSwitcher_Bridging_Header_h */
|
||||||
|
|
Loading…
Reference in New Issue