diff --git a/windows/src/desktop/kmshell/install/Keyman.Configuration.System.TIPMaintenance.pas b/windows/src/desktop/kmshell/install/Keyman.Configuration.System.TIPMaintenance.pas index 5161bfd93c..35c683dc56 100644 --- a/windows/src/desktop/kmshell/install/Keyman.Configuration.System.TIPMaintenance.pas +++ b/windows/src/desktop/kmshell/install/Keyman.Configuration.System.TIPMaintenance.pas @@ -31,6 +31,9 @@ type /// Helper function to get default BCP47 tag for an installed keyboard class function GetFirstLanguage(Keyboard: IKeymanKeyboardInstalled): string; overload; class function GetFirstLanguage(Keyboard: IKeymanKeyboardFile): string; overload; + + /// Get the BCP47 tag for the user's default language + class function GetUserDefaultLanguage: string; static; private class function GetKeyboardLanguage(const KeyboardID, BCP47Tag: string): IKeymanKeyboardLanguageInstalled; static; @@ -93,6 +96,7 @@ begin if lang = nil then Exit(False); + // TODO: can this fail? (lang as IKeymanKeyboardLanguageInstalled2).InstallTip(LangID, KeyboardToRemove); Result := True; end; @@ -106,6 +110,7 @@ begin if lang = nil then Exit(False); + // TODO: can this fail? (lang as IKeymanKeyboardLanguageInstalled2).RegisterTip(LangID); Result := True; end; @@ -117,6 +122,7 @@ var RegistrationRequired: WordBool; TemporaryKeyboardID: WideString; LangID: Integer; + childExitCode: Cardinal; begin lang := GetKeyboardLanguage(KeyboardID, (kmcom as IKeymanBCP47Canonicalization).GetCanonicalTag(BCP47Tag)); if lang = nil then @@ -131,13 +137,25 @@ begin LangID := 0; RegistrationRequired := False; - if (lang as IKeymanKeyboardLanguageInstalled2).FindInstallationLangID(LangID, TemporaryKeyboardID, RegistrationRequired, kifInstallTransientLanguage) then + if not (lang as IKeymanKeyboardLanguageInstalled2).FindInstallationLangID(LangID, TemporaryKeyboardID, RegistrationRequired, kifInstallTransientLanguage) then begin - if RegistrationRequired then - begin - WaitForElevatedConfiguration(0, '-register-tip '+IntToHex(LangID,4)+' "'+KeyboardID+'" "'+lang.BCP47Code+'"'); - end; - TUtilExecute.WaitForProcess('"'+ParamStr(0)+'" -install-tip '+IntToHex(LangID,4)+' "'+KeyboardID+'" "'+lang.BCP47Code+'" "'+TemporaryKeyboardID+'"', GetCurrentDir); + // We were not able to find a TIP, perhaps all transient TIPs have been used + Exit(False); + end; + + if RegistrationRequired then + begin + // This calls back into TTIPMaintenance.RegisterTip + if WaitForElevatedConfiguration(0, '-register-tip '+IntToHex(LangID,4)+' "'+KeyboardID+'" "'+lang.BCP47Code+'"') <> 0 then + Exit(False); + end; + + // This calls back into TTIPMaintenance.InstallTip + if not TUtilExecute.WaitForProcess('"'+ParamStr(0)+'" -install-tip '+IntToHex(LangID,4)+' "'+KeyboardID+'" "'+lang.BCP47Code+'" "'+TemporaryKeyboardID+'"', + GetCurrentDir, childExitCode) or + (childExitCode <> 0) then + begin + Exit(False); end; Result := True; @@ -178,18 +196,23 @@ begin Result := False; end; +class function TTIPMaintenance.GetUserDefaultLanguage: string; +begin + Result := TLanguageCodeUtils.TranslateWindowsLanguagesToBCP47(HKLToLanguageID(GetDefaultHKL)) +end; + class function TTIPMaintenance.GetFirstLanguage(Keyboard: IKeymanKeyboardFile): string; begin if Keyboard.Languages.Count > 0 then Result := Keyboard.Languages[0].BCP47Code - else Result := TLanguageCodeUtils.TranslateWindowsLanguagesToBCP47(HKLToLanguageID(GetDefaultHKL)); + else Result := GetUserDefaultLanguage; end; class function TTIPMaintenance.GetFirstLanguage(Keyboard: IKeymanKeyboardInstalled): string; begin if Keyboard.Languages.Count > 0 then Result := Keyboard.Languages[0].BCP47Code - else Result := TLanguageCodeUtils.TranslateWindowsLanguagesToBCP47(HKLToLanguageID(GetDefaultHKL)); + else Result := GetUserDefaultLanguage; end; class function TTIPMaintenance.GetKeyboardLanguage(const KeyboardID, diff --git a/windows/src/desktop/kmshell/install/UfrmInstallKeyboard.pas b/windows/src/desktop/kmshell/install/UfrmInstallKeyboard.pas index 60ed59279b..48386b9369 100644 --- a/windows/src/desktop/kmshell/install/UfrmInstallKeyboard.pas +++ b/windows/src/desktop/kmshell/install/UfrmInstallKeyboard.pas @@ -97,7 +97,7 @@ type procedure DeleteFileReferences; procedure CheckLogFileForWarnings(const Filename: string; Silent: Boolean); function CleanupPaths(var xml: string): string; - procedure InstallTipForKeyboard(const BCP47Tag: string); + function InstallTipForKeyboard(const BCP47Tag: string): Boolean; protected procedure FireCommand(const command: WideString; params: TStringList); override; public @@ -299,7 +299,10 @@ begin if WaitForElevatedConfiguration(Handle, '-log "'+t.Name+'" -s -i "'+FInstallFile+'='+BCP47Tag+'" -nowelcome') = 0 then begin // install the keyboard tip - InstallTipForKeyboard(BCP47Tag); + if not InstallTipForKeyboard(BCP47Tag) then + Exit(False); + + CheckForMitigationWarningFor_Win10_1803(False, ''); ModalResult := mrOk; end else @@ -381,7 +384,11 @@ begin kmcom.Keyboards.Apply; kmcom.Keyboards.Refresh; FInstalledKeyboard := (FKeyboard as IKeymanKeyboardFile2).Install2(True); - InstallTipForKeyboard(BCP47Tag); + if not InstallTipForKeyboard(BCP47Tag) then + begin + // TODO can we return a failure code? + Exit; + end; CheckForMitigationWarningFor_Win10_1803(FSilent, ALogFile); end else @@ -441,7 +448,10 @@ begin kmcom.Refresh; - InstallTipForKeyboard(BCP47Tag); + if not InstallTipForKeyboard(BCP47Tag) then + begin + Exit; + end; CheckForMitigationWarningFor_Win10_1803(FSilent, ALogFile); end; @@ -467,28 +477,51 @@ begin ModalResult := mrOk; end; -procedure TfrmInstallKeyboard.InstallTipForKeyboard(const BCP47Tag: string); -var - i: Integer; +function TfrmInstallKeyboard.InstallTipForKeyboard(const BCP47Tag: string): Boolean; + function DoInstallTipForKeyboard(const BCP47Tag: string): Boolean; + var + i: Integer; + begin + // Install the TIP for the current user + if Assigned(FKeyboard) then + begin + if BCP47Tag <> '' + then Result := TTIPMaintenance.DoInstall(FKeyboard.ID, BCP47Tag) + else Result := TTIPMaintenance.DoInstall(FKeyboard.ID, TTIPMaintenance.GetFirstLanguage(FKeyboard)); + end + else + begin + if (FPackage.Keyboards.Count = 1) and (BCP47Tag <> '') then + Result := TTIPMaintenance.DoInstall(FPackage.Keyboards[0].ID, BCP47Tag) + else + begin + Result := True; + for i := 0 to FPackage.Keyboards.Count - 1 do + // For a multi-keyboard package, it's hard to know what to do! + if not TTIPMaintenance.DoInstall(FPackage.Keyboards[i].ID, + TTIPMaintenance.GetFirstLanguage(FPackage.Keyboards[i] as IKeymanKeyboardFile)) then + Result := False; + end; + end; + end; begin // Ensure keyboard is recorded in CU registry before we install the TIP, otherwise it will be marked as disabled by default, // as installing the TIP adds CU registry settings, potentially confusing the keyboard settings kmcom.Refresh; kmcom.Apply; - // Install the TIP for the current user - if Assigned(FKeyboard) then + + Result := DoInstallTipForKeyboard(BCP47Tag); + if not Result then begin - if BCP47Tag <> '' - then TTIPMaintenance.DoInstall(FKeyboard.ID, BCP47Tag) - else TTIPMaintenance.DoInstall(FKeyboard.ID, TTIPMaintenance.GetFirstLanguage(FKeyboard)); - end - else - begin - if (FPackage.Keyboards.Count = 1) and (BCP47Tag <> '') then - TTIPMaintenance.DoInstall(FPackage.Keyboards[0].ID, BCP47Tag) - else - for i := 0 to FPackage.Keyboards.Count - 1 do - TTIPMaintenance.DoInstall(FPackage.Keyboards[i].ID, TTIPMaintenance.GetFirstLanguage(FPackage.Keyboards[i] as IKeymanKeyboardFile)); + // We'll silently fall back to installing under the default language; + // they may have hit the limit of custom languages but an error message + // is likely to be very confusing. + Result := DoInstallTipForKeyboard(TTIPMaintenance.GetUserDefaultLanguage); + if not Result then + begin + if not FSilent then + ShowMessage(MsgFromIdFormat(SKInstallLanguageTransientLimit, [BCP47Tag])); + end; end; end; diff --git a/windows/src/desktop/kmshell/install/UfrmInstallKeyboardLanguage.pas b/windows/src/desktop/kmshell/install/UfrmInstallKeyboardLanguage.pas index 87966567ce..0f3e98f93b 100644 --- a/windows/src/desktop/kmshell/install/UfrmInstallKeyboardLanguage.pas +++ b/windows/src/desktop/kmshell/install/UfrmInstallKeyboardLanguage.pas @@ -135,6 +135,8 @@ uses Keyman.System.LanguageCodeUtils, Keyman.Configuration.System.TIPMaintenance, Keyman.UI.UfrmProgress, + MessageIdentifierConsts, + MessageIdentifiers, BCP47Tag, GetOSVersion, @@ -148,7 +150,13 @@ uses function InstallKeyboardLanguage(Owner: TForm; const KeyboardID, ISOCode: string; Silent: Boolean): Boolean; begin Result := TTIPMaintenance.DoInstall(KeyboardID, ISOCode); - if Result then + if not Result then + begin + if not Silent then + ShowMessage(MsgFromIdFormat(SKInstallLanguageTransientLimit, [ISOCode])); + Exit; + end + else CheckForMitigationWarningFor_Win10_1803(Silent, ''); Result := True; @@ -233,7 +241,11 @@ begin then FCode := FCustomLanguage.Tag // Using a custom code else FCode := FLanguageVariant.BCP47Tag; - TTIPMaintenance.DoInstall(FKeyboard.ID, FCode); + if not TTIPMaintenance.DoInstall(FKeyboard.ID, FCode) then + begin + ShowMessage(MsgFromIdFormat(SKInstallLanguageTransientLimit, [FCode])); + Exit(False); + end; FKeyboardID := FKeyboard.ID; FKeyboard := nil; diff --git a/windows/src/desktop/kmshell/xml/strings.xml b/windows/src/desktop/kmshell/xml/strings.xml index cd21fc27da..f09a01d348 100644 --- a/windows/src/desktop/kmshell/xml/strings.xml +++ b/windows/src/desktop/kmshell/xml/strings.xml @@ -1068,6 +1068,12 @@ keyboard that you use in Windows. Keyman keyboards will adapt automatically to The keyboard '%0:s' is part of package '%1:s'. You must uninstall the entire package. Continue? + + + + + Unable to install keyboard language; Windows has a limit of 4 custom 'transient' languages, and you may have reached this limit. + diff --git a/windows/src/engine/kmcomapi/processes/keyboard/Keyman.System.Process.KPInstallKeyboardLanguage.pas b/windows/src/engine/kmcomapi/processes/keyboard/Keyman.System.Process.KPInstallKeyboardLanguage.pas index 5f6f77c6cd..2017a84fae 100644 --- a/windows/src/engine/kmcomapi/processes/keyboard/Keyman.System.Process.KPInstallKeyboardLanguage.pas +++ b/windows/src/engine/kmcomapi/processes/keyboard/Keyman.System.Process.KPInstallKeyboardLanguage.pas @@ -73,6 +73,7 @@ type function ConvertBCP47TagToLangID(Locale: string; var LangID: Integer): Boolean; function InstallBCP47Language(const FLocaleName: string): Boolean; function GetInputProcessorProfileMgr: ITfInputProcessorProfileMgr; + function AreMaximumTransientLanguagesInstalled: Boolean; end; implementation @@ -113,6 +114,54 @@ begin inherited Destroy; end; +function TKPInstallKeyboardLanguage.AreMaximumTransientLanguagesInstalled: Boolean; +const + MAX_TRANSIENT_LANGUAGES = 4; +var + r: TRegistry; + keys: TStringList; + isInstalled: array[0..MAX_TRANSIENT_LANGUAGES-1] of Boolean; + i: Integer; + v: Integer; + key: string; +begin + for i := 0 to high(isInstalled) do isInstalled[i] := False; + + // We'll use Control Panel/International/User Profile to find the full set of + // transient languages + r := TRegistry.Create; + keys := TStringList.Create; + try + if r.OpenKeyReadOnly(SRegKey_ControlPanelInternationalUserProfile) then + begin + r.GetKeyNames(keys); + for key in keys do + begin + if r.OpenKeyReadOnly('\' + SRegKey_ControlPanelInternationalUserProfile + '\' + key) then + begin + if r.ValueExists(SRegValue_CPIUP_TransientLangId) then + begin + v := r.ReadInteger(SRegValue_CPIUP_TransientLangId); + // Transient languages start at $2000 and go up by $400 + v := (v - $2000) div $400; + if (v >= 0) and (v <= High(isInstalled)) then + isInstalled[v] := True; + end; + end; + end; + end; + finally + keys.Free; + r.Free; + end; + + for i := 0 to High(isInstalled) do + if not isInstalled[i] then + Exit(False); + + Result := True; +end; + // TODO: change LangID to Winapi.Windows.LANGID function TKPInstallKeyboardLanguage.FindInstallationLangID(const BCP47Tag: string; var LangID: Integer; var TemporaryLayoutString: string; Flags: TKPInstallKeyboardLanguageFlags): Boolean; var @@ -152,6 +201,19 @@ begin Exit(False); end; + // + // Windows only appears to support 4 transient languages: 0x2000, 0x2400, + // 0x2800, 0x2C00. While Powershell will add extra languages above those, + // it will not install TIPs for any additional languages (e.g. 0x3000+), + // so we should stop after 4 + // + + if AreMaximumTransientLanguagesInstalled then + begin + Warn(KMN_W_ProfileInstall_CustomLocalesNotSupported); // TODO new code + Exit(False); + end; + // // Install user language with Powershell if it isn't present //