<commit_before>unit Keyman.Configuration.System.TIPMaintenance;

interface

uses
  System.Classes,

  KeymanAPI_TLB;

type
  TTIPMaintenance = class
  public
    /// <summary>Install a TIP for the current user (user context)</summary>
    class function DoInstall(const KeyboardID, BCP47Tag: string): Boolean;

    /// <summary>Install a TIP for the current user (user context)</summary>
    class function InstallTip(LangID: Integer; const KeyboardID, BCP47Tag, KeyboardToRemove: string): Boolean;

    /// <summary>Install TIPs for packages for the current user (user context)</summary>
    class function InstallTipsForPackages(Packages: TStrings): Boolean; static;

    /// <summary>Install TIP for a package for the current user (user context)</summary>
    class function InstallTipForPackage(const PackageFilename, BCP47Tag: string): Boolean; static;

    /// <summary>Register TIP for the local machine (elevated context)</summary>
    class function DoRegister(const KeyboardID, BCP47Tag: string): Boolean;

    /// <summary>Register TIP for the local machine (elevated context)</summary>
    class function RegisterTip(LangID: Integer; const KeyboardID, BCP47Tag: string): Boolean;

    /// <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; overload; static;
    class procedure GetUserDefaultLanguage(var BCP47: string; var LangID: Integer); overload; static;

    /// <summary>Get the -default-lang parameter string for kmshell</summary>
    class function GetUserDefaultLangParameterString: string; static;
  private
    class function GetKeyboardLanguage(const KeyboardID,
      BCP47Tag: string): IKeymanKeyboardLanguageInstalled; static;
  end;

implementation

uses
  System.SysUtils,
  System.Win.Registry,
  Winapi.Windows,

  Keyman.System.LanguageCodeUtils,

  KLog,
  BCP47Tag,
  glossary,
  kmint,
  RegistryKeys,
  utilkmshell,
  utilexecute,
  utilsystem;

{ TTIPMaintenance }

class function TTIPMaintenance.InstallTipsForPackages(Packages: TStrings): Boolean;
var
  i: Integer;
begin
  Result := True;
  KL.MethodEnter(nil, 'TTIPMaintenance.InstallTipsForPackages', [Packages.Text]);
  try
    for i := 0 to Packages.Count - 1 do
      // We'll attempt every package but return failure if any of them have issues
      Result := InstallTipForPackage(Packages.Names[i], Packages.ValueFromIndex[i]) and Result;
  finally
    KL.MethodExit(nil, 'TTIPMaintenance.InstallTipsForPackages', [Result]);
  end;
end;

class function TTIPMaintenance.InstallTipForPackage(const PackageFilename, BCP47Tag: string): Boolean;
var
  PackageID: string;
  n: Integer;
  pack: IKeymanPackageInstalled;
begin
  Result := False;
  KL.MethodEnter(nil, 'TTIPMaintenance.InstallTipForPackage', [PackageFilename, BCP47Tag]);
  try
    // This function has a known limitation: if a package contains more than one keyboard,
    // then the BCP47 association will be made only for the first keyboard. This is
    // considered an acceptable limitation at this time

    PackageID := ChangeFileExt(ExtractFileName(PackageFilename), '');
    n := kmcom.Packages.IndexOf(PackageID);
    if n < 0 then
      Exit(False);

    pack := kmcom.Packages[n];

    if pack.Keyboards.Count = 0 then
      Exit(False);

    if BCP47Tag = ''
      then Result := DoInstall(pack.Keyboards[0].ID, GetFirstLanguage(pack.Keyboards[0] as IKeymanKeyboardInstalled))
      else Result := DoInstall(pack.Keyboards[0].ID, BCP47Tag);
  finally
    KL.MethodExit(nil, 'TTIPMaintenance.InstallTipForPackage', [Result]);
  end;
end;

class function TTIPMaintenance.InstallTip(LangID: Integer; const KeyboardID, BCP47Tag,
  KeyboardToRemove: string): Boolean;
var
  lang: IKeymanKeyboardLanguageInstalled;
begin
  Result := False;
  KL.MethodEnter(nil, 'TTIPMaintenance.InstallTip', [LangID, KeyboardID, BCP47Tag, KeyboardToRemove]);
  try
    lang := GetKeyboardLanguage(KeyboardID, BCP47Tag);
    if lang = nil then
      Exit(False);

    // TODO: can this fail?
    (lang as IKeymanKeyboardLanguageInstalled2).InstallTip(LangID, KeyboardToRemove);
    Result := True;
  finally
    KL.MethodExit(nil, 'TTIPMaintenance.InstallTip', [Result]);
  end;
end;

class function TTIPMaintenance.RegisterTip(LangID: Integer; const KeyboardID,
  BCP47Tag: string): Boolean;
var
  lang: IKeymanKeyboardLanguageInstalled;
