Skip to content

Commit 71ab290

Browse files
committed
v2.0.1
1 parent 999d8ab commit 71ab290

File tree

8 files changed

+202
-28
lines changed

8 files changed

+202
-28
lines changed
0 Bytes
Binary file not shown.

DockAnchor.app/Contents/Info.plist

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,13 @@
2323
<key>CFBundlePackageType</key>
2424
<string>APPL</string>
2525
<key>CFBundleShortVersionString</key>
26-
<string>2.0</string>
26+
<string>2.0.1</string>
2727
<key>CFBundleSupportedPlatforms</key>
2828
<array>
2929
<string>MacOSX</string>
3030
</array>
3131
<key>CFBundleVersion</key>
32-
<string>2.0</string>
32+
<string>2.0.1</string>
3333
<key>DTCompiler</key>
3434
<string>com.apple.compilers.llvm.clang.1_0</string>
3535
<key>DTPlatformBuild</key>
31.9 KB
Binary file not shown.

DockAnchor.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@
407407
CODE_SIGN_STYLE = Automatic;
408408
COMBINE_HIDPI_IMAGES = YES;
409409
CONFIGURATION_BUILD_DIR = build;
410-
CURRENT_PROJECT_VERSION = 2.0;
410+
CURRENT_PROJECT_VERSION = 2.0.1;
411411
DEAD_CODE_STRIPPING = YES;
412412
DEVELOPMENT_TEAM = 8772WA9289;
413413
ENABLE_APP_SANDBOX = YES;
@@ -422,7 +422,7 @@
422422
"$(inherited)",
423423
"@executable_path/../Frameworks",
424424
);
425-
MARKETING_VERSION = 2.0;
425+
MARKETING_VERSION = 2.0.1;
426426
PRODUCT_BUNDLE_IDENTIFIER = bwyatt.DockAnchor;
427427
PRODUCT_NAME = "$(TARGET_NAME)";
428428
REGISTER_APP_GROUPS = YES;
@@ -443,7 +443,7 @@
443443
CODE_SIGN_STYLE = Automatic;
444444
COMBINE_HIDPI_IMAGES = YES;
445445
CONFIGURATION_BUILD_DIR = build;
446-
CURRENT_PROJECT_VERSION = 2.0;
446+
CURRENT_PROJECT_VERSION = 2.0.1;
447447
DEAD_CODE_STRIPPING = YES;
448448
DEVELOPMENT_TEAM = 8772WA9289;
449449
ENABLE_APP_SANDBOX = YES;
@@ -458,7 +458,7 @@
458458
"$(inherited)",
459459
"@executable_path/../Frameworks",
460460
);
461-
MARKETING_VERSION = 2.0;
461+
MARKETING_VERSION = 2.0.1;
462462
PRODUCT_BUNDLE_IDENTIFIER = bwyatt.DockAnchor;
463463
PRODUCT_NAME = "$(TARGET_NAME)";
464464
REGISTER_APP_GROUPS = YES;

DockAnchor.zip

11.2 KB
Binary file not shown.

DockAnchor/ContentView.swift

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ struct ContentView: View {
344344
@State private var showingNewProfile = false
345345
@State private var editingProfile: DockProfile?
346346
@State private var newProfileName = ""
347+
@State private var showingPermissionHelp = false
347348

348349
private var statusColor: Color {
349350
if dockMonitor.statusMessage.contains("Blocked") {
@@ -393,7 +394,21 @@ struct ContentView: View {
393394
Spacer()
394395
}
395396

396-
if !dockMonitor.isActive {
397+
if dockMonitor.needsPermissionReset || dockMonitor.statusMessage.lowercased().contains("permission") {
398+
HStack {
399+
Image(systemName: "exclamationmark.triangle.fill")
400+
.foregroundColor(.orange)
401+
Text("Accessibility permission required")
402+
.font(.caption)
403+
.foregroundColor(.secondary)
404+
Spacer()
405+
Button("Help") {
406+
showingPermissionHelp = true
407+
}
408+
.font(.caption)
409+
.buttonStyle(.bordered)
410+
}
411+
} else if !dockMonitor.isActive {
397412
HStack {
398413
Image(systemName: "exclamationmark.triangle")
399414
.foregroundColor(.orange)
@@ -533,9 +548,21 @@ struct ContentView: View {
533548
.environmentObject(appSettings)
534549
.preferredColorScheme(appSettings.appTheme.colorScheme)
535550
}
551+
.sheet(isPresented: $showingPermissionHelp) {
552+
PermissionHelpSheet(dockMonitor: dockMonitor)
553+
.preferredColorScheme(appSettings.appTheme.colorScheme)
554+
}
536555
.onAppear {
537-
// Request permissions on startup
538-
_ = dockMonitor.requestAccessibilityPermissions()
556+
// Check permissions on startup and show help if not granted
557+
let hasPermissions = dockMonitor.requestAccessibilityPermissions()
558+
if !hasPermissions {
559+
// Small delay to let the UI appear first
560+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
561+
showingPermissionHelp = true
562+
}
563+
// Don't perform any operations that require accessibility permissions
564+
return
565+
}
539566
// Update available displays
540567
dockMonitor.updateAvailableDisplays()
541568
// Set the anchor display from settings (using UUID for stable identification)
@@ -700,6 +727,79 @@ struct SettingsView: View {
700727
}
701728
}
702729

730+
struct PermissionHelpSheet: View {
731+
@ObservedObject var dockMonitor: DockMonitor
732+
@Environment(\.dismiss) var dismiss
733+
734+
var body: some View {
735+
VStack(spacing: 20) {
736+
HStack {
737+
Text("Accessibility Permission")
738+
.font(.title2)
739+
.fontWeight(.bold)
740+
Spacer()
741+
Button("Done") {
742+
dismiss()
743+
}
744+
.buttonStyle(.bordered)
745+
}
746+
.padding(.horizontal)
747+
.padding(.top, 16)
748+
749+
VStack(alignment: .leading, spacing: 16) {
750+
Text("DockAnchor requires Accessibility permission to monitor mouse movement and keep your dock anchored.")
751+
.font(.body)
752+
753+
Divider()
754+
755+
Text("How to enable:")
756+
.font(.headline)
757+
758+
VStack(alignment: .leading, spacing: 8) {
759+
Label("Click \"Open Accessibility Settings\" below", systemImage: "1.circle.fill")
760+
Label("Find DockAnchor in the list", systemImage: "2.circle.fill")
761+
Label("Toggle it ON", systemImage: "3.circle.fill")
762+
}
763+
.font(.body)
764+
765+
Divider()
766+
767+
Text("If permission doesn't work after an update:")
768+
.font(.headline)
769+
770+
VStack(alignment: .leading, spacing: 8) {
771+
Label("Remove DockAnchor from the list (- button)", systemImage: "1.circle")
772+
Label("Re-add it (+ button)", systemImage: "2.circle")
773+
Label("Toggle it ON", systemImage: "3.circle")
774+
}
775+
.font(.body)
776+
.foregroundColor(.secondary)
777+
778+
Spacer()
779+
780+
Button(action: {
781+
dockMonitor.openAccessibilityPreferences()
782+
}) {
783+
HStack {
784+
Image(systemName: "gearshape.fill")
785+
Text("Open Accessibility Settings")
786+
}
787+
.frame(maxWidth: .infinity)
788+
.padding(.vertical, 10)
789+
.background(Color.blue)
790+
.foregroundColor(.white)
791+
.cornerRadius(8)
792+
}
793+
.buttonStyle(.plain)
794+
}
795+
.padding(.horizontal)
796+
.padding(.bottom, 16)
797+
}
798+
.frame(width: 380, height: 420)
799+
.background(.background)
800+
}
801+
}
802+
703803
#Preview {
704804
ContentView()
705805
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)

DockAnchor/DockAnchorApp.swift

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,23 +115,27 @@ class ApplicationDelegate: NSObject, NSApplicationDelegate, ObservableObject {
115115
// This is the most critical piece - ensures menu bar icon appears
116116
menuBarManager.setup(appSettings: appSettings, dockMonitor: dockMonitor, updateChecker: updateChecker)
117117

118-
// Set the anchor display from settings (using UUID for stable identification)
119-
dockMonitor.changeAnchorDisplay(toUUID: appSettings.selectedDisplayUUID)
120-
121118
// Set initial activation policy
122119
updateActivationPolicy()
123120

124-
// Auto-start monitoring if enabled (with a small delay for system stability)
125-
if appSettings.runInBackground {
126-
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
127-
self?.dockMonitor.startMonitoring()
121+
// Only perform accessibility-dependent operations if permissions are granted
122+
let hasPermissions = dockMonitor.requestAccessibilityPermissions()
123+
if hasPermissions {
124+
// Set the anchor display from settings (using UUID for stable identification)
125+
dockMonitor.changeAnchorDisplay(toUUID: appSettings.selectedDisplayUUID)
126+
127+
// Auto-start monitoring if enabled (with a small delay for system stability)
128+
if appSettings.runInBackground {
129+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
130+
self?.dockMonitor.startMonitoring()
131+
}
128132
}
129-
}
130133

131-
// Auto-relocate dock to anchored display on launch if enabled
132-
if appSettings.autoRelocateDock {
133-
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
134-
self?.dockMonitor.relocateDockToAnchoredDisplay()
134+
// Auto-relocate dock to anchored display on launch if enabled
135+
if appSettings.autoRelocateDock {
136+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
137+
self?.dockMonitor.relocateDockToAnchoredDisplay()
138+
}
135139
}
136140
}
137141

DockAnchor/DockMonitor.swift

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,15 @@ class DockMonitor: NSObject, ObservableObject {
9090
@Published var anchoredDisplay: String = "Primary"
9191
@Published var statusMessage = "Dock Anchor Ready"
9292
@Published var availableDisplays: [DisplayInfo] = []
93+
@Published var needsPermissionReset = false
9394

9495
private var eventTap: CFMachPort?
9596
private var runLoopSource: CFRunLoopSource?
9697
private var isMonitoring = false
9798
private var anchorDisplayUUID: String = "" // Hardware UUID for stable anchor tracking
9899
private var dockPosition: DockPosition = .bottom
99100
private var cancellables = Set<AnyCancellable>()
101+
private var permissionCheckTimer: Timer?
100102

101103
/// Gets the current anchor display ID (derived from UUID)
102104
private var anchorDisplayID: CGDirectDisplayID {
@@ -373,18 +375,80 @@ class DockMonitor: NSObject, ObservableObject {
373375
}
374376

375377
func requestAccessibilityPermissions() -> Bool {
376-
let options = [kAXTrustedCheckOptionPrompt.takeRetainedValue(): true]
377-
let trusted = AXIsProcessTrustedWithOptions(options as CFDictionary)
378-
378+
// Check if already trusted (without prompting)
379+
let trusted = AXIsProcessTrusted()
380+
379381
if !trusted {
380382
DispatchQueue.main.async { [weak self] in
381383
self?.statusMessage = "Accessibility permissions required"
382384
}
385+
} else {
386+
DispatchQueue.main.async { [weak self] in
387+
self?.needsPermissionReset = false
388+
}
383389
}
384-
390+
385391
return trusted
386392
}
387-
393+
394+
/// Prompts for accessibility permissions by opening System Preferences
395+
/// Note: On modern macOS, the system dialog often just opens System Preferences
396+
/// without actually adding the app - users must manually add it with the + button
397+
func promptForAccessibilityPermissions() {
398+
// Use the string key directly to avoid takeRetainedValue() issues
399+
let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary
400+
AXIsProcessTrustedWithOptions(options)
401+
}
402+
403+
/// Checks accessibility permissions without prompting
404+
private func checkAccessibilityPermissions() -> Bool {
405+
return AXIsProcessTrusted()
406+
}
407+
408+
/// Opens System Preferences to the Accessibility pane
409+
func openAccessibilityPreferences() {
410+
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
411+
NSWorkspace.shared.open(url)
412+
}
413+
}
414+
415+
/// Starts the timer that periodically checks if permissions are still valid
416+
private func startPermissionMonitoring() {
417+
// Check every 2 seconds for permission changes
418+
permissionCheckTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
419+
self?.verifyPermissionsAndTapValidity()
420+
}
421+
}
422+
423+
/// Stops the permission monitoring timer
424+
private func stopPermissionMonitoring() {
425+
permissionCheckTimer?.invalidate()
426+
permissionCheckTimer = nil
427+
}
428+
429+
/// Verifies that accessibility permissions are still granted and event tap is valid
430+
private func verifyPermissionsAndTapValidity() {
431+
guard isMonitoring else { return }
432+
433+
// Check if accessibility permissions are still granted
434+
if !checkAccessibilityPermissions() {
435+
DispatchQueue.main.async { [weak self] in
436+
self?.statusMessage = "Accessibility permissions revoked - stopping monitoring"
437+
self?.stopMonitoring()
438+
}
439+
return
440+
}
441+
442+
// Check if the event tap is still valid
443+
if let tap = eventTap, !CFMachPortIsValid(tap) {
444+
DispatchQueue.main.async { [weak self] in
445+
self?.statusMessage = "Event tap invalidated - stopping monitoring"
446+
self?.stopMonitoring()
447+
}
448+
return
449+
}
450+
}
451+
388452
func startMonitoring() {
389453
guard requestAccessibilityPermissions() else {
390454
statusMessage = "Please grant accessibility permissions in System Preferences"
@@ -426,7 +490,11 @@ class DockMonitor: NSObject, ObservableObject {
426490
)
427491

428492
guard let eventTap = eventTap else {
429-
statusMessage = "Failed to create event tap"
493+
// Event tap creation failed even though permissions appeared granted.
494+
// This usually means the permission entry is stale (app was updated).
495+
// The user needs to remove and re-add the app in Accessibility settings.
496+
needsPermissionReset = true
497+
statusMessage = "Permission needs reset - remove and re-add app in Accessibility settings"
430498
return
431499
}
432500

@@ -435,13 +503,15 @@ class DockMonitor: NSObject, ObservableObject {
435503
CGEvent.tapEnable(tap: eventTap, enable: true)
436504

437505
isMonitoring = true
506+
startPermissionMonitoring()
438507
DispatchQueue.main.async { [weak self] in
439508
self?.isActive = true
440509
self?.statusMessage = "Dock Anchor Active - Monitoring mouse movement"
441510
}
442511
}
443-
512+
444513
func stopMonitoring() {
514+
stopPermissionMonitoring()
445515
guard isMonitoring else { return }
446516

447517
isMonitoring = false

0 commit comments

Comments
 (0)