unit Keyman.System.Process.KPInstallKeyboardLanguage; interface uses System.Win.Registry, Winapi.Windows, kpbase, KeymanContext, Windows8LanguageList, Winapi.msctf, keyman_msctf; type TKPInstallKeyboardLanguageFlags = set of (ilkInstallTransientLanguage); type TKPInstallKeyboardLanguage = class(TKPBase) public /// Given a BCP47 Tag, finds a corresponding LangID, including installing /// a temporary keyboard if necessary to establish a transient LangID. /// Current User. /// True if a language ID can be found /// A canonical BCP47 tag to install /// LangID found that corresponds to BCP47Tag /// If a transient LangID is installed, then /// Windows will have added a default keyboard for the language which should be /// removed after we install the Keyman TIP. /// If ilkInstallTransientLanguage is included in flags, /// will not install a transient language and will return False if no standard /// Windows LangID can be found that corresponds to the BCP47 tag. function FindInstallationLangID(const BCP47Tag: string; var LangID: Integer; var TemporaryLayoutString: string; Flags: TKPInstallKeyboardLanguageFlags): Boolean; /// Registers a TIP for a given keyboard + language, and adds metadata to the /// HKLM registry about the Keyman keyboard association. Requires elevation. /// Keyman keyboard to register /// BCP47 tag associated with the TIP /// Name of keyboard to display in Windows UI /// Language ID found with FindInstallationLangID /// Icon to display in Windows UI /// Name of language to display in Keyman UI procedure RegisterTip(const KeyboardID, BCP47Tag, KeyboardName: string; LangID: Integer; IconFileName, LanguageName: string); /// Installs a TIP for a given keyboard + language, for the current user. /// Keyman keyboard to associate with TIP /// BCP47 tag associated with the TIP /// Language ID found with FindInstallationLangID procedure InstallTip(const KeyboardID, BCP47Tag: string; LangID: Integer); /// Removes a temporary keyboard layout that was installed by /// FindInstallationLangID. Current User. /// TemporaryLayoutString value returned from /// FindInstallationLangID procedure UninstallTemporaryLayout(const LayoutString: string); /// Registers a TIP for each of the four transient language IDs, /// and adds metadata to the HKLM registry about the Keyman keyboard /// association. Requires elevation. /// Keyman keyboard to register /// Name of keyboard to display in Windows UI /// Icon to display in Windows UI procedure RegisterTransientTips(const KeyboardID, KeyboardName: string; IconFileName: string); constructor Create(AContext: TKeymanContext); destructor Destroy; override; private FWin8Languages: TWindows8LanguageList; function ConvertBCP47TagToLangID(Locale: string; var LangID: Integer): Boolean; function InstallBCP47Language(const FLocaleName: string): Boolean; function GetInputProcessorProfileMgr: ITfInputProcessorProfileMgr; end; implementation uses System.Classes, System.SysUtils, System.Variants, System.Win.ComObj, Winapi.ActiveX, input_installlayoutortip, isadmin, Keyman.System.MitigateWin10_1803LanguageInstall, keymanerrorcodes, KeymanPaths, RegistryKeys, BCP47Tag, glossary, TempFileManager, utilexecute, utilfiletypes, utilkeyman, utilstr, utiltsf; { TKPInstallKeyboardLanguage } constructor TKPInstallKeyboardLanguage.Create(AContext: TKeymanContext); begin FWin8Languages := TWindows8LanguageList.Create; inherited Create(AContext); end; destructor TKPInstallKeyboardLanguage.Destroy; begin FreeAndNil(FWin8Languages); inherited Destroy; end; // TODO: change LangID to Winapi.Windows.LANGID function TKPInstallKeyboardLanguage.FindInstallationLangID(const BCP47Tag: string; var LangID: Integer; var TemporaryLayoutString: string; Flags: TKPInstallKeyboardLanguageFlags): Boolean; var ml: TMitigateWin10_1803.TMitigatedLanguage; Win8Lang: TWindows8Language; tag: string; begin if BCP47Tag = '' then ErrorFmt(KMN_E_ProfileInstall_InvalidBCP47Tag, VarArrayOf([BCP47Tag])); tag := BCP47Tag; TemporaryLayoutString := ''; if ConvertBCP47TagToLangID(tag, LangID) then begin if TMitigateWin10_1803.IsMitigationRequired(LangID, ml) then begin LangID := ml.NewLanguage.Code; WarnFmt(KMN_W_ProfileInstall_Win10_1803_MitigationApplied, VarArrayOf([ml.OriginalLanguage.Name, ml.NewLanguage.Name])); end; end else begin if not (ilkInstallTransientLanguage in Flags) then begin // No warning, because this is to be expected Exit(False); end; // // Installing a custom language only supported with Win8 and later // if not FWin8Languages.IsSupported then begin Warn(KMN_W_ProfileInstall_CustomLocalesNotSupported); Exit(False); end; // // Install user language with Powershell if it isn't present // LangID := 0; if (tag = '') or not InstallBCP47Language(tag) then begin WarnFmt(KMN_W_ProfileInstall_FailedToInstallLanguage, VarArrayOf([tag])); Exit(False); end; // // Find the new language ID // FWin8Languages.Refresh; Win8Lang := FWin8Languages.FindClosestByBCP47Tag(tag); if not Assigned(Win8Lang) then begin WarnFmt(KMN_W_ProfileInstall_LanguageInstalledButNotFound, VarArrayOf([tag])); Exit(False); end; LangID := Win8Lang.LangID; if Win8Lang.InputMethods.Count = 1 then begin TemporaryLayoutString := Win8Lang.InputMethods[0]; end else // We'll continue on, but this is unexpected, so we won't try and uninstall the temporary input method. The user // will have more than one input method installed. WarnFmt(KMN_W_ProfileInstall_MoreThanOneInputMethodInstalled, VarArrayOf([Win8Lang.InputMethods.Text])); end; Result := True; end; function TKPInstallKeyboardLanguage.ConvertBCP47TagToLangID(Locale: string; var LangID: Integer): Boolean; var Win8Lang: TWindows8Language; begin if FWin8Languages.IsSupported then begin // We have to find closest because we can't install e.g. am-ET and am and am-Ethi-ET Win8Lang := FWin8Languages.FindClosestByBCP47Tag(Locale); if Assigned(Win8Lang) then begin LangID := Win8Lang.LangID; Exit(True); end; end else begin // Assuming that the tag is a Language-Script-Region triplet at most. // If you use -Variant or -Extension then YMMV. with TBCP47Tag.Create(Locale) do try Script := ''; Locale := Tag; finally Free; end; end; LangID := LocaleNameToLCID(PWideChar(Locale), 0); case LangID of LOCALE_CUSTOM_DEFAULT, LOCALE_CUSTOM_UNSPECIFIED, 0: Result := False; else Result := True; end; end; function TKPInstallKeyboardLanguage.GetInputProcessorProfileMgr: ITfInputProcessorProfileMgr; var pInputProcessorProfiles: ITfInputProcessorProfiles; begin pInputProcessorProfiles := nil; OleCheck(CoCreateInstance(CLASS_TF_InputProcessorProfiles, nil, CLSCTX_INPROC_SERVER, IID_ITfInputProcessorProfiles, pInputProcessorProfiles)); if not Supports(pInputProcessorProfiles, IID_ITfInputProcessorProfileMgr, Result) then // I3743 raise Exception.Create('Missing interface IID_ITfInputProcessorProfileMgr'); end; procedure TKPInstallKeyboardLanguage.RegisterTip(const KeyboardID, BCP47Tag, KeyboardName: string; LangID: Integer; IconFileName, LanguageName: string); var FIsAdmin: Boolean; RootPath, FKeyboardName: string; reg: TRegistry; guid: TGUID; IconIndex: Integer; pInputProcessorProfileMgr: ITfInputProcessorProfileMgr; begin if IsTransientLanguageID(LangID) then ErrorFmt(KMN_E_ProfileInstall_IsATransientLanguageCode, VarArrayOf([KeyboardID, LangID])); if not KeyboardInstalled(KeyboardID, FIsAdmin) then ErrorFmt(KMN_E_ProfileInstall_KeyboardNotFound, VarArrayOf([KeyboardID])); // I3888 if KeyboardName = '' then FKeyboardName := KeyboardID + Ext_KeymanFile else FKeyboardName := KeyboardName; reg := TRegistry.Create; try reg.RootKey := HKEY_LOCAL_MACHINE; RootPath := GetRegistryKeyboardInstallKey_LM(KeyboardID) + SRegSubKey_LanguageProfiles; if not reg.OpenKey(RootPath + '\' + BCP47Tag, True) then ErrorFmt(KMN_E_ProfileInstall_MustBeAllUsers, VarArrayOf([KeyboardID])); reg.WriteInteger(SRegValue_LanguageProfileLangID, LangID); reg.WriteString(SRegValue_LanguageProfileLocale, BCP47Tag); reg.WriteString(SRegValue_LanguageProfileName, LanguageName); if not reg.ValueExists(SRegValue_KeymanProfileGUID) then begin CreateGuid(&guid); reg.WriteString(SRegValue_KeymanProfileGUID, GuidToString(guid)); end else guid := StringToGuid(reg.ReadString(SRegValue_KeymanProfileGUID)); if IconFileName = '' then // I4555 begin IconFileName := TKeymanPaths.KeymanEngineInstallPath(TKeymanPaths.S_KeymanExe); IconIndex := 1; end else IconIndex := 0; pInputProcessorProfileMgr := GetInputProcessorProfileMgr; try OleCheck(pInputProcessorProfileMgr.RegisterProfile( // I3743 c_clsidKMTipTextService, LangID, guid, PWideChar(KeyboardName), Length(KeyboardName), PWideChar(IconFileName), Length(IconFileName), IconIndex, 0, 0, 0, 0)); except on E:EOleSysError do begin ErrorFmt(KMN_E_ProfileInstall_RegisterProfileFailed, VarArrayOf([E.Message, E.ErrorCode])); end; end; finally reg.Free; end; Context.Control.AutoApplyKeyman; end; procedure TKPInstallKeyboardLanguage.RegisterTransientTips(const KeyboardID, KeyboardName: string; IconFileName: string); var FIsAdmin: Boolean; RootPath, FKeyboardName: string; guid: TGUID; i, LangID: Integer; IconIndex: Integer; reg: TRegistry; pInputProcessorProfileMgr: ITfInputProcessorProfileMgr; begin if not KeyboardInstalled(KeyboardID, FIsAdmin) then ErrorFmt(KMN_E_ProfileInstall_KeyboardNotFound, VarArrayOf([KeyboardID])); // I3888 if KeyboardName = '' then FKeyboardName := KeyboardID + Ext_KeymanFile else FKeyboardName := KeyboardName; if IconFileName = '' then // I4555 begin IconFileName := TKeymanPaths.KeymanEngineInstallPath(TKeymanPaths.S_KeymanExe); IconIndex := 1; end else IconIndex := 0; pInputProcessorProfileMgr := GetInputProcessorProfileMgr; for i := 0 to 3 do begin LangID := i * $400 + $2000; reg := TRegistry.Create; try reg.RootKey := HKEY_LOCAL_MACHINE; RootPath := GetRegistryKeyboardInstallKey_LM(KeyboardID) + SRegSubKey_TransientLanguageProfiles; if not reg.OpenKey(RootPath + '\' + IntToHex(LangID, 4), True) then ErrorFmt(KMN_E_ProfileInstall_MustBeAllUsers, VarArrayOf([KeyboardID])); reg.WriteInteger(SRegValue_LanguageProfileLangID, LangID); if not reg.ValueExists(SRegValue_KeymanProfileGUID) then begin CreateGuid(&guid); reg.WriteString(SRegValue_KeymanProfileGUID, GuidToString(guid)); end else guid := StringToGuid(reg.ReadString(SRegValue_KeymanProfileGUID)); try OleCheck(pInputProcessorProfileMgr.RegisterProfile( // I3743 c_clsidKMTipTextService, LangID, guid, PWideChar(KeyboardName), Length(KeyboardName), PWideChar(IconFileName), Length(IconFileName), IconIndex, 0, 0, 0, 0)); except on E:EOleSysError do begin ErrorFmt(KMN_E_ProfileInstall_RegisterProfileFailed, VarArrayOf([E.Message, E.ErrorCode])); end; end; finally reg.Free; end; end; Context.Control.AutoApplyKeyman; end; procedure TKPInstallKeyboardLanguage.InstallTip(const KeyboardID, BCP47Tag: string; LangID: Integer); var FIsAdmin: Boolean; guid: TGUID; reg: TRegistry; RootPath, FLayoutInstallString: string; begin if not KeyboardInstalled(KeyboardID, FIsAdmin) then ErrorFmt(KMN_E_ProfileInstall_KeyboardNotFound, VarArrayOf([KeyboardID])); // I3888 reg := TRegistry.Create(KEY_READ); try reg.RootKey := HKEY_LOCAL_MACHINE; if IsTransientLanguageID(LangID) then begin RootPath := GetRegistryKeyboardInstallKey_LM(KeyboardID) + SRegSubKey_TransientLanguageProfiles; if not reg.OpenKeyReadOnly(RootPath + '\' + IntToHex(LangID, 4)) then ErrorFmt(KMN_E_ProfileInstall_ProfileNotFound, VarArrayOf([KeyboardID, IntToHex(LangID, 4)])); end else begin RootPath := GetRegistryKeyboardInstallKey_LM(KeyboardID) + SRegSubKey_LanguageProfiles; if not reg.OpenKeyReadOnly(RootPath + '\' + BCP47Tag) then ErrorFmt(KMN_E_ProfileInstall_ProfileNotFound, VarArrayOf([KeyboardID, BCP47Tag])); end; if not reg.ValueExists(SRegValue_KeymanProfileGUID) then ErrorFmt(KMN_E_ProfileInstall_RegistryCorrupt, VarArrayOf([KeyboardID])); guid := StringToGuid(reg.ReadString(SRegValue_KeymanProfileGUID)); finally reg.Free; end; // // Install the TIP into Windows // FLayoutInstallString := GetLayoutInstallString(LangID, guid); if not InstallLayoutOrTip(PChar(FLayoutInstallString), 0) then // I4302 ErrorFmt(KMN_E_ProfileInstall_InstallLayoutOrTipFailed, VarArrayOf([KeyboardID])); end; function TKPInstallKeyboardLanguage.InstallBCP47Language(const FLocaleName: string): Boolean; var ScriptFile: TTempFile; FLogText: string; begin // Use PowerShell ScriptFile := TTempFileManager.Get('.ps1'); with TStringList.Create do try Add('$list = Get-WinUserLanguageList'); Add('$list.Add("'+FLocaleName+'")'); Add('Set-WinUserLanguageList $list -force'); SaveToFile(ScriptFile.Name); TUtilExecute.Console('powershell.exe -ExecutionPolicy Unrestricted "& ""'+ScriptFile.Name+'"""', ExtractFilePath(ScriptFile.Name), FLogText); finally Free; ScriptFile.Free; end; Result := True; end; procedure TKPInstallKeyboardLanguage.UninstallTemporaryLayout(const LayoutString: string); begin if not InstallLayoutOrTip(PChar(LayoutString), ILOT_UNINSTALL) then ErrorFmt(KMN_E_ProfileInstall_UninstallLayoutOrTipFailed, VarArrayOf([LayoutString])); end; end.