Skip to content

Commit e8f01eb

Browse files
committed
✨ add trigger delay and improve permission for accessibility
1 parent b229519 commit e8f01eb

File tree

4 files changed

+121
-64
lines changed

4 files changed

+121
-64
lines changed

Tabula.xcodeproj/project.pbxproj

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
11C795A32C7611C100CF3F6A /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C795A22C7611C100CF3F6A /* ContentView.swift */; };
1414
11C795A52C7620A100CF3F6A /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11C795A42C7620A100CF3F6A /* AppDelegate.swift */; };
1515
11C795A92C7875CA00CF3F6A /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 11C795A82C7875CA00CF3F6A /* LaunchAtLogin */; };
16+
DBB7ED102C8CC69F00E055E6 /* PermissionsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB7ED0F2C8CC69F00E055E6 /* PermissionsService.swift */; };
1617
/* End PBXBuildFile section */
1718

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

4749
/* Begin PBXFrameworksBuildPhase section */
@@ -97,6 +99,7 @@
9799
11C7957C2C7499F500CF3F6A /* Tabula.entitlements */,
98100
11C795792C7499F500CF3F6A /* Preview Content */,
99101
11C795A22C7611C100CF3F6A /* ContentView.swift */,
102+
DBB7ED0F2C8CC69F00E055E6 /* PermissionsService.swift */,
100103
);
101104
path = Tabula;
102105
sourceTree = "<group>";
@@ -245,6 +248,7 @@
245248
isa = PBXSourcesBuildPhase;
246249
buildActionMask = 2147483647;
247250
files = (
251+
DBB7ED102C8CC69F00E055E6 /* PermissionsService.swift in Sources */,
248252
11C795A52C7620A100CF3F6A /* AppDelegate.swift in Sources */,
249253
11C795722C7499F400CF3F6A /* TabulaApp.swift in Sources */,
250254
11C795A32C7611C100CF3F6A /* ContentView.swift in Sources */,
@@ -412,7 +416,7 @@
412416
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
413417
CODE_SIGN_STYLE = Automatic;
414418
COMBINE_HIDPI_IMAGES = YES;
415-
CURRENT_PROJECT_VERSION = 1.4.0;
419+
CURRENT_PROJECT_VERSION = 1.5.0;
416420
DEVELOPMENT_ASSET_PATHS = "\"Tabula/Preview Content\"";
417421
DEVELOPMENT_TEAM = 3S6Q428WC7;
418422
ENABLE_HARDENED_RUNTIME = YES;
@@ -425,7 +429,7 @@
425429
"@executable_path/../Frameworks",
426430
);
427431
MACOSX_DEPLOYMENT_TARGET = 14.0;
428-
MARKETING_VERSION = 1.4.0;
432+
MARKETING_VERSION = 1.5.0;
429433
PRODUCT_BUNDLE_IDENTIFIER = de.keyruu.Tabula;
430434
PRODUCT_NAME = "$(TARGET_NAME)";
431435
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -442,7 +446,7 @@
442446
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
443447
CODE_SIGN_STYLE = Automatic;
444448
COMBINE_HIDPI_IMAGES = YES;
445-
CURRENT_PROJECT_VERSION = 1.4.0;
449+
CURRENT_PROJECT_VERSION = 1.5.0;
446450
DEVELOPMENT_ASSET_PATHS = "\"Tabula/Preview Content\"";
447451
DEVELOPMENT_TEAM = 3S6Q428WC7;
448452
ENABLE_HARDENED_RUNTIME = YES;
@@ -455,7 +459,7 @@
455459
"@executable_path/../Frameworks",
456460
);
457461
MACOSX_DEPLOYMENT_TARGET = 14.0;
458-
MARKETING_VERSION = 1.4.0;
462+
MARKETING_VERSION = 1.5.0;
459463
PRODUCT_BUNDLE_IDENTIFIER = de.keyruu.Tabula;
460464
PRODUCT_NAME = "$(TARGET_NAME)";
461465
SWIFT_EMIT_LOC_STRINGS = YES;

Tabula/AppDelegate.swift

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,23 @@
88
import Foundation
99
import Cocoa
1010
import AppKit
11+
import SwiftUI
1112

