fix(windows): Warn if we reach maximum transient languages

Fixes #3749.
Fixes #3759.

Adds a warning dialog when user attempts to add a transient language to
a keyboard but the maximum number of transient languages is already
installed.

If this issue arises when a user is installing a keyboard for the first
time, Keyman instead installs the keyboard for the user's default
language. This may be a little confusing, but the error condition is
difficult to explain and non-technical users will probably be stuck
and need to install under their default language in any case. Happy to
receive any pushback on this decision.
This commit is contained in:
Marc Durdin 2020-12-16 09:09:19 +11:00
parent d10be475c0
commit 246cb70840
5 changed files with 166 additions and 30 deletions

View file

@ -31,6 +31,9 @@ type
/// <summary>Helper function to get default BCP47 tag for an installed keyboard</summary>
class function GetFirstLanguage(Keyboard: IKeymanKeyboardInstalled): string; overload;
class function GetFirstLanguage(Keyboard: IKeymanKeyboardFile): string; overload;
/// <summary>Get the BCP47 tag for the user's default language</summary>
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,

View file

@ -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;

View file

@ -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;

View file

@ -1068,6 +1068,12 @@ keyboard that you use in Windows. Keyman keyboards will adapt automatically to
<!-- String Type: FormatString -->
<string name="SKKeyboardPartOfPackage" comment="Notice of need to uninstall an entire package">The keyboard '%0:s' is part of package '%1:s'. You must uninstall the entire package. Continue?</string>
<!-- PArameters: %0:s = BCP 47 code -->
<!-- Context: Formatted Messages -->
<!-- Introduced: 14.0.211 -->
<!-- String Type: FormatString -->
<string name="SKInstallLanguageTransientLimit" comment="Message shown when Keyman is unable to install a Windows language for a keyboard">Unable to install keyboard language; Windows has a limit of 4 custom 'transient' languages, and you may have reached this limit.</string>
<!-- Parameters: %0:s = Package Name -->
<!-- Context: Formatted Messages -->
<!-- Introduced: 7.0.230.0 -->

View file

@ -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
//