diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextCompatibilityCheck.m b/mac/Keyman4MacIM/Keyman4MacIM/TextCompatibilityCheck.m index 3797d64864..85932260d8 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextCompatibilityCheck.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextCompatibilityCheck.m @@ -6,19 +6,28 @@ * * Created by Shawn Schantz on 2023-05-05. * - * Check whether the current text input client is compatibility with APIs. - * Compliance is determined by attempting the current selection (location and length), - * but this is not a surefire indicator. For some apps, a valid but incorrect selection of - * {0, 0} is returned regardless of the real selection. + * Check whether the current text input client is compliant with APIs. + * Compliance is determined by getting the current selection (location and length) with + * the selectionRange API and evaluating the results. + * But this is not a surefire indicator. For several apps, a valid but incorrect selection of + * {0, 0}, or possibly something else, is returned regardless of the real selection. * - * When {0, 0} is returned, then we can check to see if a subsequent insert results in a - * change in the location. It always should, because it cannot replace at location 0. - * If the location does change, then the selection API is compliant. + * When {0, 0} is returned, then we can check to see if a later call to the insertText API + * results in a change in the location. It always should, because there is no text to + * replace if the client truly is at location 0. + * If the location does change, then we assume the selection API is compliant. * - * Even worse for some apps, the selection API works, but attempts - * to replace during an insert do not work. There is no way to detect this behavior, - * so these apps must be hard-coded as non-compliant. A couple of known apps - * that behave this way are Brackets (Adobe OS project) and MacVIM + * Even worse for some apps, the selection API returns a non-zero location, but attempts + * to replace text during a call to insertText do not function properly. There is no way to + * detect this behavior, so these apps must be hard-coded as non-compliant. A couple + * of known apps that behave this way are Brackets (Adobe OS project) and MacVIM + * + * We really only have the ability to test the selection API, and if it does not work, then + * Keyman functionality is limited. Lacking the current location + * - we cannot pass a range for reading the context + * - we cannot pass a replacement range when calling insertText + * The result is that we are limited in key processing, using only locally cached context, and + * we can only delete by sending backspace events. */ #import "TextCompatibilityCheck.h" @@ -31,8 +40,6 @@ NSString *const kKMLegacyApps = @"KMLegacyApps"; @property (readonly) id client; @property BOOL apiComplianceUncertain; @property BOOL hasCompliantSelectionApi; -@property (readonly) BOOL hasReadApi; -@property (readonly) BOOL hasInsertApi; @end @@ -45,27 +52,17 @@ NSString *const kKMLegacyApps = @"KMLegacyApps"; _clientApplicationId = appId; _apiComplianceUncertain = YES; - // first check in the noncompliant app lists - // TODO: uncomment after testing - //BOOL isUncompliantApp = ![self containedInNoncompliantAppLists:clientAppId]; - BOOL isUncompliantApp = NO; - - if (isUncompliantApp) { - _apiComplianceUncertain = NO; - self.hasCompliantSelectionApi = NO; - } else { + // if we do not have hard-coded noncompliance, then test the app + if (![self applyNoncompliantAppLists:appId]) { [self testApiCompliance:client]; } - - _hasReadApi = [client respondsToSelector:@selector(attributedSubstringFromRange:)]; - _hasInsertApi = [client respondsToSelector:@selector(insertText:replacementRange:)]; } return self; } -(NSString *)description { -return [NSString stringWithFormat:@"apiComplianceUncertain: %d, hasWorkingSelectionApi: %d, hasReadAPI: %d, hasInsertAPI: %d, canGetSelection: %d, canReadText: %d, canInsertText: %d, canReplaceText: %d, mustBackspaceUsingEvents: %d, clientAppId: %@, client: %@", self.apiComplianceUncertain, self.hasCompliantSelectionApi, self.hasReadApi, self.hasInsertApi, [self canGetSelection], [self canReadText], [self canInsertText], [self canReplaceText], [self mustBackspaceUsingEvents], _clientApplicationId, _client]; +return [NSString stringWithFormat:@"apiComplianceUncertain: %d, hasCompliantSelectionApi: %d, canReadText: %d, canReplaceText: %d, mustBackspaceUsingEvents: %d, clientAppId: %@, client: %@", self.apiComplianceUncertain, self.hasCompliantSelectionApi, [self canReadText], [self canReplaceText], [self mustBackspaceUsingEvents], _clientApplicationId, _client]; } /** test to see if the API selectedRange functions properly for the text input client */ @@ -136,16 +133,22 @@ return [NSString stringWithFormat:@"apiComplianceUncertain: %d, hasWorkingSelect } /** - * Checks if the client app is known to be non-complian, first by checking the hard-coded non-compliant app list - * and then by checking the user-managed (via user defaults) non-compliant app list + * Apply the hard-coded non-compliant app list and the user-managed (via user defaults) non-compliant app list. + * If true, mark the app as non-compliant and certain (not apiComplianceUncertain). */ -- (BOOL)containedInNoncompliantAppLists:(NSString *)clientAppId { +- (BOOL)applyNoncompliantAppLists:(NSString *)clientAppId { BOOL isAppNonCompliant = [self containedInHardCodedNoncompliantAppList:clientAppId]; if (!isAppNonCompliant) { isAppNonCompliant = [self containedInUserManagedNoncompliantAppList:clientAppId]; } NSLog(@"containedInNoncompliantAppLists: for app %@: %@", clientAppId, isAppNonCompliant?@"yes":@"no"); + + if (isAppNonCompliant) { + self.apiComplianceUncertain = NO; + self.hasCompliantSelectionApi = NO; + } + return isAppNonCompliant; } @@ -157,6 +160,8 @@ return [NSString stringWithFormat:@"apiComplianceUncertain: %d, hasWorkingSelect BOOL isAppNonCompliant = ([clientAppId isEqual: @"com.github.atom"] || [clientAppId isEqual: @"com.collabora.libreoffice-free"] || [clientAppId isEqual: @"org.libreoffice.script"] || + [clientAppId isEqual: @"org.vim.MacVim"] || + [clientAppId isEqual: @"io.brackets.appshell"] || [clientAppId isEqual: @"com.axosoft.gitkraken"] || [clientAppId isEqual: @"org.sil.app.builder.scripture.ScriptureAppBuilder"] || [clientAppId isEqual: @"org.sil.app.builder.reading.ReadingAppBuilder"] || @@ -201,53 +206,6 @@ return [NSString stringWithFormat:@"apiComplianceUncertain: %d, hasWorkingSelect return isAppNonCompliant; } -/** - * Checks if the client app requires legacy input mode, first by checking the user defaults, if they exist, - * then, by our hard-coded list. - */ -/* -- (BOOL)isClientAppLegacy:(NSString *)clientAppId { - BOOL isAppNonCompliant = NO; - - NSArray *legacyAppsUserDefaults = self.legacyAppsUserDefaults; - - BOOL result = NO; - - if(legacyAppsUserDefaults != nil) { - result = [self isClientAppLegacy:clientAppId fromArray:legacyAppsUserDefaults]; - } - - if(!result) { - // TODO: Pages and Keynote (and possibly lots of other undiscovered apps that are otherwise compliant - // with Apple's IM framework) have a problem in that if the user selects a different font (or other - // formatting) and then types a sequence that causes characters to be added to the document and then - // subsequently replaced, the replacement causes the formatting decision to be forgotten. This can be - // "fixed" by treating them as legacy apps, but it causes other problems. - result = ([clientAppId isEqual: @"com.github.atom"] || - [clientAppId isEqual: @"com.collabora.libreoffice-free"] || - [clientAppId isEqual: @"org.libreoffice.script"] || - [clientAppId isEqual: @"com.axosoft.gitkraken"] || - [clientAppId isEqual: @"org.sil.app.builder.scripture.ScriptureAppBuilder"] || - [clientAppId isEqual: @"org.sil.app.builder.reading.ReadingAppBuilder"] || - [clientAppId isEqual: @"org.sil.app.builder.dictionary.DictionaryAppBuilder"] || - //[clientAppId isEqual: @"com.microsoft.Word"] || // 2020-11-24[mcd]: Appears to work well in Word 16.43, disable legacy by default - [clientAppId isEqual: @"org.openoffice.script"] || - [clientAppId isEqual: @"com.adobe.illustrator"] || - [clientAppId isEqual: @"com.adobe.InDesign"] || - [clientAppId isEqual: @"com.adobe.Photoshop"] || - [clientAppId isEqual: @"com.adobe.AfterEffects"] || - [clientAppId isEqual: @"com.microsoft.VSCode"] || - [clientAppId isEqual: @"com.google.Chrome"] || - [clientAppId hasPrefix: @"net.java"] || - [clientAppId isEqual: @"com.Keyman.test.legacyInput"] - //||[clientAppId isEqual: @"ro.sync.exml.Oxygen"] - Oxygen has worse problems - ); - } - - return result; -} -*/ - /** * Checks array for a list of possible regexes to match a client app id */ @@ -275,25 +233,16 @@ return [NSString stringWithFormat:@"apiComplianceUncertain: %d, hasWorkingSelect return self.apiComplianceUncertain; } - --(BOOL) canGetSelection { - return self.hasCompliantSelectionApi; -} - -(BOOL) canReadText { // seems simple, but every application tested that returned the selection also successfully read the text using attributedSubstringFromRange - BOOL canReadText = self.hasReadApi && self.canGetSelection; - return canReadText; -} - --(BOOL) canInsertText { - // all applications tested can insertText (though replacement may not work) - return self.hasInsertApi; + + BOOL hasReadApi = [self.client respondsToSelector:@selector(attributedSubstringFromRange:)]; + return hasReadApi && self.hasCompliantSelectionApi; } -(BOOL) canReplaceText { // testing shows that every application that returns the selection, can also replace text - return self.hasInsertApi && self.canGetSelection; + return self.hasCompliantSelectionApi; } -(BOOL) mustBackspaceUsingEvents { @@ -301,43 +250,4 @@ return [NSString stringWithFormat:@"apiComplianceUncertain: %d, hasWorkingSelect return !self.canReplaceText; } -/* --(void) experiment { - NSRange notFoundRange = NSMakeRange(NSNotFound, NSNotFound); - NSRange zeroRange = NSMakeRange(0, 0); - NSRange selectionRange = [self.client selectedRange]; - - if (NSEqualRanges(selectionRange, notFoundRange)) { - NSLog(@"**compatibility** ERROR selectedRange = {NSNotFound, NSNotFound}"); - } else if (NSEqualRanges(selectionRange, notFoundRange)) { - NSLog(@"**compatibility** ERROR selectedRange = {0,0}"); - } else { - NSLog(@"**compatibility** selectedRange = %@", NSStringFromRange(selectionRange)); - } - - NSRange contextRange = NSMakeRange(1, 0); - NSLog(@"**compatibility** get string of empty range, location 1, length 0"); - - NSAttributedString *attributedText = [self.client attributedSubstringFromRange:contextRange]; - - if (attributedText == nil) { - NSLog(@" **compatibility** attributedSubstringFromRange({1,0}) is nil"); - } else { - NSString *context = attributedText.string; - NSLog(@" **compatibility** attributedSubstringFromRange = %@", context); - } - - contextRange = NSMakeRange(1, 1); - NSLog(@"**compatibility** get string at location 1, length 1"); - - attributedText = [self.client attributedSubstringFromRange:contextRange]; - - if (attributedText == nil) { - NSLog(@" **compatibility** attributedSubstringFromRange({1,1}) is nil"); - } else { - NSString *context = attributedText.string; - NSLog(@" **compatibility** attributedSubstringFromRange = %@", context); - } -} -*/ @end