begin
  Result := False;
  KL.MethodEnter(nil, 'TTIPMaintenance.RegisterTip', [KeyboardID, BCP47Tag]);
  try
    lang := GetKeyboardLanguage(KeyboardID, BCP47Tag);
    if lang = nil then
      Exit(False);

    // TODO: can this fail?
    (lang as IKeymanKeyboardLanguageInstalled2).RegisterTip(LangID);
    Result := True;
  finally
    KL.MethodExit(nil, 'TTIPMaintenance.RegisterTip', [Result]);
  end;
end;

class function TTIPMaintenance.DoInstall(const KeyboardID,
  BCP47Tag: string): Boolean;
var
  lang: IKeymanKeyboardLanguageInstalled;
  RegistrationRequired: WordBool;
  TemporaryKeyboardID: WideString;
  LangID: Integer;
  childExitCode: Cardinal;
  CanonicalTag, Command: string;
begin
  Result := False;
  KL.MethodEnter(nil, 'TTIPMaintenance.DoInstall', [KeyboardID, BCP47Tag]);
  try
    CanonicalTag := (kmcom as IKeymanBCP47Canonicalization).GetCanonicalTag(BCP47Tag);
    lang := GetKeyboardLanguage(KeyboardID, CanonicalTag);
    KL.Log('BCP47Tag = %s, CanonicalTag = %s, lang.BCP47Code = %s', [BCP47Tag, CanonicalTag, lang.BCP47Code]);
    if lang = nil then
      // The keyboard was not found
      Exit(False);

    if lang.IsInstalled then
      // After canonicalization, we may find the language is already installed
      Exit(True);

    TemporaryKeyboardID := '';
    LangID := 0;
    RegistrationRequired := False;

    if not (lang as IKeymanKeyboardLanguageInstalled2).FindInstallationLangID(LangID, TemporaryKeyboardID, RegistrationRequired, kifInstallTransientLanguage) then
    begin
      KL.Log('Failed to find installation langid');
      // We were not able to find a TIP, perhaps all transient TIPs have been used
      Exit(False);
    end;

    KL.Log('LangID=%x, TemporaryKeyboardID=%s, RegistrationRequired=%s', [LangID, TemporaryKeyboardID, BoolToStr(RegistrationRequired, True)]);
<commit_msg>fix(windows): don't log before checking nil when installing keyboard<commit_after>unit Keyman.Configuration.System.TIPMaintenance;

interface

uses
  System.Classes,

  KeymanAPI_TLB;

type
  TTIPMaintenance = class
  public
    /// <summary>Install a TIP for the current user (user context)</summary>
    class function DoInstall(const KeyboardID, BCP47Tag: string): Boolean;

    /// <summary>Install a TIP for the current user (user context)</summary>
    class function InstallTip(LangID: Integer; const KeyboardID, BCP47Tag, KeyboardToRemove: string): Boolean;

    /// <summary>Install TIPs for packages for the current user (user context)</summary>
    class function InstallTipsForPackages(Packages: TStrings): Boolean; static;

    /// <summary>Install TIP for a package for the current user (user context)</summary>
    class function InstallTipForPackage(const PackageFilename, BCP47Tag: string): Boolean; static;

    /// <summary>Register TIP for the local machine (elevated context)</summary>
    class function DoRegister(const KeyboardID, BCP47Tag: string): Boolean;

    /// <summary>Register TIP for the local machine (elevated context)</summary>
    class function RegisterTip(LangID: Integer; const KeyboardID, BCP47Tag: string): Boolean;

    /// <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; overload; static;
    class procedure GetUserDefaultLanguage(var BCP47: string; var LangID: Integer); overload; static;

    /// <summary>Get the -default-lang parameter string for kmshell</summary>
    class function GetUserDefaultLangParameterString: string; static;
  private
    class function GetKeyboardLanguage(const KeyboardID,
      BCP47Tag: string): IKeymanKeyboardLanguageInstalled; static;
  end;

implementation

uses
  System.SysUtils,
  System.Win.Registry,
  Winapi.Windows,

  Keyman.System.LanguageCodeUtils,

  KLog,
  BCP47Tag,
  glossary,
  kmint,
  RegistryKeys,
  utilkmshell,
  utilexecute,
  utilsystem;

{ TTIPMaintenance }

class function TTIPMaintenance.InstallTipsForPackages(Packages: TStrings): Boolean;
var
  i: Integer;
begin
  Result := True;
  KL.MethodEnter(nil, 'TTIPMaintenance.InstallTipsForPackages', [Packages.Text]);
  try
    for i := 0 to Packages.Count - 1 do
      // We'll attempt every package but return failure if any of them have issues
      Result := InstallTipForPackage(Packages.Names[i], Packages.ValueFromIndex[i]) and Result;
  finally
    KL.MethodExit(nil, 'TTIPMaintenance.InstallTipsForPackages', [Result]);
  end;
end;

class function TTIPMaintenance.InstallTipForPackage(const PackageFilename, BCP47Tag: string): Boolean;
var
  PackageID: string;
  n: Integer;
  pack: IKeymanPackageInstalled;
begin
  Result := False;
  KL.MethodEnter(nil, 'TTIPMaintenance.InstallTipForPackage', [PackageFilename, BCP47Tag]);
  try
    // This function has a known limitation: if a package contains more than one keyboard,
    // then the BCP47 association will be made only for the first keyboard. This is
    // considered an acceptable limitation at this time

    PackageID := ChangeFileExt(ExtractFileName(PackageFilename), '');
    n := kmcom.Packages.IndexOf(PackageID);
    if n < 0 then
      Exit(False);

    pack := kmcom.Packages[n];

    if pack.Keyboards.Count = 0 then
      Exit(False);

    if BCP47Tag = ''
      then Result := DoInstall(pack.Keyboards[0].ID, GetFirstLanguage(pack.Keyboards[0] as IKeymanKeyboardInstalled))
      else Result := DoInstall(pack.Keyboards[0].ID, BCP47Tag);
  finally
    KL.MethodExit(nil, 'TTIPMaintenance.InstallTipForPackage', [Result]);
  end;
end;

class function TTIPMaintenance.InstallTip(LangID: Integer; const KeyboardID, BCP47Tag,
  KeyboardToRemove: string): Boolean;
var
  lang: IKeymanKeyboardLanguageInstalled;
begin
  Result := False;
  KL.MethodEnter(nil, 'TTIPMaintenance.InstallTip', [LangID, KeyboardID, BCP47Tag, KeyboardToRemove]);
  try
    lang := GetKeyboardLanguage(KeyboardID, BCP47Tag);
    if lang = nil then
      Exit(False);

    // TODO: can this fail?
    (lang as IKeymanKeyboardLanguageInstalled2).InstallTip(LangID, KeyboardToRemove);
    Result := True;
  finally
    KL.MethodExit(nil, 'TTIPMaintenance.InstallTip', [Result]);
  end;
end;

class function TTIPMaintenance.RegisterTip(LangID: Integer; const KeyboardID,
  BCP47Tag: string): Boolean;
var
  lang: IKeymanKeyboardLanguageInstalled;
begin
  Result := False;
  KL.MethodEnter(nil, 'TTIPMaintenance.RegisterTip', [KeyboardID, BCP47Tag]);
  try
    lang := GetKeyboardLanguage(KeyboardID, BCP47Tag);
    if lang = nil then
      Exit(False);

    // TODO: can this fail?
    (lang as IKeymanKeyboardLanguageInstalled2).RegisterTip(LangID);
    Result := True;
  finally
    KL.MethodExit(nil, 'TTIPMaintenance.RegisterTip', [Result]);
  end;
end;

class function TTIPMaintenance.DoInstall(const KeyboardID,
  BCP47Tag: string): Boolean;
var
  lang: IKeymanKeyboardLanguageInstalled;
  RegistrationRequired: WordBool;
  TemporaryKeyboardID: WideString;
  LangID: Integer;
  childExitCode: Cardinal;
  CanonicalTag, Command: string;
begin
  Result := False;
  KL.MethodEnter(nil, 'TTIPMaintenance.DoInstall', [KeyboardID, BCP47Tag]);
  try
    CanonicalTag := (kmcom as IKeymanBCP47Canonicalization).GetCanonicalTag(BCP47Tag);
    lang := GetKeyboardLanguage(KeyboardID, CanonicalTag);
    if lang = nil then
    begin
      // The keyboard was not found
      KL.Log('lang not found: BCP47Tag = %s, CanonicalTag = %s', [BCP47Tag, CanonicalTag]);
      Exit(False);
    end;

    KL.Log('BCP47Tag = %s, CanonicalTag = %s, lang.BCP47Code = %s', [BCP47Tag, CanonicalTag, lang.BCP47Code]);

    if lang.IsInstalled then
      // After canonicalization, we may find the language is already installed
      Exit(True);

    TemporaryKeyboardID := '';
    LangID := 0;
    RegistrationRequired := False;

    if not (lang as IKeymanKeyboardLanguageInstalled2).FindInstallationLangID(LangID, TemporaryKeyboardID, RegistrationRequired, kifInstallTransientLanguage) then
    begin
      KL.Log('Failed to find installation langid');
      // We were not able to find a TIP, perhaps all transient TIPs have been used
      Exit(False);
    end;

    KL.Log('LangID=%x, TemporaryKeyboardID=%s, RegistrationRequired=%s', [LangID, TemporaryKeyboardID, BoolToStr(RegistrationRequired, True)]);

{'fraction_non_alphanumeric': 0.0970006868656033, 'fraction_numerical': 0.010837212852018621, 'mean_word_length': 4.4874371859296485, 'pattern_counts': {'":': 0, '<': 41, '<?xml version=': 0, '>': 39, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2773497', 'n_tokens_mistral': 4061, 'n_tokens_neox': 3924, 'n_words': 1093}