Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FB8947755: Add API to open specific System Preferences panes #184

Open
sindresorhus opened this issue Dec 21, 2020 · 0 comments
Open

FB8947755: Add API to open specific System Preferences panes #184

sindresorhus opened this issue Dec 21, 2020 · 0 comments

Comments

@sindresorhus
Copy link
Member

  • Date: 2020-12-21
  • Resolution: Open
  • Area: AppKit
  • OS: macOS 11.1
  • Type: Suggestion

Description

Many apps need the ability to open a specific pane in System Preferences. The most common use is to direct users to manually enable some kind of privacy permission or to toggle an app extension. I also use it to help the user change some system settings, like in “Language & Region”. Some panes support a URL scheme x-apple.systempreferences:, while for others we have to open /System/Library/PreferencePanes/\(name).prefPane. And to open sub-panes of the Extensions system preferences, our only choice is to use an API which is now deprecated (FB8910479). This results in a large amount of boilerplate. It would be nice if the system provided a unified API to open System Preferences panes and sub-panes. This would save developers a lot of time and headache.

I imagine the API would be something like this:

NSWorkspace.shared.openSystemPreferencesPane(.extensions) // Pane

NSWorkspace.shared.openSystemPreferencesPane(.extensionsActions) // Sub-pane

NSWorkspace.shared.openSystemPreferencesPane(.privacyCalendars) // Sub-pane

NSWorkspace.shared.openSystemPreferencesPane(.languageAndRegion) // Pane

Alternatively solution: Support the URL scheme for more panes and sub-panes. The most critical missing one is to open sub-panes for the Extensions pane in System Preferences.


And for reference, this is what I’m currently using in my apps:

extension NSWorkspace {
	enum SystemPreferencesType {
		case extensions
		case extensionsActions
		case extensionsFinder
		case extensionsPhotosEditing
		case extensionsPhotosProjects
		case extensionsQuickLook
		case extensionsShare
		case extensionsToday
		case extensionsXcodeSourceEditor
		case privacyAccessibility
		case privacyAutomation
		case privacyCalendars
		case privacyMicrophone
		case iCloud
		case network
		case languageAndRegion
		case dateAndTime
	}

	private func openPreferencePane(withIdentifier identifier: String) {
		let prefix = "x-apple.systempreferences:"
		open(URL(string: "\(prefix)\(identifier)")!)
	}

	private func urlForPreferencePane(named name: String) -> URL {
		URL(fileURLWithPath: "/System/Library/PreferencePanes/\(name).prefPane", isDirectory: true)
	}

	private func openPreferencePane(named name: String) {
		open(urlForPreferencePane(named: name))
	}

	private func openExtensionsSubPane(withIdentifier identifier: String) {
		let parameters = [
			"action": "revealExtensionPoint",
			"protocol": identifier // Almost all identifiers: https://developer.apple.com/documentation/bundleresources/information_property_list/nsextension/nsextensionpointidentifier
		]
		let data = try? PropertyListSerialization.data(fromPropertyList: parameters, format: .xml, options: 0)
		let descriptor = NSAppleEventDescriptor(descriptorType: FourCharCode(string: "ptru"), data: data)

		// TODO: The 10.15 API doesn't execute the AppleEvent as it expects the app to be already running (FB8910479).
//		if #available(macOS 10.15, *) {
//			let configuration = NSWorkspace.OpenConfiguration()
//			configuration.addsToRecentItems = false
//			configuration.appleEvent = descriptor
//
//			NSWorkspace.shared.open(urlForPreferencePane(named: "Extensions"), configuration: configuration) { _, error in
//				if let error = error {
//					error.presentAsModal()
//					return
//				}
//			}
//		} else {
			NSWorkspace.shared.open([urlForPreferencePane(named: "Extensions")], withAppBundleIdentifier: nil, additionalEventParamDescriptor: descriptor, launchIdentifiers: nil)
		//}
	}

	func openSystemPreferences(at type: SystemPreferencesType) {
		switch type {
		case .extensions:
			openPreferencePane(named: "Extensions")
		case .extensionsActions:
			openExtensionsSubPane(withIdentifier: "com.apple.ui-services")
		case .extensionsFinder:
			openExtensionsSubPane(withIdentifier: "com.apple.FinderSync")
		case .extensionsPhotosEditing:
			openExtensionsSubPane(withIdentifier: "com.apple.photo-editing")
		case .extensionsPhotosProjects:
			openExtensionsSubPane(withIdentifier: "com.apple.photo-project")
		case .extensionsQuickLook:
			openExtensionsSubPane(withIdentifier: "com.apple.quicklook.preview")
		case .extensionsShare:
			openExtensionsSubPane(withIdentifier: "com.apple.share-services")
		case .extensionsToday:
			openExtensionsSubPane(withIdentifier: "com.apple.widget-extension")
		case .extensionsXcodeSourceEditor:
			openExtensionsSubPane(withIdentifier: "com.apple.dt.Xcode.extension.source-editor")
		case .privacyAccessibility:
			openPreferencePane(withIdentifier: "com.apple.preference.security?Privacy_Accessibility")
		case .privacyAutomation:
			openPreferencePane(withIdentifier: "com.apple.preference.security?Privacy_Automation")
		case .privacyCalendars:
			openPreferencePane(withIdentifier: "com.apple.preference.security?Privacy_Calendars")
		case .privacyMicrophone:
			openPreferencePane(withIdentifier: "com.apple.preference.security?Privacy_Microphone")
		case .iCloud:
			openPreferencePane(withIdentifier: "com.apple.preferences.icloud")
		case .network:
			openPreferencePane(withIdentifier: "com.apple.preference.network")
		case .languageAndRegion:
			openPreferencePane(named: "Localization")
		case .dateAndTime:
			openPreferencePane(named: "DateAndTime")
		}
	}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant