Fix IME activation issues on macOS 12

We now let the Installer to call the TextInputSources API. Since macOS
12, users are prompted to allow enabling of third-party IMEs in
Preferences.app the momemnt TISRegisterInputSource or
TISEnableInputSource is called. By moving the activation to the
Installer, a user will clearly see that it's the Installer that wants to
enable the IME.

In addition, we had to make necessary changes so that on macOS 12 and
later, the Installer always enable the default input source. This is due
to the observation that the kTISPropertyInputSourceIsEnabled becomes
unreliable on macOS 12--it may be true even if the user has removed the
input mode from their active input mode list in Preferences.app.
This commit is contained in:
Lukhnos Liu 2021-11-22 21:17:35 -08:00
parent b85029dec1
commit c1bea8c382
4 changed files with 93 additions and 13 deletions

View File

@ -33,6 +33,7 @@
6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; }; 6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; };
6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; }; 6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; };
6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */; }; 6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A38BC2715FC158A00A8A51F /* InputMethodKit.framework */; };
6AB3620D274CA50700AC7547 /* OVInputSourceHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4ECA15FC0D6400ABF4B3 /* OVInputSourceHelper.m */; };
6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; }; 6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A0D4EA615FC0D2D00ABF4B3 /* Cocoa.framework */; };
6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; }; 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; };
6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; }; 6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; };
@ -660,6 +661,7 @@
files = ( files = (
6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */, 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */,
6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */, 6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */,
6AB3620D274CA50700AC7547 /* OVInputSourceHelper.m in Sources */,
6ACA41FF15FC1D9000935EF6 /* main.m in Sources */, 6ACA41FF15FC1D9000935EF6 /* main.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -1038,6 +1040,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = ../;
INFOPLIST_FILE = "Source/Installer/Installer-Info.plist"; INFOPLIST_FILE = "Source/Installer/Installer-Info.plist";
MACOSX_DEPLOYMENT_TARGET = 10.10; MACOSX_DEPLOYMENT_TARGET = 10.10;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
@ -1070,6 +1073,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
HEADER_SEARCH_PATHS = ../;
INFOPLIST_FILE = "Source/Installer/Installer-Info.plist"; INFOPLIST_FILE = "Source/Installer/Installer-Info.plist";
MACOSX_DEPLOYMENT_TARGET = 10.10; MACOSX_DEPLOYMENT_TARGET = 10.10;
PRODUCT_BUNDLE_IDENTIFIER = "org.openvanilla.McBopomofo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_BUNDLE_IDENTIFIER = "org.openvanilla.McBopomofo.${PRODUCT_NAME:rfc1034identifier}";

View File

@ -27,6 +27,7 @@
#import "AppDelegate.h" #import "AppDelegate.h"
#import <sys/mount.h> #import <sys/mount.h>
#import "OVInputSourceHelper.h"
static NSString *const kTargetBin = @"McBopomofo"; static NSString *const kTargetBin = @"McBopomofo";
static NSString *const kTargetType = @"app"; static NSString *const kTargetType = @"app";
@ -55,7 +56,6 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) {
@synthesize progressSheet = _progressSheet; @synthesize progressSheet = _progressSheet;
@synthesize progressIndicator = _progressIndicator; @synthesize progressIndicator = _progressIndicator;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification - (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{ {
_installingVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:(id)kCFBundleVersionKey]; _installingVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:(id)kCFBundleVersionKey];
@ -130,9 +130,9 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) {
// Schedule the install action in runloop so that the sheet gets a change to dismiss itself. // Schedule the install action in runloop so that the sheet gets a change to dismiss itself.
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
if (returnCode == NSModalResponseContinue) { if (returnCode == NSModalResponseContinue) {
[self installInputMethodWithWarning:NO]; [self installInputMethodWithPreviousExists:YES previousVersionNotFullyDeactivatedWarning:NO];
} else { } else {
[self installInputMethodWithWarning:YES]; [self installInputMethodWithPreviousExists:YES previousVersionNotFullyDeactivatedWarning:YES];
} }
}); });
}]; }];
@ -143,7 +143,7 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) {
} }
} }
[self installInputMethodWithWarning:NO]; [self installInputMethodWithPreviousExists:NO previousVersionNotFullyDeactivatedWarning:NO];
} }
- (void)timerTick:(NSTimer *)timer - (void)timerTick:(NSTimer *)timer
@ -162,7 +162,7 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) {
} }
- (void)installInputMethodWithWarning:(BOOL)warning - (void)installInputMethodWithPreviousExists:(BOOL)previousVersionExists previousVersionNotFullyDeactivatedWarning:(BOOL)warning
{ {
// If the unzipped archive does not exist, this must be a dev-mode installer. // If the unzipped archive does not exist, this must be a dev-mode installer.
NSString *targetBundle = [_archiveUtil unzipNotarizedArchive]; NSString *targetBundle = [_archiveUtil unzipNotarizedArchive];
@ -177,23 +177,80 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) {
[NSApp terminate:self]; [NSApp terminate:self];
} }
NSArray *installArgs = [NSArray arrayWithObjects:@"install", nil]; NSBundle *imeBundle = [NSBundle bundleWithPath:[kTargetPartialPath stringByExpandingTildeInPath]];
NSTask *installTask = [NSTask launchedTaskWithLaunchPath:[kTargetFullBinPartialPath stringByExpandingTildeInPath] arguments:installArgs]; NSCAssert(imeBundle != nil, @"Target bundle must exists");
[installTask waitUntilExit]; NSURL *imeBundleURL = imeBundle.bundleURL;
if ([installTask terminationStatus] != 0) { NSString *imeIdentifier = imeBundle.bundleIdentifier;
RunAlertPanel(NSLocalizedString(@"Install Failed", nil), NSLocalizedString(@"Cannot activate the input method.", nil), NSLocalizedString(@"Cancel", nil));
[NSApp terminate:self]; TISInputSourceRef inputSource = [OVInputSourceHelper inputSourceForInputSourceID:imeIdentifier];
// if this IME name is not found in the list of available IMEs
if (!inputSource) {
NSLog(@"Registering input source %@ at %@.", imeIdentifier, imeBundleURL.absoluteString);
// then register
BOOL status = [OVInputSourceHelper registerInputSource:imeBundleURL];
if (!status) {
// TODO: Localize.
NSString *message = [NSString stringWithFormat:@"Fatal error: Cannot register input source %@ at %@.", imeIdentifier, imeBundleURL.absoluteString];
RunAlertPanel(@"Fatal Error", message, @"Abort");
[self endAppWithDelay];
return;
}
inputSource = [OVInputSourceHelper inputSourceForInputSourceID:imeIdentifier];
// if it still doesn't register successfully, bail.
if (!inputSource) {
// TODO: Localize.
NSString *message = [NSString stringWithFormat:@"Fatal error: Cannot find input source %@ after registration.", imeIdentifier];
RunAlertPanel(@"Fatal Error", message, @"Abort");
[self endAppWithDelay];
return;
}
}
BOOL isMacOS12OrAbove = NO;
if (@available(macOS 12.0, *)) {
NSLog(@"macOS 12 or later detected.");
isMacOS12OrAbove = YES;
} else {
NSLog(@"Installer runs with the pre-macOS 12 flow.");
}
// If the IME is not enabled, enable it. Also, unconditionally enable it on macOS 12.0+,
// as the kTISPropertyInputSourceIsEnabled can still be true even if the IME is *not*
// enabled in the user's current set of IMEs (which means the IME does not show up in
// the user's input menu).
BOOL mainInputSourceEnabled = [OVInputSourceHelper inputSourceEnabled:inputSource];
if (!mainInputSourceEnabled || isMacOS12OrAbove) {
mainInputSourceEnabled = [OVInputSourceHelper enableInputSource:inputSource];
if (mainInputSourceEnabled) {
NSLog(@"Input method enabled: %@", imeIdentifier);
} else {
NSLog(@"Failed to enable input method: %@", imeIdentifier);
}
} }
if (warning) { if (warning) {
RunAlertPanel(NSLocalizedString(@"Attention", nil), NSLocalizedString(@"McBopomofo is upgraded, but please log out or reboot for the new version to be fully functional.", nil), NSLocalizedString(@"OK", nil)); RunAlertPanel(NSLocalizedString(@"Attention", nil), NSLocalizedString(@"McBopomofo is upgraded, but please log out or reboot for the new version to be fully functional.", nil), NSLocalizedString(@"OK", nil));
} else { } else {
RunAlertPanel(NSLocalizedString(@"Installation Successful", nil), NSLocalizedString(@"McBopomofo is ready to use.", nil), NSLocalizedString(@"OK", nil)); // Only prompt a warning if pre-macOS 12. The flag is not indicative of anything meaningful due to the need of user intervention in Prefernces.app on macOS 12.
if (!mainInputSourceEnabled && !isMacOS12OrAbove) {
// TODO: Localize
RunAlertPanel(@"Warning", @"Input method may not be fully enabled. Please check Preferences.app.", @"Continue");
} else {
RunAlertPanel(NSLocalizedString(@"Installation Successful", nil), NSLocalizedString(@"McBopomofo is ready to use.", nil), NSLocalizedString(@"OK", nil));
}
} }
[[NSApplication sharedApplication] performSelector:@selector(terminate:) withObject:self afterDelay:0.1]; [self endAppWithDelay];
} }
- (void)endAppWithDelay
{
[[NSApplication sharedApplication] performSelector:@selector(terminate:) withObject:self afterDelay:0.1];
}
- (IBAction)cancelAction:(id)sender - (IBAction)cancelAction:(id)sender
{ {

View File

@ -41,6 +41,7 @@
+ (BOOL)inputSourceEnabled:(TISInputSourceRef)inInputSource; + (BOOL)inputSourceEnabled:(TISInputSourceRef)inInputSource;
+ (BOOL)enableInputSource:(TISInputSourceRef)inInputSource; + (BOOL)enableInputSource:(TISInputSourceRef)inInputSource;
+ (BOOL)enableAllInputModesForInputSourceBundleID:(NSString *)inID; + (BOOL)enableAllInputModesForInputSourceBundleID:(NSString *)inID;
+ (BOOL)enableInputMode:(NSString *)modeID forInputSourceBundleID:(NSString *)bundleID;
+ (BOOL)disableInputSource:(TISInputSourceRef)inInputSource; + (BOOL)disableInputSource:(TISInputSourceRef)inInputSource;
// register (i.e. make available to Input Source tab in Language & Text Preferences) // register (i.e. make available to Input Source tab in Language & Text Preferences)

View File

@ -90,6 +90,24 @@
return enabled; return enabled;
} }
+ (BOOL)enableInputMode:(NSString *)modeID forInputSourceBundleID:(NSString *)bundleID
{
for (id source in [self allInstalledInputSources]) {
TISInputSourceRef inputSource = (__bridge TISInputSourceRef)source;
NSString *inputSoureBundleID = (__bridge NSString *)TISGetInputSourceProperty(inputSource, kTISPropertyBundleID);
NSString *inputSourceModeID = (NSString *)CFBridgingRelease(TISGetInputSourceProperty(inputSource, kTISPropertyInputModeID));
if ([modeID isEqual:inputSourceModeID] && [bundleID isEqual:inputSoureBundleID]) {
BOOL enabled = [self enableInputSource:inputSource];
NSLog(@"Attempt to enable input source of mode: %@, bundle ID: %@, result: %d", modeID, bundleID, enabled);
return enabled;
}
}
NSLog(@"Failed to find any matching input source of mode: %@, bundle ID: %@", modeID, bundleID);
return NO;
}
+ (BOOL)disableInputSource:(TISInputSourceRef)inInputSource + (BOOL)disableInputSource:(TISInputSourceRef)inInputSource
{ {
OSStatus status = TISDisableInputSource(inInputSource); OSStatus status = TISDisableInputSource(inInputSource);