Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
src/renderer/styles/dist
test/results
137 changes: 104 additions & 33 deletions src/main/accessibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,87 +10,131 @@ let accessibilityCheckPromise = null
* Returns true on non-macOS platforms
*/
const checkAccessibilityPermissions = () => {

if ( !is.macos ) {

return true

}

// Use systemPreferences to check accessibility permissions
const { systemPreferences } = require( 'electron' )

try {

// This will return true if accessibility is enabled
return systemPreferences.isTrustedAccessibilityClient( false )

} catch ( error ) {

log.warn( 'Error checking accessibility permissions:', error )

return false

}

}

/**
* Request accessibility permissions with user dialog
*/
const requestAccessibilityPermissions = async () => {

if ( !is.macos ) {

return true

}

// Check if we're already in the process of requesting permissions
if ( accessibilityCheckPromise ) {

return accessibilityCheckPromise

}

const { systemPreferences } = require( 'electron' )

try {

// First check if already granted
if ( systemPreferences.isTrustedAccessibilityClient( false ) ) {

return true

}

// Create a promise to handle the permission request flow
accessibilityCheckPromise = new Promise( async resolve => {
// Show explanation dialog first
const result = await dialog.showMessageBox( {
type: 'info',
title: 'Accessibility Permission Required',
message: 'CrossOver needs accessibility permissions to capture mouse and keyboard events.',
detail: 'This allows features like:\n• Mouse follow mode\n• Hide crosshair on mouse/key press\n• Resize crosshair when aiming\n• Tilt crosshair controls\n\nClick "Open System Preferences" to grant permissions, then restart CrossOver.',
buttons: [ 'Open System Preferences', 'Skip for Now', 'Quit' ],
defaultId: 0,
cancelId: 1,
} )
accessibilityCheckPromise = new Promise( ( resolve, reject ) => {

// Wrap the async flow in an IIFE so we can use await while
// keeping the Promise executor itself synchronous (required
// by eslint's `no-async-promise-executor` rule).
( async () => {

// Show explanation dialog first
const result = await dialog.showMessageBox( {
type: 'info',
title: 'Accessibility Permission Required',
message: 'CrossOver needs accessibility permissions to capture mouse and keyboard events.',
detail: 'This allows features like:\n• Mouse follow mode\n• Hide crosshair on mouse/key press\n• Resize crosshair when aiming\n• Tilt crosshair controls\n\nClick "Open System Preferences" to grant permissions, then restart CrossOver.',
buttons: [
'Open System Preferences', 'Skip for Now', 'Quit',
],
defaultId: 0,
cancelId: 1,
} )

switch ( result.response ) {

switch ( result.response ) {
case 0: // Open System Preferences
// Try to prompt for accessibility (this will open System Preferences)
systemPreferences.isTrustedAccessibilityClient( true )

// Also open System Preferences directly to the right pane
try {

await shell.openExternal( 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility' )

} catch ( error ) {

log.warn( 'Could not open System Preferences directly:', error )

}

// Show follow-up dialog
setTimeout( async () => {
const followUpResult = await dialog.showMessageBox( {
type: 'question',
title: 'Restart Required',
message: 'After granting accessibility permissions in System Preferences, CrossOver needs to restart.',
detail: 'Have you granted accessibility permissions to CrossOver?',
buttons: [ 'Restart Now', 'I\'ll Restart Later' ],
defaultId: 0,
} )

if ( followUpResult.response === 0 ) {
app.relaunch()
app.quit()
} else {
// Save a flag to check permissions on next startup
preferences.value( 'hidden.needsAccessibilityCheck', true )
resolve( false )

try {

const followUpResult = await dialog.showMessageBox( {
type: 'question',
title: 'Restart Required',
message: 'After granting accessibility permissions in System Preferences, CrossOver needs to restart.',
detail: 'Have you granted accessibility permissions to CrossOver?',
buttons: [ 'Restart Now', 'I\'ll Restart Later' ],
defaultId: 0,
} )

if ( followUpResult.response === 0 ) {

app.relaunch()
app.quit()

} else {

// Save a flag to check permissions on next startup
preferences.value( 'hidden.needsAccessibilityCheck', true )
resolve( false )

}

} catch ( error ) {

reject( error )

}

}, 2000 )
break

Expand All @@ -103,73 +147,100 @@ const requestAccessibilityPermissions = async () => {
app.quit()
resolve( false )
break
}

default:
// Unhandled response, assume permissions not granted yet
resolve( false )

}

} )().catch( reject )

} )

const result = await accessibilityCheckPromise
accessibilityCheckPromise = null

return result

} catch ( error ) {

log.error( 'Error requesting accessibility permissions:', error )
accessibilityCheckPromise = null

return false

}

}

/**
* Show a notification that accessibility features are disabled
*/
const showAccessibilityDisabledNotification = () => {

const notification = require( './notification' )
notification( {
title: 'Accessibility Features Disabled',
body: 'Some CrossOver features require accessibility permissions. Enable them in System Preferences > Security & Privacy > Accessibility.',
} )

}

/**
* Check if we should skip accessibility checks (user chose to skip)
*/
const shouldSkipAccessibilityCheck = () => {
return preferences.value( 'hidden.accessibilitySkipped' ) === true
}
const shouldSkipAccessibilityCheck = () => preferences.value( 'hidden.accessibilitySkipped' ) === true

/**
* Reset accessibility preferences (for settings reset)
*/
const resetAccessibilityPreferences = () => {

preferences.value( 'hidden.accessibilitySkipped', false )
preferences.value( 'hidden.needsAccessibilityCheck', false )

}

/**
* Initialize accessibility check on app startup
*/
const initializeAccessibilityCheck = async () => {

if ( !is.macos ) {

return true

}

// Check if we need to recheck permissions after restart
if ( preferences.value( 'hidden.needsAccessibilityCheck' ) ) {

preferences.value( 'hidden.needsAccessibilityCheck', false )

if ( checkAccessibilityPermissions() ) {

const notification = require( './notification' )
notification( {
title: 'Accessibility Enabled',
body: 'CrossOver can now use advanced input features!',
} )

return true

}

}

// If user hasn't explicitly skipped, and permissions aren't granted, ask
if ( !shouldSkipAccessibilityCheck() && !checkAccessibilityPermissions() ) {
return await requestAccessibilityPermissions()

return requestAccessibilityPermissions()

}

return checkAccessibilityPermissions()

}

const accessibility = {
Expand Down
Loading