spiegel-keyman/developer/src/kmconvert/Keyman.Developer.System.ImportWindowsKeyboard.pas
Marc Durdin f8ce6c0ed1 fix(developer): handle failure to create destination path when generating new project
Add better error handling for situation where destination path cannot be
created, for all project types. Also report back the Windows error
message which hopefully clarifies why the directory could not be
created.

Fixes: #14348
Fixes: KEYMAN-DEVELOPER-1P1
Fixes: KEYMAN-DEVELOPER-2SN
Test-bot: skip
2025-08-30 06:15:11 +02:00

436 lines
13 KiB
ObjectPascal

unit Keyman.Developer.System.ImportWindowsKeyboard;
interface
uses
System.SysUtils,
Keyman.Developer.System.KeyboardProjectTemplate,
UKeymanTargets;
type
EImportWindowsKeyboard = class(Exception);
TImportWindowsKeyboard = class
private
FBaseKeyboardID: string;
FBaseName: string;
FProjectFilename: string;
{ Options }
FSourceKLID: string;
FDestinationPath: string;
FNameTemplate: string;
FAuthor: string;
FVersion: string;
FKeyboardIDTemplate: string;
FBCP47Tags: string;
FCopyright: string;
FTargets: TKeymanTargets;
FFullCopyright: string;
FDescription: string;
function LoadKLIDDetails: Boolean;
function ImportKeyboard(const DestinationFilename, DestinationKVKSFilename: string): Boolean;
function GenerateIcon(const IconFilename: string): Boolean;
procedure SetAuthor(const Value: string);
procedure SetCopyright(const Value: string);
procedure SetDestinationPath(const Value: string);
procedure SetKeyboardIDTemplate(const Value: string);
procedure SetBCP47Tags(const Value: string);
procedure SetNameTemplate(const Value: string);
procedure SetSourceKLID(const Value: string);
procedure SetVersion(const Value: string);
procedure InjectSystemStores(const KeyboardFilename, OSKFilename,
IconFilename, TouchLayoutFilename: string);
function ConvertOSKToTouchLayout(const OSKFilename, TouchLayoutFilename: string): Boolean;
function FindBCP47TagForKLID: string; overload;
function GetProjectFilename: string;
procedure SetTargets(const Value: TKeymanTargets);
procedure SetFullCopyright(const Value: string);
procedure SetDescription(const Value: string);
public
function Execute: Boolean; overload;
class function FindBCP47TagForKLID(KLID: string): string; overload;
class function GetKLIDDetails(const KLID: string; var Name,
KeyboardID: string): Boolean; static;
property SourceKLID: string read FSourceKLID write SetSourceKLID;
property DestinationPath: string read FDestinationPath write SetDestinationPath;
property KeyboardIDTemplate: string read FKeyboardIDTemplate write SetKeyboardIDTemplate;
property NameTemplate: string read FNameTemplate write SetNameTemplate;
property Copyright: string read FCopyright write SetCopyright;
property FullCopyright: string read FFullCopyright write SetFullCopyright;
property Version: string read FVersion write SetVersion;
property BCP47Tags: string read FBCP47Tags write SetBCP47Tags;
property Author: string read FAuthor write SetAuthor;
property Description: string read FDescription write SetDescription;
property Targets: TKeymanTargets read FTargets write SetTargets;
property ProjectFilename: string read GetProjectFilename;
end;
implementation
uses
System.Classes,
System.Win.Registry,
Winapi.Windows,
BCP47Tag,
Keyman.Developer.System.ImportKeyboardDLL,
Keyman.Developer.System.GenerateKeyboardIcon,
Keyman.Developer.System.VisualKeyboardToTouchLayoutConverter,
Keyman.System.KeyboardUtils,
KeymanVersion,
KeyboardParser,
kmxfileconsts,
RegistryKeys,
TempFileManager,
utilfiletypes;
{ TImportWindowsKeyboard }
function Fail(m: string): Boolean;
begin
Result := False;
writeln(m);
end;
function TImportWindowsKeyboard.LoadKLIDDetails: Boolean;
begin
Result := GetKLIDDetails(FSourceKLID, FBaseName, FBaseKeyboardID);
end;
class function TImportWindowsKeyboard.GetKLIDDetails(const KLID: string; var Name, KeyboardID: string): Boolean;
var
r: TRegistry;
begin
r := TRegistry.Create;
try
r.RootKey := HKEY_LOCAL_MACHINE;
if not r.OpenKeyReadOnly(SRegKey_KeyboardLayouts_LM + '\' + KLID) then
Exit(False);
if not r.ValueExists(SRegValue_KeyboardLayoutFile) or not
r.ValueExists(SRegValue_KeyboardLayoutText) then
Exit(False);
KeyboardID := TKeyboardUtils.CleanKeyboardID(ChangeFileExt(r.ReadString(SRegValue_KeyboardLayoutFile), ''));
Name := r.ReadString(SRegValue_KeyboardLayoutText);
finally
r.Free;
end;
Result := True;
end;
function TImportWindowsKeyboard.GetProjectFilename: string;
var
FTemplate: TKeyboardProjectTemplate;
begin
if FProjectFilename = '' then
begin
// Lookup the KLID in the registry and read basic details
if not LoadKLIDDetails then
raise EImportWindowsKeyboard.Create('The keyboard identified by '+FSourceKLID+' could not be found.');
FTemplate := TKeyboardProjectTemplate.Create(FDestinationPath, Format(FKeyboardIDTemplate, [FBaseKeyboardID]), KMXKeymanTargets + [ktWeb]);
try
Result := FTemplate.ProjectFilename;
finally
FTemplate.Free;
end;
end
else
Result := FProjectFIlename;
end;
procedure TImportWindowsKeyboard.SetAuthor(const Value: string);
begin
FAuthor := Value;
end;
procedure TImportWindowsKeyboard.SetCopyright(const Value: string);
begin
FCopyright := Value;
end;
procedure TImportWindowsKeyboard.SetDescription(const Value: string);
begin
FDescription := Value;
end;
procedure TImportWindowsKeyboard.SetDestinationPath(const Value: string);
begin
FDestinationPath := IncludeTrailingPathDelimiter(Value);
end;
procedure TImportWindowsKeyboard.SetFullCopyright(const Value: string);
begin
FFullCopyright := Value;
end;
procedure TImportWindowsKeyboard.SetKeyboardIDTemplate(const Value: string);
begin
if Value = ''
then FKeyboardIDTemplate := '%s'
else FKeyboardIDTemplate := Value;
end;
procedure TImportWindowsKeyboard.SetBCP47Tags(const Value: string);
begin
FBCP47Tags := Value;
end;
procedure TImportWindowsKeyboard.SetNameTemplate(const Value: string);
begin
if Value = ''
then FNameTemplate := '%s'
else FNameTemplate := Value;
end;
procedure TImportWindowsKeyboard.SetSourceKLID(const Value: string);
begin
FSourceKLID := Value;
end;
procedure TImportWindowsKeyboard.SetTargets(const Value: TKeymanTargets);
begin
FTargets := Value;
end;
procedure TImportWindowsKeyboard.SetVersion(const Value: string);
begin
FVersion := Value;
end;
function TImportWindowsKeyboard.Execute: Boolean;
var
FTemplate: TKeyboardProjectTemplate;
DestinationOSKTempFile: TTempFile;
DestinationOSKFilename: string;
begin
if not ForceDirectories(FDestinationPath) then
Exit(Fail('The destination path '+FDestinationPath+' could not be created.'));
// Lookup the KLID in the registry and read basic details
if not LoadKLIDDetails then
Exit(Fail('The keyboard identified by '+FSourceKLID+' could not be found.'));
// Create a new folder in destination path
DestinationOSKTempFile := nil;
FTemplate := TKeyboardProjectTemplate.Create(FDestinationPath, Format(FKeyboardIDTemplate, [FBaseKeyboardID]), FTargets);
try
//
// These parameters apply to .kmn and .kps so set them even though
// ImportKeyboard will overwrite the template .kmn
//
FTemplate.Name := Format(FNameTemplate, [FBaseName]);
FTemplate.Copyright := FCopyright;
FTemplate.FullCopyright := FFullCopyright;
FTemplate.Author := FAuthor;
FTemplate.Description := FDescription;
FTemplate.Version := FVersion;
FTemplate.IncludeIcon := True;
//
// Set languages in package
//
if FBCP47Tags = '' then
FBCP47Tags := FindBCP47TagForKLID;
FTemplate.BCP47Tags := FBCP47Tags;
//
// Creates a .kmn, .kvk, .ico, .kps and any other generic files
//
try
FTemplate.Generate;
except
on E:EKeyboardProjectTemplate do
Exit(Fail('Unable to generate template: '+E.Message));
on E:EFOpenError do
Exit(Fail('Unable to create project; template files may be missing: '+E.Message));
end;
FProjectFilename := FTemplate.ProjectFilename;
if FTemplate.OSKFilename = '' then
begin
// We need an OSK file in order to generate the touch layout. OSKFilename
// will be empty if the user selects a touch-only target. So we'll use a
// temp file in this scenario.
DestinationOSKTempFile := TTempFileManager.Get(Ext_VisualKeyboardSource);
DestinationOSKFilename := DestinationOSKTempFile.Name;
end
else
DestinationOSKFilename := FTemplate.OSKFilename;
// Run importkeyboard into destination file; this replaces the keyboard template
// file that has been generated
if not ImportKeyboard(FTemplate.KeyboardFilename, DestinationOSKFilename) then
Exit(Fail('Unable to run importkeyboard on '+FSourceKLID));
// Replace .ico with a new one based on the language id
// TODO: this goes in the keyboard project template generation I think
if FTemplate.IconFilename <> '' then
if not GenerateIcon(FTemplate.IconFilename) then
Exit(Fail('Unable to generate an icon for '+FTemplate.KeyboardFilename));
// Load the source .kmn and add bitmap, copyright, visualkeyboard, touch layout fields
// note, we use FTemplate.OSKFilename here deliberately, because we only
// generate OSK-related fields if they are in use
InjectSystemStores(FTemplate.KeyboardFilename, FTemplate.OSKFilename, FTemplate.IconFilename, FTemplate.TouchLayoutFilename);
if FTemplate.TouchLayoutFilename <> '' then
begin
// Take the generated OSK and convert it into a default touch layout
if not ConvertOSKToTouchLayout(DestinationOSKFilename, FTemplate.TouchLayoutFilename) then
Exit(Fail('Unable to create a default touch layout based on the OSK for '+FTemplate.KeyboardFilename));
end;
finally
FreeAndNil(FTemplate);
FreeAndNil(DestinationOSKTempFile);
end;
Result := True;
end;
class function TImportWindowsKeyboard.FindBCP47TagForKLID(KLID: string): string;
var
buf: array[0..8] of char;
FLanguageID: Word;
begin
FLanguageID := LOWORD(StrToInt('$'+KLID));
if GetLocaleInfo(FLanguageID, LOCALE_SISO639LANGNAME, buf, 8) > 0
then Result := buf
else Result := '';
end;
function TImportWindowsKeyboard.FindBCP47TagForKLID: string;
begin
Result := FindBCP47TagForKLID(FSourceKLID);
end;
procedure TImportWindowsKeyboard.InjectSystemStores(const KeyboardFilename, OSKFilename, IconFilename, TouchLayoutFilename: string);
var
kp: TKeyboardParser;
sl: TStringList;
begin
kp := TKeyboardParser.Create;
try
kp.FileName := KeyboardFilename;
kp.LoadFromFile(KeyboardFilename);
if IconFilename <> '' then
kp.Features.Add(kfIcon);
if OSKFilename <> '' then
kp.Features.Add(kfOSK);
if (TouchKeymanTargets+[ktAny]) * FTargets <> [] then
kp.Features.Add(kfTouchLayout);
// TODO: Are these file settings actually doing anything? Or is it controlled
// entirely by kp.Features.Add -- which could cause this to fall over a little
// if we change filenames for any reason in the future
kp.SetSystemStoreValue(ssName, Format(FNameTemplate, [FBaseName]));
kp.SetSystemStoreValue(ssVersion, SKeymanKeyboardVersion);
if OSKFilename <> '' then
kp.SetSystemStoreValue(ssVisualKeyboard, ExtractFileName(OSKFilename));
if IconFilename <> '' then
kp.SetSystemStoreValue(ssBitmap, ExtractFilename(IconFilename));
kp.SetSystemStoreValue(ssLayoutFile, ExtractFileName(TouchLayoutFilename));
kp.SetSystemStoreValue(ssTargets, KeymanTargetsToString(FTargets));
kp.SetSystemStoreValue(ssCopyright, FCopyright);
if FVersion <> '' then
kp.SetSystemStoreValue(ssKeyboardVersion, FVersion);
if FAuthor <> '' then
kp.InitialComment := kp.InitialComment + 'Run by: ' + FAuthor + #13#10;
sl := TStringList.Create;
try
sl.Text := kp.KeyboardText;
sl.SaveToFile(KeyboardFilename, TEncoding.UTF8);
finally
sl.Free;
end;
finally
kp.Free;
end;
end;
function TImportWindowsKeyboard.ImportKeyboard(const DestinationFilename, DestinationKVKSFilename: string): Boolean;
var
ik: TImportKeyboardDLL;
sl: TStringList;
ss: TStringStream;
begin
ik := TImportKeyboardDLL.Create(FSourceKLID);
try
try
ik.Execute;
except
on E:EImportKeyboardDLL do
begin
writeln(E.Message);
Exit(False);
end;
end;
// Still need a BOM for .kmn ... for now
sl := TStringList.Create;
try
sl.Text := ik.KMN;
sl.SaveToFile(DestinationFilename, TEncoding.UTF8);
finally
sl.Free;
end;
// No BOM for .kvks
ss := TStringStream.Create(ik.KVKS, TEncoding.UTF8);
try
ss.SaveToFile(DestinationKVKSFilename);
finally
ss.Free;
end;
finally
ik.Free;
end;
Result := True;
end;
function TImportWindowsKeyboard.GenerateIcon(
const IconFilename: string): Boolean;
begin
Result := TKeyboardIconGenerator.GenerateIcon(FBCP47Tags, IconFilename, 0);
end;
function TImportWindowsKeyboard.ConvertOSKToTouchLayout(const OSKFilename, TouchLayoutFilename: string): Boolean;
var
converter: TVisualKeyboardToTouchLayoutConverter;
FNewLayout: string;
ss: TStringStream;
begin
converter := TVisualKeyboardToTouchLayoutConverter.Create(OSKFilename);
try
if converter.Execute(FNewLayout) then
begin
ss := TStringStream.Create(FNewLayout, TEncoding.UTF8);
try
ss.SaveToFile(TouchLayoutFilename);
finally
ss.Free;
end;
end
else
Exit(False);
finally
converter.Free;
end;
Result := True;
end;
end.