mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-08 15:07:33 +00:00
* Keyman for Windows 10.0 Open Source * Squashed 'windows/src/ext/jedi/jedi/' content from commit f444ad2 git-subtree-dir: windows/src/ext/jedi/jedi git-subtree-split: f444ad2da4693851e523f1ea6bd541f701904c24 * Squashed 'windows/src/ext/jedi/jcl/' content from commit d63d3c9fd git-subtree-dir: windows/src/ext/jedi/jcl git-subtree-split: d63d3c9fd9ff84efdd8159084ec6a60313644243 * Squashed 'windows/src/ext/jedi/jvcl/' content from commit bee19f3b4 git-subtree-dir: windows/src/ext/jedi/jvcl git-subtree-split: bee19f3b46909fde2fa92c06cd2706f41d99f6c3 * Add required .res files * Add required .res files * Add docbook files (forced) * Add required libxslt * Add required jedi files * Add installation files * Tweak .gitignore for open * CI * Remove KMW from Developer source (#122) * Remove KMW from Developer source (copies during build) * Remove KMW from Developer source (copies during build) * Remove KMW from Developer source (copies during build) * Fixup release build and copy license, readme from kmw during build * Remove obsolete build help documentation * Keyman Engine 10 on Windows regression for shift states (#129) * Improve #128 -- cleaner debug messages * Fixes #127, shift state now resets correctly; and more work for #128 * Fixes #130 (#131)
357 lines
9.6 KiB
ObjectPascal
357 lines
9.6 KiB
ObjectPascal
unit InstallCustomLanguageMain;
|
|
|
|
interface
|
|
|
|
function Run: Integer;
|
|
|
|
implementation
|
|
|
|
uses
|
|
Xml.XmlIntf,
|
|
Xml.XmlDoc,
|
|
keymanapi_TLB,
|
|
Winapi.ActiveX,
|
|
winapi.shlwapi,
|
|
Winapi.Windows,
|
|
System.Win.ComObj,
|
|
System.Win.Registry,
|
|
BCP47SuppressScriptRegistry,
|
|
TempFileManager,
|
|
Winapi.msctf,
|
|
utilexecute,
|
|
System.Classes,
|
|
System.SysUtils,
|
|
System.Variants;
|
|
|
|
var
|
|
kmcom: IKeyman = nil;
|
|
|
|
function ResolveLocaleName(nameToResolve, localeName: PWideChar; cchLocaleName: Integer): Integer; stdcall; external 'kernel32.dll' name 'ResolveLocaleName';
|
|
|
|
function ExtractBaseLanguageCode: string;
|
|
var
|
|
FScript: string;
|
|
FLocaleScript: string;
|
|
FLocaleName: string;
|
|
FScripts: TStringList;
|
|
function ExtractLangCode(t: string): string;
|
|
var
|
|
n: Integer;
|
|
begin
|
|
n := Pos('-', t);
|
|
if n = 0
|
|
then Result := t
|
|
else Result := Copy(t, 1, n-1);
|
|
end;
|
|
begin
|
|
FScripts := TStringList.Create;
|
|
with FScripts do
|
|
try
|
|
FScripts.Text := SuppressScriptSubtagRegistry;
|
|
|
|
FLocaleName := ExtractLangCode(ParamStr(2));
|
|
FScript := FScripts.Values[FLocaleName];
|
|
if FScript <> '' then
|
|
begin
|
|
FLocaleScript := FLocaleName + '-' + FScript;
|
|
if SameText(Copy(ParamStr(2), 1, Length(FLocaleScript)), FLocaleScript) then
|
|
FLocaleName := Copy(ParamStr(2), 1, Length(FLocaleName)) +
|
|
Copy(ParamStr(2), Length(FLocaleScript)+1, MaxInt)
|
|
else
|
|
FLocaleName := ParamStr(2);
|
|
end
|
|
else
|
|
FLocaleName := ParamStr(2);
|
|
finally
|
|
Free;
|
|
end;
|
|
|
|
Result := FLocaleName;
|
|
end;
|
|
|
|
function PowerShell(command: string): string;
|
|
begin
|
|
TUtilExecute.Console('powershell.exe -ExecutionPolicy Unrestricted -Command "& {'+command+'}"', ExtractFilePath(ParamStr(0)), Result);
|
|
end;
|
|
|
|
function FindBCP47Language(FLocaleName: string): Cardinal;
|
|
type
|
|
TLanguageTagLCID = record
|
|
LanguageTag: string;
|
|
Autonym: string;
|
|
EnglishName: string;
|
|
LocalizedName: string;
|
|
ScriptName: string;
|
|
LCID: Cardinal;
|
|
end;
|
|
var
|
|
i, j: Integer;
|
|
FXMLDoc: IXMLDocument;
|
|
List: IXMLNode;
|
|
Language: IXMLNode;
|
|
LanguageTags: array of TLanguageTagLCID;
|
|
Node: IXMLNode;
|
|
Name: string;
|
|
begin
|
|
FXMLDoc := LoadXMLData(Powershell('Get-WinUserLanguageList | ConvertTo-Xml -As String'));
|
|
List := FXMLDoc.DocumentElement.ChildNodes[0];
|
|
SetLength(LanguageTags, List.ChildNodes.Count);
|
|
for i := 0 to List.ChildNodes.Count - 1 do
|
|
begin
|
|
Language := List.ChildNodes[i];
|
|
LanguageTags[i].LanguageTag := '';
|
|
LanguageTags[i].LCID := 0;
|
|
for j := 0 to Language.ChildNodes.Count - 1 do
|
|
begin
|
|
Node := Language.ChildNodes[j];
|
|
Name := Node.Attributes['Name'];
|
|
if Name = 'LanguageTag' then
|
|
LanguageTags[i].LanguageTag := VarToStr(Node.NodeValue)
|
|
else if Name = 'Autonym' then
|
|
LanguageTags[i].Autonym := VarToStr(Node.NodeValue)
|
|
else if Name = 'EnglishName' then
|
|
LanguageTags[i].EnglishName := VarToStr(Node.NodeValue)
|
|
else if Name = 'LocalizedName' then
|
|
LanguageTags[i].LocalizedName := VarToStr(Node.NodeValue)
|
|
else if Name = 'ScriptName' then
|
|
LanguageTags[i].ScriptName := VarToStr(Node.NodeValue)
|
|
else if Name = 'InputMethodTips' then
|
|
begin
|
|
if Node.ChildNodes.Count > 0 then
|
|
LanguageTags[i].LCID := StrToInt('$'+Copy(Node.ChildNodes[0].NodeValue, 1, 4));
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
for i := Low(LanguageTags) to High(LanguageTags) do
|
|
if SameText(Copy(LanguageTags[i].LanguageTag, 1, Length(FLocaleName)), FLocaleName) then
|
|
Exit(LanguageTags[i].LCID);
|
|
|
|
Result := 0;
|
|
end;
|
|
|
|
type
|
|
TSHLoadIndirectString = function(const pszSource: PWideChar; pszOutBuf: PWideChar; cchOutBuf: UINT; ppvReserved: Pointer): HRESULT; stdcall;
|
|
|
|
var
|
|
hSHLWApi: THandle = 0;
|
|
FSHLoadIndirectString: TSHLoadIndirectString = nil;
|
|
|
|
function LoadIndirectString(const name: WideString): WideString;
|
|
var
|
|
buf: array[0..260] of WideChar;
|
|
begin
|
|
Result := '';
|
|
|
|
if hSHLWApi = 0 then
|
|
begin
|
|
hSHLWApi := LoadLibrary('shlwapi.dll');
|
|
if hSHLWApi = 0 then
|
|
Exit;
|
|
FSHLoadIndirectString := GetProcAddress(hSHLWApi, 'SHLoadIndirectString');
|
|
end;
|
|
|
|
if not Assigned(FSHLoadIndirectString) then Exit;
|
|
if FSHLoadIndirectString(PWideChar(name), buf, Length(buf), nil) = S_OK then
|
|
Result := buf;
|
|
end;
|
|
|
|
type
|
|
TRegistryHelper = class helper for TRegistry
|
|
procedure ReadMultiString(const ValueName: string; Strings: TStrings);
|
|
end;
|
|
|
|
procedure TRegistryHelper.ReadMultiString(const ValueName: string; Strings: TStrings);
|
|
var
|
|
valueType: DWORD;
|
|
valueLen: DWORD;
|
|
p, buffer: PChar;
|
|
begin
|
|
Strings.Clear;
|
|
|
|
if not CheckResult(RegQueryValueEx(CurrentKey, PChar(ValueName), nil, @valueType, nil, @valueLen)) then
|
|
raise ERegistryException.Create('Unable to find value '+ValueName);
|
|
|
|
if valueType <> REG_MULTI_SZ then
|
|
raise ERegistryException.Create('Value '+ValueName+' does not have type REG_MULTI_SZ');
|
|
|
|
if valueLen = 0 then
|
|
// Empty
|
|
Exit;
|
|
|
|
GetMem(buffer, valueLen + 1);
|
|
try
|
|
RegQueryValueEx(CurrentKey, PChar(ValueName), nil, nil, PBYTE(buffer), @valueLen);
|
|
buffer[valueLen] := #0;
|
|
|
|
p := buffer;
|
|
while p^ <> #0 do
|
|
begin
|
|
Strings.Add(p);
|
|
Inc(p, lstrlen(p) + 1);
|
|
end;
|
|
finally
|
|
FreeMem(buffer);
|
|
end;
|
|
end;
|
|
|
|
function FindBCP47Language2(FLocaleName: string): Cardinal;
|
|
type
|
|
TLanguageTagLCID = record
|
|
LanguageTag: string;
|
|
// Autonym: string;
|
|
// EnglishName: string;
|
|
LocalizedName: string;
|
|
// ScriptName: string;
|
|
TransientLangId, LCID: Cardinal;
|
|
end;
|
|
var
|
|
n, i, j: Integer;
|
|
Language: string;
|
|
LanguageTags: array of TLanguageTagLCID;
|
|
Name: string;
|
|
FValueNames: TStringList;
|
|
FLanguageNames: TStringList;
|
|
begin
|
|
with TRegistry.Create do
|
|
try
|
|
if OpenKeyReadOnly('Control Panel\International\User Profile') and ValueExists('Languages') then
|
|
begin
|
|
FLanguageNames := TStringList.Create;
|
|
FValueNames := TStringList.Create;
|
|
try
|
|
ReadMultiString('Languages', FLanguageNames);
|
|
|
|
SetLength(LanguageTags, FLanguageNames.Count);
|
|
for i := 0 to FLanguageNames.Count - 1 do
|
|
begin
|
|
Language := FLanguageNames[i];
|
|
LanguageTags[i].LanguageTag := Language;
|
|
if OpenKeyReadOnly('\Control Panel\International\User Profile\'+Language) then
|
|
begin
|
|
if ValueExists('TransientLangId') then
|
|
LanguageTags[i].TransientLangId := ReadInteger('TransientLangId');
|
|
if ValueExists('CachedLanguageName') then
|
|
LanguageTags[i].LocalizedName := LoadIndirectString(ReadString('CachedLanguageName'));
|
|
FValueNames.Clear;
|
|
GetValueNames(FValueNames);
|
|
for j := 0 to FValueNames.Count - 1 do
|
|
if not SameText(FValueNames[j], 'TransientLangId') and (GetDataType(FValueNames[j]) = rdInteger) then
|
|
begin
|
|
if Copy(FValueNames[j],5,1)=':' then
|
|
LanguageTags[i].LCID := StrToIntDef('$'+Copy(FValueNames[j],1,4),0);
|
|
end;
|
|
end;
|
|
end;
|
|
finally
|
|
FLanguageNames.Free;
|
|
FValueNames.Free;
|
|
end;
|
|
end;
|
|
finally
|
|
Free;
|
|
end;
|
|
|
|
for i := Low(LanguageTags) to High(LanguageTags) do
|
|
if SameText(Copy(LanguageTags[i].LanguageTag, 1, Length(FLocaleName)), FLocaleName) then
|
|
Exit(LanguageTags[i].LCID);
|
|
|
|
Result := 0;
|
|
end;
|
|
|
|
function InstallBCP47Language(FLocaleName: string): Boolean;
|
|
var
|
|
ScriptFilename: string;
|
|
begin
|
|
// Use PowerShell
|
|
ScriptFilename := TTempFileManager.Get('.ps1').Name;
|
|
|
|
with TStringList.Create do
|
|
try
|
|
Add('$list = Get-WinUserLanguageList');
|
|
Add('$list.Add("'+FLocaleName+'")');
|
|
Add('Set-WinUserLanguageList $list -force');
|
|
SaveToFile(ScriptFilename);
|
|
finally
|
|
Free;
|
|
end;
|
|
|
|
TUtilExecute.WaitForProcess('powershell.exe -ExecutionPolicy Unrestricted "& ""'+ScriptFilename+'"""', ExtractFilePath(ScriptFilename));
|
|
Result := True;
|
|
end;
|
|
|
|
function RemoveDefaultKeyboardForLanguage(FLocaleName: string): Boolean;
|
|
var
|
|
ScriptFilename: string;
|
|
begin
|
|
ScriptFilename := TTempFileManager.Get('.ps1').Name;
|
|
|
|
with TStringList.Create do
|
|
try
|
|
Add('$list = Get-WinUserLanguageList');
|
|
Add('$item = $list | Where-Object {$_.LanguageTag -like "'+FLocaleName+'"}');
|
|
Add('$result = $item.InputMethodTips.RemoveAll({ param($m) $m -match "[0-9a-z]{4}:[0-9a-z]{8}" })');
|
|
Add('Set-WinUserLanguageList $list -force');
|
|
SaveToFile(ScriptFilename);
|
|
TUtilExecute.WaitForProcess('powershell.exe -ExecutionPolicy Unrestricted "& ""'+ScriptFilename+'"""', ExtractFilePath(ScriptFilename));
|
|
finally
|
|
Free;
|
|
end;
|
|
Result := True;
|
|
end;
|
|
|
|
function Run: Integer;
|
|
var
|
|
Code, n: Integer;
|
|
FLocaleName: string;
|
|
begin
|
|
if ParamCount < 2 then
|
|
begin
|
|
writeln('InstallCustomLanguage <KeyboardName> <BCP47Code>');
|
|
Exit(1);
|
|
end;
|
|
|
|
FLocaleName := ExtractBaseLanguageCode;
|
|
|
|
kmcom := CoKeyman.Create;
|
|
|
|
n := kmcom.Keyboards.IndexOf(ParamStr(1));
|
|
if n < 0 then
|
|
begin
|
|
writeln('Keyboard '+ParamStr(1)+' not found.');
|
|
Exit(3);
|
|
end;
|
|
|
|
Code := FindBCP47Language2(FLocaleName);
|
|
|
|
if Code = 0 then
|
|
begin
|
|
writeln('Installing language and keyboard');
|
|
if not InstallBCP47Language(FLocaleName) then
|
|
begin
|
|
writeln('Language '+ParamStr(2)+' was not successfully installed.');
|
|
Exit(4);
|
|
end;
|
|
|
|
Code := FindBCP47Language2(FLocaleName);
|
|
if Code = 0 then
|
|
begin
|
|
writeln('Language '+ParamStr(2)+' was installed but could not be resolved.');
|
|
Exit(5);
|
|
end;
|
|
|
|
kmcom.Keyboards[n].Languages.InstallByLangID(Code);
|
|
|
|
writeln('Removing default keyboard');
|
|
RemoveDefaultKeyboardForLanguage(FLocaleName);
|
|
end
|
|
else
|
|
begin
|
|
writeln('Adding keyboard to already installed language');
|
|
kmcom.Keyboards[n].Languages.InstallByLangID(Code);
|
|
end;
|
|
|
|
Exit(0);
|
|
end;
|
|
|
|
end.
|