1213
class AppDelegate: NSObject, NSApplicationDelegate {
1314
private var flagsMonitor: Any?
15+
private var permissionsService = PermissionsService.shared
1416

1517
func applicationDidFinishLaunching(_ notification: Notification) {
16-
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
17-
let enabled = AXIsProcessTrustedWithOptions(options)
18-
if !enabled {
19-
CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: 0, wheel2: 0, wheel3: 0)?.post(tap: CGEventTapLocation.cghidEventTap)
20-
UserDefaults.standard.setValue(true, forKey: "needsPermission")
21-
return
22-
} else {
23-
UserDefaults.standard.setValue(false, forKey: "needsPermission")
24-
}
25-
18+
PermissionsService.acquireAccessibilityPrivileges()
19+
permissionsService.pollAccessibilityPrivileges(onTrusted: self.scrollMonitor)
20+
}
21+
22+
func scrollMonitor() {
2623
var mouseMonitor: Any?
2724
var initialPos: CGPoint?
2825
var lastPos: CGPoint?
2926
var lastDelta: CGPoint?
27+
var stillPressed = true
3028
flagsMonitor = NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { event in
3129
if let mouseMonitor = mouseMonitor {
3230
NSEvent.removeMonitor(mouseMonitor)
@@ -36,55 +34,67 @@ class AppDelegate: NSObject, NSApplicationDelegate {
3634
var all = self.allModifiers()
3735
all.remove(self.getModifierFlag())
3836
if event.modifierFlags.contains(self.getModifierFlag()) && event.modifierFlags.intersection(all).isEmpty {
39-
let scrollSpeedAny = UserDefaults.standard
40-
.object(forKey: "scrollSpeed")
41-
let scrollSpeed = scrollSpeedAny != nil ? scrollSpeedAny as! CGFloat : 20.0
37+
stillPressed = true
4238

43-
let naturalScrollingAny = UserDefaults.standard.object(forKey: "naturalScrolling")
44-
let naturalScrolling = naturalScrollingAny != nil ? naturalScrollingAny as! Bool : true
45-
let xEnabledAny = UserDefaults.standard.object(forKey: "xEnabled")
46-
let xEnabled = xEnabledAny != nil ? xEnabledAny as! Bool : true
47-
let yEnabledAny = UserDefaults.standard.object(forKey: "yEnabled")
48-
let yEnabled = yEnabledAny != nil ? yEnabledAny as! Bool : true
39+
let triggerDelayAny = UserDefaults.standard
40+
.object(forKey: "triggerDelay")
41+
let triggerDelay = triggerDelayAny != nil ? triggerDelayAny as! Double : 0.0
4942

50-
initialPos = CGEvent(source: nil)!.location
51-
mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { _ in
52-
let pos = CGEvent(source: nil)!.location
53-
if let lastPos = lastPos {
54-
let delta = CGPoint(x: lastPos.x - pos.x, y: lastPos.y - pos.y)
55-
if delta == CGPoint() {
56-
return
57-
}
58-
var scroll = true
59-
if let lastDelta = lastDelta {
60-
if delta == CGPoint(x:-lastDelta.x,y:-lastDelta.y) {
61-
scroll = false
62-
}
63-
}
64-
if scroll {
65-
var x: Int32 = 0
66-
var y: Int32 = 0
67-
68-
if xEnabled {
69-
x = Int32(delta.x * scrollSpeed)
43+
DispatchQueue.main.asyncAfter(deadline: .now() + (triggerDelay / 1000.0)) {
44+
if (stillPressed == false) {
45+
return
46+
}
47+
let scrollSpeedAny = UserDefaults.standard
48+
.object(forKey: "scrollSpeed")
49+
let scrollSpeed = scrollSpeedAny != nil ? scrollSpeedAny as! CGFloat : 20.0
50+
51+
let naturalScrollingAny = UserDefaults.standard.object(forKey: "naturalScrolling")
52+
let naturalScrolling = naturalScrollingAny != nil ? naturalScrollingAny as! Bool : true
53+
let xEnabledAny = UserDefaults.standard.object(forKey: "xEnabled")
54+
let xEnabled = xEnabledAny != nil ? xEnabledAny as! Bool : true
55+
let yEnabledAny = UserDefaults.standard.object(forKey: "yEnabled")
56+
let yEnabled = yEnabledAny != nil ? yEnabledAny as! Bool : true
57+
58+
initialPos = CGEvent(source: nil)!.location
59+
mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { _ in
60+
let pos = CGEvent(source: nil)!.location
61+
if let lastPos = lastPos {
62+
let delta = CGPoint(x: lastPos.x - pos.x, y: lastPos.y - pos.y)
63+
if delta == CGPoint() {
64+
return
7065
}
71-
if yEnabled {
72-
y = Int32(delta.y * scrollSpeed)
66+
var scroll = true
67+
if let lastDelta = lastDelta {
68+
if delta == CGPoint(x:-lastDelta.x,y:-lastDelta.y) {
69+
scroll = false
70+
}
7371
}
74-
75-
if naturalScrolling {
76-
x = -x
77-
y = -y
72+
if scroll {
73+
var x: Int32 = 0
74+
var y: Int32 = 0
75+
76+
if xEnabled {
77+
x = Int32(delta.x * scrollSpeed)
78+
}
79+
if yEnabled {
80+
y = Int32(delta.y * scrollSpeed)
81+
}
82+
83+
if naturalScrolling {
84+
x = -x
85+
y = -y
86+
}
87+
88+
let scrollEvent = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: y, wheel2: x, wheel3: 0)
89+
scrollEvent?.post(tap: CGEventTapLocation.cghidEventTap)
7890
}
79-
80-
let scrollEvent = CGEvent(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: y, wheel2: x, wheel3: 0)
81-
scrollEvent?.post(tap: CGEventTapLocation.cghidEventTap)
91+
lastDelta=delta
8292
}
83-
lastDelta=delta
93+
lastPos = pos
8494
}
85-
lastPos = pos
8695
}
8796
} else {
97+
stillPressed = false
8898
if initialPos != nil && lastPos != nil {
8999
let mouseEvent = CGEvent(mouseEventSource: nil, mouseType: CGEventType.mouseMoved, mouseCursorPosition: initialPos!, mouseButton: CGMouseButton.left)
90100
mouseEvent?.post(tap: CGEventTapLocation.cghidEventTap)

Tabula/ContentView.swift

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
// Created by Lucas Rott on 21.08.24.
66
//
77

8-
import AppKit
98
import SwiftUI
109
import LaunchAtLogin
1110

@@ -17,16 +16,25 @@ struct ContentView: View {
1716
@AppStorage("xEnabled") private var xEnabled = true
1817
@AppStorage("yEnabled") private var yEnabled = true
1918
@AppStorage("needsPermission") private var needsPermission = false
19+
@AppStorage("triggerDelay") private var triggerDelay = 0.0
2020

2121
var body: some View {
2222
VStack {
2323
if needsPermission {
24-
Text("This app needs accessbility access! Please restart the app after you've given the permission.")
24+
Text("This app needs accessbility access!")
2525
} else {
2626
Form {
2727
LabeledContent("General:") {
2828
LaunchAtLogin.Toggle()
2929
}
30+
Slider(value: $triggerDelay, in: 0...2000, step: 100.0) {
31+
Text("Trigger Delay:")
32+
} minimumValueLabel: {
33+
Text("0")
34+
} maximumValueLabel: {
35+
Text("2000")
36+
}
37+
Text("\(triggerDelay, specifier: "%.0f") ms")
3038
Picker("Modifier:", selection: $modifier) {
3139
Text("Option \(Image(systemName: "option"))").tag("option")
3240
Text("Control \(Image(systemName: "control"))").tag("control")
@@ -41,14 +49,12 @@ struct ContentView: View {
4149
Toggle("X", isOn: $xEnabled)
4250
Toggle("Y", isOn: $yEnabled)
4351
}
44-
VStack {
45-
Slider(value: $scrollSpeed, in: 1...100) {
46-
Text("Scroll Speed:")
47-
} minimumValueLabel: {
48-
Text("1")
49-
} maximumValueLabel: {
50-
Text("100")
51-
}
52+
Slider(value: $scrollSpeed, in: 1...100) {
53+
Text("Scroll Speed:")
54+
} minimumValueLabel: {
55+
Text("1")
56+
} maximumValueLabel: {
57+
Text("100")
5258
}
5359
Text("\(scrollSpeed, specifier: "%.0f")")
5460
}.multilineTextAlignment(.leading)

Tabula/PermissionsService.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// PermissionsService.swift
3+
// Tabula
4+
//
5+
// Created by Lucas Rott on 07.09.24.
6+
//
7+
8+
import Cocoa
9+
import SwiftUI
10+
11+
// Thanks to https://github.com/othyn/macos-auto-clicker/blob/main/auto-clicker/Services/PermissionsService.swift
12+
// This is a big pain.
13+
14+
final class PermissionsService: ObservableObject {
15+
static var shared: PermissionsService = .init()
16+
private init() {}
17+
18+
func pollAccessibilityPrivileges(onTrusted: @escaping () -> Void) {
19+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
20+
let isTrusted = AXIsProcessTrusted()
21+
UserDefaults.standard.setValue(!isTrusted, forKey: "needsPermission")
22+
23+
if !isTrusted {
24+
self.pollAccessibilityPrivileges(onTrusted: onTrusted)
25+
} else {
26+
onTrusted()
27+
}
28+
}
29+
}
30+
31+
static func acquireAccessibilityPrivileges() {
32+
let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true]
33+
let enabled = AXIsProcessTrustedWithOptions(options)
34+
35+
print(enabled)
36+
}
37+
}

0 commit comments

Comments
 (0)