Skip to content

Commit

Permalink
✨ add trigger delay and improve permission for accessibility
Browse files Browse the repository at this point in the history
  • Loading branch information
Keyruu committed Sep 7, 2024
1 parent b229519 commit e8f01eb
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 64 deletions.
12 changes: 8 additions & 4 deletions Tabula.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
11C795A32C7611C100CF3F6A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C795A22C7611C100CF3F6A /* ContentView.swift */; };
11C795A52C7620A100CF3F6A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C795A42C7620A100CF3F6A /* AppDelegate.swift */; };
11C795A92C7875CA00CF3F6A /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 11C795A82C7875CA00CF3F6A /* LaunchAtLogin */; };
DBB7ED102C8CC69F00E055E6 /* PermissionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB7ED0F2C8CC69F00E055E6 /* PermissionsService.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -42,6 +43,7 @@
11C7958B2C7499F500CF3F6A /* TabulaUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TabulaUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
11C795A22C7611C100CF3F6A /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
11C795A42C7620A100CF3F6A /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
DBB7ED0F2C8CC69F00E055E6 /* PermissionsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsService.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -97,6 +99,7 @@
11C7957C2C7499F500CF3F6A /* Tabula.entitlements */,
11C795792C7499F500CF3F6A /* Preview Content */,
11C795A22C7611C100CF3F6A /* ContentView.swift */,
DBB7ED0F2C8CC69F00E055E6 /* PermissionsService.swift */,
);
path = Tabula;
sourceTree = "<group>";
Expand Down Expand Up @@ -245,6 +248,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DBB7ED102C8CC69F00E055E6 /* PermissionsService.swift in Sources */,
11C795A52C7620A100CF3F6A /* AppDelegate.swift in Sources */,
11C795722C7499F400CF3F6A /* TabulaApp.swift in Sources */,
11C795A32C7611C100CF3F6A /* ContentView.swift in Sources */,
Expand Down Expand Up @@ -412,7 +416,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1.4.0;
CURRENT_PROJECT_VERSION = 1.5.0;
DEVELOPMENT_ASSET_PATHS = "\"Tabula/Preview Content\"";
DEVELOPMENT_TEAM = 3S6Q428WC7;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -425,7 +429,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.4.0;
MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = de.keyruu.Tabula;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -442,7 +446,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1.4.0;
CURRENT_PROJECT_VERSION = 1.5.0;
DEVELOPMENT_ASSET_PATHS = "\"Tabula/Preview Content\"";
DEVELOPMENT_TEAM = 3S6Q428WC7;
ENABLE_HARDENED_RUNTIME = YES;
Expand All @@ -455,7 +459,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.4.0;
MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = de.keyruu.Tabula;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down
110 changes: 60 additions & 50 deletions Tabula/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,23 @@
import Foundation
import Cocoa
import AppKit
import SwiftUI

class AppDelegate: NSObject, NSApplicationDelegate {
private var flagsMonitor: Any?
private var permissionsService = PermissionsService.shared

func applicationDidFinishLaunching(_ notification: Notification) {
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
let enabled = AXIsProcessTrustedWithOptions(options)
if !enabled {
CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: 0, wheel2: 0, wheel3: 0)?.post(tap: CGEventTapLocation.cghidEventTap)
UserDefaults.standard.setValue(true, forKey: "needsPermission")
return
} else {
UserDefaults.standard.setValue(false, forKey: "needsPermission")
}

PermissionsService.acquireAccessibilityPrivileges()
permissionsService.pollAccessibilityPrivileges(onTrusted: self.scrollMonitor)
}

