diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 66dd9c97..64f7b808 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -33,6 +33,7 @@ 6A2E40F9253A6AA000D1AE1D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6A2E40F5253A69DA00D1AE1D /* Images.xcassets */; }; 6A38BC1515FC117A00A8A51F /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 6A38BBF615FC117A00A8A51F /* data.txt */; }; 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 */; }; 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ACA41E915FC1D9000935EF6 /* AppDelegate.m */; }; 6ACA41FA15FC1D9000935EF6 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41EA15FC1D9000935EF6 /* InfoPlist.strings */; }; @@ -660,6 +661,7 @@ files = ( 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */, 6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */, + 6AB3620D274CA50700AC7547 /* OVInputSourceHelper.m in Sources */, 6ACA41FF15FC1D9000935EF6 /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1038,6 +1040,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ../; INFOPLIST_FILE = "Source/Installer/Installer-Info.plist"; MACOSX_DEPLOYMENT_TARGET = 10.10; ONLY_ACTIVE_ARCH = YES; @@ -1070,6 +1073,7 @@ GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ../; INFOPLIST_FILE = "Source/Installer/Installer-Info.plist"; MACOSX_DEPLOYMENT_TARGET = 10.10; PRODUCT_BUNDLE_IDENTIFIER = "org.openvanilla.McBopomofo.${PRODUCT_NAME:rfc1034identifier}"; diff --git a/Source/Installer/AppDelegate.m b/Source/Installer/AppDelegate.m index 435e2c37..863b4ec0 100644 --- a/Source/Installer/AppDelegate.m +++ b/Source/Installer/AppDelegate.m @@ -27,6 +27,7 @@ #import "AppDelegate.h" #import +#import "OVInputSourceHelper.h" static NSString *const kTargetBin = @"McBopomofo"; static NSString *const kTargetType = @"app"; @@ -55,7 +56,6 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { @synthesize progressSheet = _progressSheet; @synthesize progressIndicator = _progressIndicator; - - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { _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. dispatch_async(dispatch_get_main_queue(), ^{ if (returnCode == NSModalResponseContinue) { - [self installInputMethodWithWarning:NO]; + [self installInputMethodWithPreviousExists:YES previousVersionNotFullyDeactivatedWarning:NO]; } 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 @@ -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. NSString *targetBundle = [_archiveUtil unzipNotarizedArchive]; @@ -177,23 +177,80 @@ void RunAlertPanel(NSString *title, NSString *message, NSString *buttonTitle) { [NSApp terminate:self]; } - NSArray *installArgs = [NSArray arrayWithObjects:@"install", nil]; - NSTask *installTask = [NSTask launchedTaskWithLaunchPath:[kTargetFullBinPartialPath stringByExpandingTildeInPath] arguments:installArgs]; - [installTask waitUntilExit]; - if ([installTask terminationStatus] != 0) { - RunAlertPanel(NSLocalizedString(@"Install Failed", nil), NSLocalizedString(@"Cannot activate the input method.", nil), NSLocalizedString(@"Cancel", nil)); - [NSApp terminate:self]; + NSBundle *imeBundle = [NSBundle bundleWithPath:[kTargetPartialPath stringByExpandingTildeInPath]]; + NSCAssert(imeBundle != nil, @"Target bundle must exists"); + NSURL *imeBundleURL = imeBundle.bundleURL; + NSString *imeIdentifier = imeBundle.bundleIdentifier; + + 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) { 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 { - 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)); + } } + [self endAppWithDelay]; +} + +- (void)endAppWithDelay +{ [[NSApplication sharedApplication] performSelector:@selector(terminate:) withObject:self afterDelay:0.1]; } - - (IBAction)cancelAction:(id)sender { diff --git a/Source/OVInputSourceHelper.h b/Source/OVInputSourceHelper.h index b55981ed..cc4c8919 100644 --- a/Source/OVInputSourceHelper.h +++ b/Source/OVInputSourceHelper.h @@ -41,6 +41,7 @@ + (BOOL)inputSourceEnabled:(TISInputSourceRef)inInputSource; + (BOOL)enableInputSource:(TISInputSourceRef)inInputSource; + (BOOL)enableAllInputModesForInputSourceBundleID:(NSString *)inID; ++ (BOOL)enableInputMode:(NSString *)modeID forInputSourceBundleID:(NSString *)bundleID; + (BOOL)disableInputSource:(TISInputSourceRef)inInputSource; // register (i.e. make available to Input Source tab in Language & Text Preferences) diff --git a/Source/OVInputSourceHelper.m b/Source/OVInputSourceHelper.m index d9815879..b7638a34 100644 --- a/Source/OVInputSourceHelper.m +++ b/Source/OVInputSourceHelper.m @@ -90,6 +90,24 @@ 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 { OSStatus status = TISDisableInputSource(inInputSource);