spiegel-keyman/common/windows/delphi/general/KeymanPaths.pas
Marc Durdin 34d6eb89fa fix(windows): Ensure default locale always appears in list of locales
* Add `TKeymanPaths.KeymanLocalePath` for consistent access to locales
  folder, and update references.
* Remove the `SKDefaultLanguageCode` string and hard code the default
  language in CustomisationMessages.pas.
* Hide the test locale 'qqq' on release builds.

Fixes: #15164
2026-05-20 15:51:25 +02:00

498 lines
15 KiB
ObjectPascal

unit KeymanPaths;
interface
uses
System.SysUtils;
type
EKeymanPath = class(Exception);
TKeymanPaths = class
private
const S_CEF_DebugPath = 'Debug_CEFPath';
const S_CEF_EnvVar = 'KEYMAN_CEF4DELPHI_ROOT';
const S_CEF_SubFolder = 'cef\';
const S_CEF_LibCef = 'libcef.dll';
const S_CEF_SubProcess = 'kmbrowserhost.exe';
const S_CEF_SubProcess_Developer = 'kmdbrowserhost.exe';
const S_CustomisationFilename = 'desktop_pro.pxx';
const S_KeymanAppData_UpdateCache = 'Keyman\UpdateCache\';
class var FHaveCachedKeymanRoot: Boolean;
class var FCachedKeymanRoot: string;
public
const S_KMShell = 'kmshell.exe';
const S_TSysInfoExe = 'tsysinfo.exe';
const S_KeymanExe = 'keyman.exe';
const S_CfgIcon = 'cfgicon.ico';
const S_AppIcon = 'appicon.ico';
const S_KeymanMenuTitleImage = 'menutitle.png';
const S_FallbackKeyboardPath = 'Keyboards\';
const S__Package = '_Package\';
const S_MCompileExe = 'mcompile.exe';
const S_UpdateCache_Metadata = 'cache.json';
class function ErrorLogPath(const app: string = ''): string; static;
class function KeymanHelpPath(const HelpFile: string): string; static;
class function KeymanUpdateCachePath(const filename: string = ''): string; static;
class function KeymanDesktopInstallPath(const filename: string = ''): string; static;
class function KeymanEngineInstallPath(const filename: string = ''): string; static;
class function KeymanDesktopInstallDir: string; static;
class function KeymanEngineInstallDir: string; static;
class function KeyboardsInstallPath(const filename: string = ''): string; static;
class function KeyboardsInstallDir: string; static;
class function KeymanConfigStaticHttpFilesPath(const filename: string = ''): string; static;
class function KeymanCustomisationPath: string; static;
class function KeymanLocalePath: string; static;
class function KeymanCoreLibraryPath(const Filename: string): string; static;
class function CEFPath: string; static; // Chromium Embedded Framework
class function CEFDataPath(const mode: string): string; static;
class function CEFSubprocessPath(IsDeveloper: Boolean): string; static;
class function RunningFromSource(var keyman_root: string): Boolean; static;
end;
function GetFolderPath(csidl: Integer): string;
implementation
uses
Winapi.Windows,
Winapi.ActiveX,
Winapi.ShlObj,
System.Classes,
System.Win.Registry,
DebugPaths,
RegistryKeys;
// Duplicated from utilsystem to avoid importing other units
function GetFolderPath(csidl: Integer): string;
var
buf: array[0..260] of Char;
idl: PItemIDList;
begin
Result := '';
if SUCCEEDED(SHGetFolderLocation(0, csidl, 0, 0, idl)) then
begin
if SHGetPathFromIDList(idl, buf) then
Result := buf;
ILFree(idl);
end;
if (Result = '') and (csidl = CSIDL_PROGRAM_FILES) then
with TRegistry.Create do // I2890
try
RootKey := HKEY_LOCAL_MACHINE;
if not OpenKeyReadOnly('Software\Microsoft\Windows\CurrentVersion') then // I2890
RaiseLastOSError(LastError);
Result := ReadString('ProgramFilesDir');
finally
Free;
end;
if Result <> '' then
if Result[Length(Result)] <> '\' then Result := Result + '\';
end;
class function TKeymanPaths.KeymanDesktopInstallDir: string;
begin
Result := ExtractFileDir(KeymanDesktopInstallPath);
end;
type
TRegSzHelper = class helper for TRegistry
function ReadMultiSz(const name: string; strings: TStrings): Boolean;
end;
function TRegSzHelper.ReadMultiSz(const name: string; Strings: TStrings): boolean;
var
iSizeInWChar, iSizeInByte: integer;
Buffer: array of WChar;
p, EndBuffer: PWChar;
begin
iSizeInByte := GetDataSize(name);
iSizeInWChar := iSizeInByte div SizeOf(WChar);
if iSizeInByte > 0 then
begin
SetLength(Buffer, iSizeInWChar);
iSizeInWChar := ReadBinaryData(name, Buffer[0], iSizeInByte) div sizeof(WChar);
p := @Buffer[0];
EndBuffer := p;
Inc(EndBuffer, iSizeInWChar);
while p < EndBuffer do
begin
Strings.Append(p);
p := StrScan(p, #0);
Inc(p);
end;
Result := True;
end
else
Result := False;
end;
class function TKeymanPaths.KeymanDesktopInstallPath(const filename: string = ''): string;
var
s: TStrings;
begin
//
// If the current executable is kmshell.exe, then return the folder it is in (unless
// a debug path overrides it).
//
// Otherwise, Keyman takes precedence if installed. If it is not installed,
// check the installed OEM product registry and the first installed OEM product is
// used. Currently, I assess that the likelihood of two OEM products being installed
// without Keyman being installed is low, and we can live with the first one taking
// precedence.
//
// Finally, if no OEM product can be found, then abort.
//
Result := '';
if SameText(ExtractFileName(ParamStr(0)), TKeymanPaths.S_KMShell) then
begin
// Just for kmshell.exe, use the current folder
Result := ExtractFilePath(ParamStr(0));
end;
if Result = '' then
begin
// If Keyman is installed, then use its install path.
with TRegistry.Create do // I2890
try
RootKey := HKEY_LOCAL_MACHINE;
if OpenKeyReadOnly(SRegKey_KeymanDesktop_LM) and ValueExists(SRegValue_RootPath) then
Result := ReadString(SRegValue_RootPath);
finally
Free;
end;
end;
if Result = '' then
begin
// Look in the registry for any installed OEM product; this is a multistring,
// but we only want the first value
with TRegistry.Create do
try
RootKey := HKEY_LOCAL_MACHINE;
if OpenKeyReadOnly(SRegKey_KeymanEngine_LM) and ValueExists(SRegValue_Engine_OEMProductPath) then
begin
s := TStringList.Create;
try
ReadMultiSz(SRegValue_Engine_OEMProductPath, s);
if s.Count > 0 then
Result := s[0];
finally
s.Free;
end;
end;
finally
Free;
end;
end;
Result := GetDebugPath('KeymanDesktop', Result);
if Result = '' then
raise EKeymanPath.Create('Unable to find the Keyman directory. You should reinstall the product.');
Result := IncludeTrailingPathDelimiter(Result) + filename;
end;
class function TKeymanPaths.KeymanEngineInstallDir: string;
begin
Result := ExtractFileDir(KeymanEngineInstallPath);
end;
class function TKeymanPaths.KeymanEngineInstallPath(const filename: string = ''): string;
begin
with TRegistry.Create do // I2890
try
RootKey := HKEY_LOCAL_MACHINE;
if OpenKeyReadOnly(SRegKey_KeymanEngine_LM) and ValueExists(SRegValue_RootPath) then
Result := ReadString(SRegValue_RootPath);
finally
Free;
end;
Result := GetDebugPath('KeymanEngine', Result);
if Result = '' then
raise EKeymanPath.Create('Unable to find the Keyman Engine directory. You should reinstall the product.');
Result := IncludeTrailingPathDelimiter(Result) + filename;
end;
class function TKeymanPaths.CEFPath: string;
function AppendSlash(const path: string): string;
begin
Result := path;
if Result <> '' then
Result := IncludeTrailingPathDelimiter(path);
end;
function IsValidPath(const path: string): Boolean;
begin
Result := (path <> '') and FileExists(path + S_CEF_LibCef);
end;
begin
// cef\ subfolder of executable path
Result := ExtractFilePath(ParamStr(0)) + S_CEF_SubFolder;
if IsValidPath(Result) then
Exit;
// Debug_CEFPath registry setting
Result := AppendSlash(GetDebugPath(S_CEF_DebugPath, ''));
if IsValidPath(Result) then
Exit;
// KEYMAN_CEF4DELPHI_ROOT environment variable
Result := AppendSlash(GetEnvironmentVariable(S_CEF_EnvVar));
if IsValidPath(Result) then
Exit;
// Same folder as executable
Result := ExtractFilePath(ParamStr(0));
if IsValidPath(Result) then
Exit;
// Keyman Desktop installation folder + cef\
try
Result := KeymanDesktopInstallPath+S_CEF_SubFolder
except
on E:EKeymanPath do
Result := '';
end;
if IsValidPath(Result) then
Exit;
// Failed, could not find libcef.dll
Result := '';
end;
class function TKeymanPaths.CEFSubprocessPath(IsDeveloper: Boolean): string;
var
keyman_root: string;
begin
// Same folder as executable
// TODO: make this a little cleaner by passing in expected subprocess name
if IsDeveloper
then Result := ExtractFilePath(ParamStr(0)) + S_CEF_SubProcess_Developer
else Result := ExtractFilePath(ParamStr(0)) + S_CEF_SubProcess;
if FileExists(Result) then Exit;
// On developer machines, if we are running within the source repo, then use
// those paths
if TKeymanPaths.RunningFromSource(keyman_root) then
begin
// Source repo, bin folder
Result := keyman_root + 'windows\bin\desktop\' + S_CEF_SubProcess;
if FileExists(Result) then Exit;
// Source repo, source folder
Result := keyman_root + 'windows\bin\desktop\kmbrowserhost\win32\debug\' + S_CEF_SubProcess;
if FileExists(Result) then Exit;
Result := keyman_root + 'windows\bin\desktop\kmbrowserhost\win32\release\' + S_CEF_SubProcess;
if FileExists(Result) then Exit;
end;
// Check final install location - in Keyman for Windows install folder
try
Result := KeymanDesktopInstallPath(S_CEF_SubProcess);
except
on E:EKeymanPath do
Result := '';
end;
if (Result <> '') and FileExists(Result) then Exit;
Result := '';
end;
class function TKeymanPaths.CEFDataPath(const mode: string): string;
begin
Result := GetFolderPath(CSIDL_APPDATA) + SFolderCEFBrowserData + '\' + mode;
end;
class function TKeymanPaths.KeyboardsInstallDir: string;
begin
Result := ExcludeTrailingPathDelimiter(KeyboardsInstallPath);
end;
class function TKeymanPaths.KeyboardsInstallPath(const filename: string): string;
begin
with TRegistry.Create do // I2890
try
// TODO: We only want to use %CommonAppData%\Keyman now for all keyboards
// So we should eliminate use of RootKeyboardAdminPath and S_FallbackKeyboardPath
RootKey := HKEY_LOCAL_MACHINE;
if OpenKeyReadOnly(SRegKey_KeymanEngine_LM) and ValueExists(SRegValue_RootKeyboardAdminPath) then
begin
Result := ReadString(SRegValue_RootKeyboardAdminPath)
end
else
begin
Result := GetFolderPath(CSIDL_COMMON_APPDATA);
if Result = '' then
Result := TKeymanPaths.KeymanEngineInstallPath(TKeymanPaths.S_FallbackKeyboardPath)
else
Result := Result + SFolderKeymanKeyboard;
end;
finally
Free;
end;
Result := GetDebugPath('Keyboards', Result, True);
if Result = '' then
raise EKeymanPath.Create('Unable to find the Keyboards directory. You should reinstall the product.');
Result := Result + Filename;
end;
class function TKeymanPaths.ErrorLogPath(const app: string): string;
begin
Result := GetFolderPath(CSIDL_LOCAL_APPDATA) + SFolderKeymanEngineDiag + '\';
ForceDirectories(Result); // I2768
if app <> '' then
Result := Result + app + '-' + IntToStr(GetCurrentProcessId) + '.log';
end;
class function TKeymanPaths.KeymanConfigStaticHttpFilesPath(const filename: string): string;
var
keyman_root: string;
begin
// Look up KEYMAN_ROOT development variable -- if found and executable
// within that path then use that as source path
if TKeymanPaths.RunningFromSource(keyman_root) then
begin
Exit(keyman_root + 'windows\src\desktop\kmshell\xml\' + filename);
end;
Result := GetDebugPath('KeymanConfigStaticHttpFilesPath', '');
if Result = '' then
begin
Result := ExtractFilePath(ParamStr(0));
// The xml files may be in the same folder as the executable
// for some 3rd party distributions of Keyman files.
if FileExists(Result + 'xml\strings.xml') then
Result := Result + 'xml\';
end;
Result := Result + filename;
end;
class function TKeymanPaths.KeymanLocalePath: string;
var
keyman_root: string;
begin
// Look up KEYMAN_ROOT development variable -- if found and executable
// within that path then use that as source path
if TKeymanPaths.RunningFromSource(keyman_root) then
begin
Exit(keyman_root + 'windows\src\desktop\kmshell\locale\');
end;
Result := TKeymanPaths.KeymanDesktopInstallPath('locale\');
end;
class function TKeymanPaths.KeymanCustomisationPath: string;
var
keyman_root: string;
begin
// On developer machines, if we are running within the source repo, then use
// those paths
keyman_root := GetEnvironmentVariable('KEYMAN_ROOT').Replace('/', '\');
if (keyman_root <> '') and SameText(keyman_root, ParamStr(0).Substring(0, keyman_root.Length)) then
begin
// Source repo, bin folder
Result := IncludeTrailingPathDelimiter(keyman_root) + 'windows\src\desktop\branding';
if DirectoryExists(Result) then
Exit(Result + '\' + S_CustomisationFilename);
end;
Result := TKeymanPaths.KeymanDesktopInstallPath(S_CustomisationFilename);
end;
class function TKeymanPaths.KeymanHelpPath(const HelpFile: string): string;
var
keyman_root: string;
begin
// On developer machines, if we are running within the source repo, then use
// those paths
if TKeymanPaths.RunningFromSource(keyman_root) then
begin
// Source repo, bin folder
Result := keyman_root + 'windows\bin\desktop\' + HelpFile;
if FileExists(Result) then Exit;
end;
// Same folder as executable
Result := ExtractFilePath(ParamStr(0)) + HelpFile;
if FileExists(Result) then Exit;
Result := TKeymanPaths.KeymanDesktopInstallPath(HelpFile);
if FileExists(Result) then Exit;
Result := '';
end;
class function TKeymanPaths.KeymanUpdateCachePath(const filename: string): string;
begin
Result := GetFolderPath(CSIDL_LOCAL_APPDATA) + S_KeymanAppData_UpdateCache + filename;
end;
class function TKeymanPaths.RunningFromSource(var keyman_root: string): Boolean;
begin
if FHaveCachedKeymanRoot then
begin
keyman_root := FCachedKeymanRoot;
Exit(keyman_root <> '');
end;
// On developer machines, if we are running within the source repo, then use
// those paths
keyman_root := GetEnvironmentVariable('KEYMAN_ROOT').Replace('/', '\', [rfReplaceAll]);
if keyman_root <> '' then
keyman_root := IncludeTrailingPathDelimiter(keyman_root);
Result := (keyman_root <> '') and SameText(keyman_root, ParamStr(0).Substring(0, keyman_root.Length));
FHaveCachedKeymanRoot := True;
if Result then
FCachedKeymanRoot := keyman_root;
end;
class function TKeymanPaths.KeymanCoreLibraryPath(const Filename: string): string;
var
keyman_root: string;
configuration: string;
begin
// Look up KEYMAN_ROOT development variable -- if found and executable
// within that path then use that as source path
if TKeymanPaths.RunningFromSource(keyman_root) then
begin
{$IFDEF DEBUG}
configuration := 'debug';
{$ELSE}
configuration := 'release';
{$ENDIF}
Exit(keyman_root + 'core\build\x86\'+configuration+'\src\' + Filename);
end;
Result := GetDebugPath('KeymanCoreLibraryPath', '');
if Result = '' then
begin
Result := ExtractFilePath(ParamStr(0));
end;
Result := IncludeTrailingPathDelimiter(Result) + Filename;
end;
end.