func scrollMonitor() {
var mouseMonitor: Any?
var initialPos: CGPoint?
var lastPos: CGPoint?
var lastDelta: CGPoint?
var stillPressed = true
flagsMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { event in
if let mouseMonitor = mouseMonitor {
NSEvent.removeMonitor(mouseMonitor)
Expand All @@ -36,55 +34,67 @@ class AppDelegate: NSObject, NSApplicationDelegate {
var all = self.allModifiers()
all.remove(self.getModifierFlag())
if event.modifierFlags.contains(self.getModifierFlag()) && event.modifierFlags.intersection(all).isEmpty {
let scrollSpeedAny = UserDefaults.standard
.object(forKey: "scrollSpeed")
let scrollSpeed = scrollSpeedAny != nil ? scrollSpeedAny as! CGFloat : 20.0
stillPressed = true

let naturalScrollingAny = UserDefaults.standard.object(forKey: "naturalScrolling")
let naturalScrolling = naturalScrollingAny != nil ? naturalScrollingAny as! Bool : true
let xEnabledAny = UserDefaults.standard.object(forKey: "xEnabled")
let xEnabled = xEnabledAny != nil ? xEnabledAny as! Bool : true
let yEnabledAny = UserDefaults.standard.object(forKey: "yEnabled")
let yEnabled = yEnabledAny != nil ? yEnabledAny as! Bool : true
let triggerDelayAny = UserDefaults.standard
.object(forKey: "triggerDelay")
let triggerDelay = triggerDelayAny != nil ? triggerDelayAny as! Double : 0.0

initialPos = CGEvent(source: nil)!.location
mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { _ in
let pos = CGEvent(source: nil)!.location
if let lastPos = lastPos {
let delta = CGPoint(x: lastPos.x - pos.x, y: lastPos.y - pos.y)
if delta == CGPoint() {
return
}
var scroll = true
if let lastDelta = lastDelta {
if delta == CGPoint(x:-lastDelta.x,y:-lastDelta.y) {
scroll = false
}
}
if scroll {
var x: Int32 = 0
var y: Int32 = 0

if xEnabled {
x = Int32(delta.x * scrollSpeed)
DispatchQueue.main.asyncAfter(deadline: .now() + (triggerDelay / 1000.0)) {
if (stillPressed == false) {
return
}
let scrollSpeedAny = UserDefaults.standard
.object(forKey: "scrollSpeed")
let scrollSpeed = scrollSpeedAny != nil ? scrollSpeedAny as! CGFloat : 20.0

let naturalScrollingAny = UserDefaults.standard.object(forKey: "naturalScrolling")
let naturalScrolling = naturalScrollingAny != nil ? naturalScrollingAny as! Bool : true
let xEnabledAny = UserDefaults.standard.object(forKey: "xEnabled")
let xEnabled = xEnabledAny != nil ? xEnabledAny as! Bool : true
let yEnabledAny = UserDefaults.standard.object(forKey: "yEnabled")
let yEnabled = yEnabledAny != nil ? yEnabledAny as! Bool : true

initialPos = CGEvent(source: nil)!.location
mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { _ in
let pos = CGEvent(source: nil)!.location
if let lastPos = lastPos {
let delta = CGPoint(x: lastPos.x - pos.x, y: lastPos.y - pos.y)
if delta == CGPoint() {
return
}
if yEnabled {
y = Int32(delta.y * scrollSpeed)
var scroll = true
if let lastDelta = lastDelta {
if delta == CGPoint(x:-lastDelta.x,y:-lastDelta.y) {
scroll = false
}
}

if naturalScrolling {
x = -x
y = -y
if scroll {
var x: Int32 = 0
var y: Int32 = 0

if xEnabled {
x = Int32(delta.x * scrollSpeed)
}
if yEnabled {
y = Int32(delta.y * scrollSpeed)
}

if naturalScrolling {
x = -x
y = -y
}

let scrollEvent = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: y, wheel2: x, wheel3: 0)
scrollEvent?.post(tap: CGEventTapLocation.cghidEventTap)
}

let scrollEvent = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: y, wheel2: x, wheel3: 0)
scrollEvent?.post(tap: CGEventTapLocation.cghidEventTap)
lastDelta=delta
}
lastDelta=delta
lastPos = pos
}
lastPos = pos
}
} else {
stillPressed = false
if initialPos != nil && lastPos != nil {
let mouseEvent = CGEvent(mouseEventSource: nil, mouseType: CGEventType.mouseMoved, mouseCursorPosition: initialPos!, mouseButton: CGMouseButton.left)
mouseEvent?.post(tap: CGEventTapLocation.cghidEventTap)
Expand Down
26 changes: 16 additions & 10 deletions Tabula/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Created by Lucas Rott on 21.08.24.
//

import AppKit
import SwiftUI
import LaunchAtLogin

Expand All @@ -17,16 +16,25 @@ struct ContentView: View {
@AppStorage("xEnabled") private var xEnabled = true
@AppStorage("yEnabled") private var yEnabled = true
@AppStorage("needsPermission") private var needsPermission = false
@AppStorage("triggerDelay") private var triggerDelay = 0.0

var body: some View {
VStack {
if needsPermission {
Text("This app needs accessbility access! Please restart the app after you've given the permission.")
Text("This app needs accessbility access!")
} else {
Form {
LabeledContent("General:") {
LaunchAtLogin.Toggle()
}
Slider(value: $triggerDelay, in: 0...2000, step: 100.0) {
Text("Trigger Delay:")
} minimumValueLabel: {
Text("0")
} maximumValueLabel: {
Text("2000")
}
Text("\(triggerDelay, specifier: "%.0f") ms")
Picker("Modifier:", selection: $modifier) {
Text("Option \(Image(systemName: "option"))").tag("option")
Text("Control \(Image(systemName: "control"))").tag("control")
Expand All @@ -41,14 +49,12 @@ struct ContentView: View {
Toggle("X", isOn: $xEnabled)
Toggle("Y", isOn: $yEnabled)
}
VStack {
Slider(value: $scrollSpeed, in: 1...100) {
Text("Scroll Speed:")
} minimumValueLabel: {
Text("1")
} maximumValueLabel: {
Text("100")
}
Slider(value: $scrollSpeed, in: 1...100) {
Text("Scroll Speed:")
} minimumValueLabel: {
Text("1")
} maximumValueLabel: {
Text("100")
}
Text("\(scrollSpeed, specifier: "%.0f")")
}.multilineTextAlignment(.leading)
Expand Down
37 changes: 37 additions & 0 deletions Tabula/PermissionsService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// PermissionsService.swift
// Tabula
//
// Created by Lucas Rott on 07.09.24.
//

import Cocoa
import SwiftUI

// Thanks to https://github.com/othyn/macos-auto-clicker/blob/main/auto-clicker/Services/PermissionsService.swift
// This is a big pain.

final class PermissionsService: ObservableObject {
static var shared: PermissionsService = .init()
private init() {}

func pollAccessibilityPrivileges(onTrusted: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let isTrusted = AXIsProcessTrusted()
UserDefaults.standard.setValue(!isTrusted, forKey: "needsPermission")

if !isTrusted {
self.pollAccessibilityPrivileges(onTrusted: onTrusted)
} else {
onTrusted()
}
}
}

static func acquireAccessibilityPrivileges() {
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
let enabled = AXIsProcessTrustedWithOptions(options)

print(enabled)
}
}

0 comments on commit e8f01eb

Please sign in to comment.