/// some common User Interface functions and dialogs for mORMot
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit mORMotUILogin;

(*
    This file is part of Synopse mORMot framework.

    Synopse mORMot framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse mORMot framework.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):
  - igors233

  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****



    Version 1.4 - February 8, 2010
    - whole Synopse SQLite3 database framework released under the GNU Lesser
      General Public License version 3, instead of generic "Public Domain"

    Version 1.5 - February 18, 2010
    - allow to pickup the User Name from a Combo Box

    Version 1.9
    - improved Delphi 2009/2010 UnicodeString compatibility
    - new PassWord method to enter a password e.g. for Cypher (i.e. with no UserName)

    Version 1.13
    - now use TMS component pack only if USETMSPACK global conditional is defined
    - introducing new TSynButton, mapping either to the default TBitButton,
      either to TAdvGlowButton (if USETMSPACK conditional is defined)
    - introducing a new TTaskDialog record/object, to access the new Vista/Seven
      TaskDialog, with a fallback dialog written in Delphi under XP
    - use a best available font (Calibri or Tahoma), and useful bitmaps

    Version 1.15
    - new InputBox global function
    - new QueryMasked parameter to display * in InputBox/InputQuery editor field

    Version 1.16
    - InputBox function will now focus the input field component by default

    Version 1.18
    - renamed SQLite3UILogin.pas to mORMotUILogin.pas
    - added ShowLastClientError() and ShowException() functions
    - added TLoginForm.OnIdleProcess and OnIdleProcessForm callback events,
      ready to be assigned to TSQLRestClientURI.OnIdle property

*)

interface

{$I Synopse.inc} // define HASINLINE CPU32 CPU64

uses
  {$ifdef MSWINDOWS}
    Windows,
    CommCtrl,
  {$endif}
  {$ifdef FPC}
    //LCLProc, LCLIntf, LCLType,
    LCLType,
    LCLIntf,
    LResources,
    SynTaskDialog, Controls, StdCtrls, ExtCtrls, Classes in '.\Samples\ThirdPartyDemos\Ondrej\SynTaskDialog4Lazarus\SynTaskDialog.pas',
  {$else}
    Consts,
    PsAPI,
    SynTaskDialog,
  {$endif FPC}
  Messages,
  SysUtils,
  Classes,
  Graphics,
  Controls,
  Forms,
  StdCtrls,
  ExtCtrls,
  Buttons,
  {$ifdef USETMSPACK}
    AdvGlowButton,
    TaskDialog,
    TaskDialogEx,
    AdvToolBarStylers,
    AdvToolBar,
  {$endif USETMSPACK}
  SynGdiPlus,
  SynCommons,
  SynTable,
  mORMot,
  mORMotUI;

type
  /// Form used to Log User and enter its password
  TLoginForm = class(TForm)
    Label1: TLabel;
    Label2: TLabel;
    Edit2: TEdit;
    Image1: TImage;
    Label3: TLabel;
    Bevel1: TBevel;
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
  protected
    Edit: TWinControl;
    function EditText: string;
    class procedure HandleApplicationException(Sender: TObject; E: Exception);
  public
    /// display the Login dialog window
    class function Login(const aTitle, aText: string; var aUserName, aPassWord: string;
      AllowUserNameChange: boolean; const CSVComboValues: string): boolean;
    /// display the password dialog window
    class function PassWord(const aTitle, aText: string; var aPassWord: string): boolean;
  public
    /// TOnIdleSQLRestClient-like event to process Windows Messages
    // - to be assigned e.g. to TSQLRestClientURI.OnIdle property
    // - global OnIdleProcessCursorChangeTimeout variable is used to display
    // the crHourGlass cursor after a given time elapsed
    class procedure OnIdleProcess(Sender: TSynBackgroundThreadAbstract; ElapsedMS: Integer);
    /// TOnIdleSQLRestClient-like event to process Windows Messages and write a
    // temporary form on screen if it takes too long
    // - to be assigned e.g. to TSQLRestClientURI.OnIdle property
    // - global OnIdleProcessCursorChangeTimeout variable is used to display
    // the crHourGlass cursor after a given time elapsed
    // - global OnIdleProcessTemporaryFormTimeout variable is used to display
    // a temporary form after a given time elapsed
    class procedure OnIdleProcessForm(Sender: TSynBackgroundThreadAbstract; ElapsedMS: Integer);
  end;

var
  /// define when TLoginForm.OnIdleProcess() has to display the crHourGlass cursor
  // after a given time elapsed, in milliseconds
  // - default is 100 ms
  OnIdleProcessCursorChangeTimeout: integer = 100;

  /// define when TLoginForm.OnIdleProcessForm() has to display the temporary
  // form after a given time elapsed, in milliseconds
  // - default is 2000 ms, i.e. 2 seconds
  OnIdleProcessTemporaryFormTimeout: integer = 2000;

  /// define the message text displayed by TLoginForm.OnIdleProcessForm()
  // - default is sOnIdleProcessFormMessage resourcestring, i.e. 'Please wait...'
  OnIdleProcessTemporaryFormMessage: string;


{$ifdef USETMSPACK}

/// create a temporary AdvTaskDialog component, with the current style
function CreateAdvTaskDialog: TAdvTaskDialogEx;

/// create a temporary AdvInputTaskDialog component, with the current style
function CreateAdvInputTaskDialog: TAdvInputTaskDialogEx;

/// retrieve the main toolbar from main form
function GetMainPager: TAdvToolBarPager;

/// retrieve the main styler from main form
function GetMainStyler: TAdvToolBarOfficeStyler;

/// set the style for a form and a its buttons
// - return the toolbar styler found, if any
function SetStyle(Form: TComponent; Styler: TAdvToolBarOfficeStyler=nil): TAdvToolBarOfficeStyler;

{$else}

/// set the style for a form and a its buttons
// - set the Default Font for all components, i.e. Calibri if available
procedure SetStyle(Form: TComponent);

{$endif USETMSPACK}


/// show an (error) message, using a Vista-Style dialog box
procedure ShowMessage(const Msg: string; Error: boolean=false); overload;

/// convert an error message into html compatible equivalency
// - allow to display < > & correctly
function HtmlEscape(const Msg: string): string;

/// show an (error) message, using a Vista-Style dialog box
procedure ShowMessage(const Msg, Inst: string; Error: boolean=false); overload;

/// show an error dialog box, with the corresponding Client-Side information
// - retrieve last error message from Client.LastError* values
function ShowLastClientError(Client: TSQLRestClientURI; const ContextMessage: string='';
  CommonButtons: TCommonButtons=[cbOk]): integer;

/// show an error dialog box, corresponding to a specified exception
function ShowException(E: Exception; CommonButtons: TCommonButtons=[cbOk];
  const ContextMessage: string=''): integer;

/// ask the User to choose Yes or No [and Cancel], using a Vista-Style dialog box
function YesNo(const aQuestion: string; const aConfirm: string =''; withCancel: boolean=true;
  Warning: boolean=false): integer;

/// ask the User to enter some string value
// - if QueryMasked=TRUE, will mask the prompt with '*' chars (e.g. for entering
// a password)
function InputQuery(const ACaption, APrompt: string; var Value: string;
  QueryMasked: boolean=false): Boolean;

/// ask the User to enter some string value
// - if QueryMasked=TRUE, will mask the prompt with '*' chars (e.g. for entering
// a password)
function InputBox(const ACaption, APrompt, ADefault: string; QueryMasked: boolean=false): string;

/// ask the User to select one item from an array of strings
// - return the selected index, -1 if Cancel button was pressed
function InputSelect(const ACaption, APrompt, AItemsText, ASelectedText: string): integer;

/// ask the User to select one enumerate item
// - use internally TEnumType.GetCaption() to retrieve the text to be displayed
// - Index must be an instance of this enumeration type (internaly mapped to a PByte)
function InputSelectEnum(const ACaption, APrompt: string; EnumTypeInfo: PTypeInfo;
  var Index): boolean;

/// ask the User to choose between some Commands
// - return the selected command index, starting numerotation at 100
function Choose(const aTitle, aContent, aFooter: string; const Commands: array of string;
  aFooterIcon: TTaskDialogFooterIcon=tfiInformation): integer; overload;

/// ask the User to choose between some Commands
// - return the selected command index, starting numerotation at 100
// - this overloaded function expect the Content and the Commands to be
// supplied as CSV string (Content as first CSV, then commands)
function Choose(const aTitle, aCSVContent: string): integer; overload;

resourcestring
  sClientServerError = 'An error occured during Client-Server process';

  sOnIdleProcessFormMessage = 'Please wait...';


/// ensure that the program is launched once
// - the main project .dpr source file must contain:
// !begin
// !  Application.Initialize;
// !  EnsureSingleInstance; // program is launched once
// !  Application.CreateForm(TMainForm, MainForm);
// ! ....
procedure EnsureSingleInstance;

type
  PTPanel = ^TPanel;

/// popup a temporary form with a message over all forms
// - caller should execute result.Free and reset the cursor if needed
function CreateTempForm(const aCaption: string;
  aPanelReference: PTPanel=nil; ScreenCursorHourGlass: boolean=false;
  aCaptionColor: integer=clNavy; aCaptionSize: integer=12): TForm;



implementation

{$ifdef FPC}
{$R *.lfm}
{$else}
{$R *.dfm}
{$endif}

{$R SQLite3UILogin.res}

/// popup a temporary form with a message over all forms
function CreateTempForm(const aCaption: string;
  aPanelReference: PTPanel=nil; ScreenCursorHourGlass: boolean=false;
  aCaptionColor: integer=clNavy; aCaptionSize: integer=12): TForm;
var P: TPanel;
begin
  if ScreenCursorHourGlass then
    Screen.Cursor := crHourGlass;
  Result := TForm.Create(nil);
  Result.Width := 400; Result.Height := 100;
  Result.Position := poScreenCenter;
  Result.FormStyle := fsStayOnTop;
  Result.BorderStyle := bsNone;
  P := TPanel.Create(Result);
  with P do begin
    Color := clWhite;
    BorderStyle := bsSingle;
    Parent := Result;
    Font.Name := 'Tahoma'; Font.Size := aCaptionSize;
    Font.Color := aCaptionColor;  Font.Style := [fsBold];
    Caption := aCaption;
    Align := alClient;
  end;
  if aPanelReference<>nil then
    aPanelReference^ := P;
{  with TImage.Create(Result) do begin
    Parent := P;
    Transparent := true;
    Picture.Bitmap.LoadFromResourceName(hInstance,'ROC');
    SetBounds((330-156)div 2,20,156,73);
  end;
  with TLabel.Create(Result) do begin
    Parent := P;
    Font.Name := 'Tahoma'; Font.Size := 11;
    Font.Color := clNavy;  Font.Style := [fsBold];
    Alignment := taCenter;
    Caption := aCaption;
    SetBounds(0,120,330,100);
  end; }
  Result.Show;
  Application.ProcessMessages;
end;

procedure ShowMessage(const Msg: string; Error: boolean=false);
begin
  if Error then
    ShowMessage(HtmlEscape(Msg),SMsgDlgError,Error) else
    ShowMessage(HtmlEscape(Msg),SMsgDlgInformation,Error);
end;

function HtmlEscape(const Msg: string): string;
{$ifdef USETMSPACK}
var i: integer;
    ins: string;
{$endif}
begin
  result := Msg;
{$ifdef USETMSPACK}
  for i := length(Msg) downto 1 do begin
    case Msg[i] of
      '"': ins := '&quot';
      '&': ins := '&amp';
      '<': ins := '&lt';
      '>': ins := '&gt';
      else Continue;
    end;
    result[i] := ';';
    insert(ins,result,i);
   end;
{$endif}
end;


{$ifdef USETMSPACK}

function CreateAdvTaskDialog: TAdvTaskDialogEx;
var Style: TAdvToolBarOfficeStyler;
begin
  result := TAdvTaskDialogEx.Create(Application);
  if Application.MainForm=nil then
    result.Title := Application.Title else
    result.Title := Application.MainForm.Caption;
  result.Options := result.Options+[doHyperlinks];
  result.NonNativeDialog := nndAlways; // we need hyperlinks=HTML -> non native!
  Style := GetMainStyler;
  if Style<>nil then
    result.Appearance := Style.GlowButtonAppearance;
end;

function Choose(const aTitle, aContent, aFooter: string; const Commands: array of string;
  aFooterIcon: TTaskDialogFooterIcon=tfiInformation): integer;
var i: integer;
begin
  with TAdvTaskDialog(CreateAdvTaskDialog) do
  try
    Instruction := aTitle;
    Content := aContent;
    Icon := tiQuestion;
    CustomButtons.Clear;
    for i := 0 to high(Commands) do
      CustomButtons.Add(Commands[i]);
    DefaultButton := 100;
    Options := Options+[doCommandLinks];
    if aFooter<>'' then begin
      FooterIcon := aFooterIcon;
      if aFooterIcon=tfiWarning then
        Footer := '<b>'+SMsgDlgWarning+'</b>:'#13+aFooter else
        Footer := aFooter;
    end;
    result := Execute;
  finally
    Free;
  end;
end;

type
  /// we definitively NEED these properties
  TAdvInputTaskDialogPublished = class(TAdvInputTaskDialogEx)
  published
    property Options;
    property NonNativeDialog;
  end;

function CreateAdvInputTaskDialog: TAdvInputTaskDialogEx;
var Style: TAdvToolBarOfficeStyler;
begin
  result := TAdvInputTaskDialogPublished.Create(Application);
  result.Title := Application.MainForm.Caption;
  TAdvInputTaskDialogPublished(result).Options := TAdvInputTaskDialogPublished(result).Options+[doHyperlinks];
  TAdvInputTaskDialogPublished(result).NonNativeDialog := nndAlways; // we need hyperlinks=HTML -> non native!
  Style := GetMainStyler;
  if Style<>nil then
    result.Appearance := Style.GlowButtonAppearance;
end;

function InputQuery(const ACaption, APrompt: string; var Value: string;
  QueryMasked: boolean=false): Boolean;
begin
  with CreateAdvInputTaskDialog do
  try
    Instruction := ACaption;
    Content := APrompt;
    InputType := itEdit;
    CommonButtons := [cbOk,cbCancel];
    InputText := Value;
    result := false;
    if Execute<>mrOk then
      exit;
    result := true;
    Value := InputText;
  finally
    Free;
  end;
end;

function InputSelect(const ACaption, APrompt, AItemsText, ASelectedText: string): integer;
var Items: TStringList;
begin
  result := -1;
  if AItemsText<>'' then
  with CreateAdvInputTaskDialog do
  try
    Instruction := ACaption;
    Content := APrompt;
    InputType := itComboList;
    CommonButtons := [cbOk,cbCancel];
    Items := TStringList.Create;
    try
      Items.Text := AItemsText;
      InputItems := Items;
      InputText := ASelectedText;
      if Execute<>mrOk then
        exit;
      result := Items.IndexOf(InputText);
    finally
      Items.Free;
    end;
  finally
    Free;
  end;
end;

function Choose(const aTitle, aCSVContent: string): integer; overload;
var Commands: array of string;
    Content: string;
    P: PChar;
begin
  P := pointer(aCSVContent);
  Content := GetNextItemString(P);
  while P<>nil do begin
    SetLength(Commands,length(Commands)+1);
    Commands[high(Commands)] := GetNextItemString(P);
  end;
  result := Choose(aTitle,Content,'',Commands);
end;

{$else}

function Choose(const aTitle, aContent, aFooter: string; const Commands: array of string;
  aFooterIcon: TTaskDialogFooterIcon=tfiInformation): integer;
var Task: TTaskDialog;
    i: integer;
begin
  Task.Inst := aTitle;
  Task.Content := aContent;
  Task.Footer := aFooter;
  for i := 0 to high(Commands) do
    Task.Buttons := Task.Buttons+Commands[i]+#10;
  result := Task.Execute([],100,[tdfUseCommandLinks],tiQuestion,aFooterIcon);
  if result<100 then
    result := -1 else
    dec(result,100);
end;

function InputQuery(const ACaption, APrompt: string; var Value: string;
  QueryMasked: boolean=false): Boolean;
const FLAGS: array[boolean] of TTaskDialogFlags = (
   [tdfQuery,tdfQueryFieldFocused],[tdfQuery,tdfQueryMasked,tdfQueryFieldFocused]);
var Task: TTaskDialog;
begin
  Task.Inst := ACaption;
  Task.Content := APrompt;
  Task.Query := Value;
  result := Task.Execute([cbOk,cbCancel],0,FLAGS[QueryMasked],tiQuestion)=mrOk;
  if result then
    Value := Task.Query;
end;

function InputSelect(const ACaption, APrompt, AItemsText, ASelectedText: string): integer;
var Task: TTaskDialog;
begin
  result := -1;
  if AItemsText='' then
    exit;
  Task.Inst := ACaption;
  Task.Content := APrompt;
  Task.Selection := AItemsText;
  Task.Query := ASelectedText;
  if Task.Execute([cbOk,cbCancel],0,[],tiQuestion)=mrOk then
    result := Task.SelectionRes;
end;

function Choose(const aTitle, aCSVContent: string): integer; overload;
var Task: TTaskDialog;
    P: PChar;
begin
  Task.Inst := aTitle;
  P := pointer(aCSVContent);
  Task.Content := GetNextItemString(P);
  while P<>nil do
    Task.Buttons := Task.Buttons+GetNextItemString(P)+#10;
  result := Task.Execute([],100,[tdfUseCommandLinks],tiQuestion);
  if result<100 then
    result := -1 else
    dec(result,100);
end;

{$endif USETMSPACK}

function InputBox(const ACaption, APrompt, ADefault: string; QueryMasked: boolean=false): string;
begin
  result := ADefault;
  if not InputQuery(ACaption,APrompt,Result,QueryMasked) then
     result := ADefault;
end;

function InputSelectEnum(const ACaption, APrompt: string; EnumTypeInfo: PTypeInfo;
  var Index): boolean;
var i: integer;
begin
  result := false;
  if (EnumTypeInfo<>nil) and (EnumTypeInfo^.Kind=tkEnumeration) then begin
    i := InputSelect(ACaption,APrompt,EnumTypeInfo^.EnumBaseType^.GetCaptionStrings,
     EnumTypeInfo^.EnumBaseType^.GetCaption(PByte(@Index)^));
    if i>=0 then begin
      PByte(@Index)^ := i;
      result := true;
    end;
  end;
end;

procedure ShowMessage(const Msg, Inst: string; Error: boolean=false);
const
  IconError: array[boolean] of TTaskDialogIcon = (tiInformation, tiError);
{$ifndef USETMSPACK}
var Task: TTaskDialog;
{$endif}
begin
{$ifdef USETMSPACK}
  with CreateAdvTaskDialog do
  try
    Instruction := Inst;
    Icon := IconError[Error];
    Content := Msg;
    CommonButtons := [cbOk];
    Execute;
  finally
    Free;
  end;
{$else}
  Task.Inst := Inst;
  Task.Content := Msg;
  Task.Execute([cbOK],mrOk,[],IconError[Error]);
{$endif}
end;

function ShowLastClientError(Client: TSQLRestClientURI; const ContextMessage: string='';
  CommonButtons: TCommonButtons=[cbOk]): integer;
var details: string;
{$ifndef USETMSPACK}
    Task: TTaskDialog;
{$endif}
begin
  if Client<>nil then begin
    if Client.LastErrorMessage<>'' then
      details := UTF8ToString(Client.LastErrorMessage)+#13#10;
    if Client.LastErrorException<>nil then
      details := Format('%sException %s raised - ErrorCode=%d',
        [details,Client.LastErrorException.ClassName,Client.LastErrorCode]);
  end;
{$ifdef USETMSPACK}
  with CreateAdvTaskDialog do
  try
    Instruction := sClientServerError;
    Content := ContextMessage;
    Icon := tiError;
    CommonButtons := [cbOk];
    result := Execute;
  finally
    Free;
  end;
{$else}
  if ContextMessage='' then
    Task.Content := sClientServerError else
    Task.Content := ContextMessage;
  Task.Info := details;
  result := Task.Execute([cbOK],mrOk,[],tiError);
{$endif}
end;

function ShowException(E: Exception; CommonButtons: TCommonButtons;
  const ContextMessage: string): integer;
var details: string;
{$ifndef USETMSPACK}
    Task: TTaskDialog;
{$endif}
begin
  if E=nil then begin
    ShowMessage(ContextMessage,true);
    result := mrNone;
    exit;
  end;
  details := UTF8ToString(ObjectToJSONDebug(E));
{$ifdef USETMSPACK}
  with CreateAdvTaskDialog do
  try
    Instruction := sClientServerError;
    Content := ContextMessage;
    Icon := tiError;
    CommonButtons := [cbOk];
    result := Execute;
  finally
    Free;
  end;
{$else}
  Task.Content := E.Message;
  Task.Inst := ContextMessage;
  Task.Info := details;
  result := Task.Execute(CommonButtons,mrOk,[],tiError);
{$endif}
end;

function YesNo(const aQuestion, aConfirm: string; withCancel: boolean; Warning: boolean): integer;
var Confirm: string;
{$ifndef USETMSPACK}
    Task: TTaskDialog;
{$endif}
const
  IconWarning: array[boolean] of TTaskDialogIcon = (
    tiQuestion, tiWarning);
  BtsCancel: array[boolean] of TCommonButtons = (
    [cbYes, cbNo], [cbYes, cbNo, cbCancel]);
begin
  if aConfirm='' then
    Confirm := SMsgDlgConfirm else
    Confirm := aConfirm;
{$ifdef USETMSPACK}
  with CreateAdvTaskDialog do
  try
    Instruction := Confirm;
    Icon := IconWarning[Warning];
    Content := aQuestion;
    CommonButtons := BtsCancel[withCancel];
    result := Execute;
  finally
    Free;
  end;
{$else}
  Task.Inst := Confirm;
  Task.Content := aQuestion;
  result := Task.Execute(BtsCancel[withCancel],0,[],IconWarning[Warning]);
{$endif}
end;

{$ifdef Crazy}
function FileNameFromWndMatch(const Wnd: HWnd; const AppFileName: string): boolean;
var ProcessID: DWORD;
    hProc: THandle;
    tmp: array[0..MAX_PATH] of char;
begin
  GetWindowThreadProcessId(Wnd, @ProcessId);
  hProc := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false, ProcessId);
  if hProc <> 0 then begin
    {$ifdef FPC}
    if GetModuleFileName(hProc, tmp, MAX_PATH) = 0 then
    {$else}
    if GetModuleFileNameEx(hProc, 0, tmp, MAX_PATH) = 0 then
    {$endif}
      tmp[0] := #0;
    CloseHandle(hProc);
    result := ExtractFileName(string(tmp)) = AppFileName;
  end else
    result := false;
end;
{$endif Crazy}

procedure EnsureSingleInstance;
var Wnd: HWnd;
    ToFindClass, WndClass, AppFileName: string;
    tmp: array[byte] of char;
    function FileNameFromWndMatch: boolean;
    var ProcessID: DWORD;
        hProc: THandle;
        tmp: array[0..MAX_PATH] of char;
    begin
      GetWindowThreadProcessId(Wnd, @ProcessId);
      hProc := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false, ProcessId);
      if hProc <> 0 then begin
        {$ifdef FPC}
        if GetModuleFileName(hProc, tmp, MAX_PATH) = 0 then
        {$else}
        if GetModuleFileNameEx(hProc, 0, tmp, MAX_PATH) = 0 then
        {$endif}
          tmp[0] := #0;
        CloseHandle(hProc);
        result := ExtractFileName(string(tmp)) = AppFileName;
      end else
        result := false;
    end;
begin
  if Application = nil then
    exit;
  { Try and create a semaphore. If we succeed, then check if the semaphore was
    already present. If it was then a previous instance is floating around.
    Note the OS will free the returned semaphore handle when the app shuts
    so we can forget about it }
  AppFileName := ExtractFileName(ExeVersion.ProgramFileName);
  if (CreateSemaphore(nil, 0, 1, Pointer(AppFileName)) <> 0) and
     (GetLastError = ERROR_ALREADY_EXISTS) then begin
    if GetClassName({$ifdef FPC}Application.MainFormHandle{$else}Application.Handle{$endif}, tmp, high(tmp)) = 0 then
      exit;
    ToFindClass := tmp;
    Wnd := GetWindow({$ifdef FPC}Application.MainFormHandle{$else}Application.Handle{$endif}, GW_HWNDFIRST);
    while Wnd <> 0 do begin
      { Look for the other TApplication window out there }
      if Wnd <> {$ifdef FPC}Application.MainFormHandle{$else}Application.Handle{$endif} then begin
        { Check if got the same class and filename }
        if GetClassName(Wnd, tmp, high(tmp)) = 0 then
          tmp[0] := #0;
        WndClass := tmp;
        if (ToFindClass = WndClass) and FileNameFromWndMatch then begin
          { This technique is used by the VCL: post a message then bring the
            window to the top, before the message gets processed }
          PostMessage(Wnd, WM_SYSCOMMAND, SC_RESTORE, 0);
          SetForegroundWindow(Wnd);
          break;
        end
      end;
      Wnd := GetWindow(Wnd, GW_HWNDNEXT);
    end;
    AppFileName := ''; // avoid memory leak due to Halt brutal execution
    WndClass := '';
    ToFindClass := '';
    Halt; // Stop this new instance
  end
end;

procedure TLoginForm.FormCreate(Sender: TObject);
var P: TSynPicture;
begin
  SetStyle(self);
  TSynButton.CreateKind(Self,cbOK,136,163,75,30).Anchors := [akLeft,akBottom];
  TSynButton.CreateKind(Self,cbCancel,224,163,75,30).Anchors := [akLeft,akBottom];
  P := TSynPicture.Create;
  try
    P.LoadFromResourceName(HInstance,'UILogin'); // SQLite3uilogin.png
    Image1.Picture.Assign(P);
  finally
    P.Free;
  end;
end;

class function TLoginForm.PassWord(const aTitle, aText: string;
  var aPassWord: string): boolean;
var LoginForm: TLoginForm;
begin
  Application.CreateForm(TLoginForm,LoginForm);
  try
    with LoginForm do begin
      Label1.Hide;
      Label3.Caption := aText;
      Label3.Width := LoginForm.ClientWidth;
      Bevel1.Hide;
      ClientHeight := 180;
      LoginForm.Caption := ' '+aTitle;
      result := (LoginForm.ShowModal=mrOk);
      if result then
        aPassWord := SysUtils.trim(LoginForm.Edit2.Text);
    end;
  finally
    LoginForm.Free;
  end;
end;

class function TLoginForm.Login(const aTitle, aText: string; var aUserName, aPassWord: string;
  AllowUserNameChange: boolean; const CSVComboValues: string): boolean;
var T: string;
    i: integer;
    LoginForm: TLoginForm;
begin
  Application.CreateForm(TLoginForm,LoginForm);
  with LoginForm do
  try
    Label3.Caption := aText;
    Label3.Width := LoginForm.ClientWidth;
    Bevel1.Visible := aText<>'';
    Height := 240+Label3.Height;
    Bevel1.Height := Label3.Height;
    if not AllowUserNameChange or (CSVComboValues='') then begin
      Edit := TEdit.Create(LoginForm);
      Edit.Parent := LoginForm;
      TEdit(Edit).Text := aUserName;
    end else begin
      Edit := TComboBox.Create(LoginForm);
      Edit.Parent := LoginForm;
      with TComboBox(Edit) do begin
        Text := aUserName;
        Style := csDropDownList;
        Items.Text := StringReplace(CSVComboValues,',',#13#10,[rfReplaceAll]);
        ItemIndex := Items.IndexOf(aUserName);
      end;
    end;
    {$ifdef FPC}
    // strange, but needed
    Edit.SetBounds(104,Height-Edit2.Height-84,193,22);
    {$else}
    Edit.SetBounds(104,Edit2.Top-32,193,22);
    {$endif}
    Edit.Anchors := [akLeft,akBottom];
    Edit.Enabled := AllowUserNameChange;
    Edit2.Text := '';
    if aTitle='' then
      if Application.MainForm=nil then
        T := Application.Title else
        T := Application.MainForm.Caption else begin
      T := aTitle;
      for i := 1 to length(T) do
        if T[i]<' ' then
          T[i] := ' ';
    end;
    Caption := ' '+T;
    result := (ShowModal=mrOk);
    if result then begin
      aPassWord := SysUtils.trim(Edit2.Text);
      if Edit.Enabled then
        aUserName := SysUtils.trim(EditText);
    end;
  finally
    LoginForm.Free;
  end;
end;

{$ifdef USETMSPACK}

function GetMainPager: TAdvToolBarPager;
var C: integer;
begin
  with Application.MainForm do
    for C := 0 to ComponentCount-1 do begin
      result := pointer(Components[C]);
      if result.InheritsFrom(TAdvToolBarPager) then
        exit;
    end;
  result := nil;
end;

var
 fMainStyler: TAdvToolBarOfficeStyler = nil;

function GetMainStyler: TAdvToolBarOfficeStyler;
var C: integer;
begin
  result := fMainStyler;
  if (result<>nil) or (Application.MainForm=nil) then
    exit;
  with Application.MainForm do
    for C := 0 to ComponentCount-1 do begin
      result := pointer(Components[C]);
      if result.InheritsFrom(TAdvToolBarOfficeStyler) then begin
        fMainStyler := result;
        exit;
      end;
    end;
  result := nil;
end;

function SetStyle(Form: TComponent; Styler: TAdvToolBarOfficeStyler=nil): TAdvToolBarOfficeStyler;
var i: integer;
    C: TComponent;
begin
  if Styler=nil then
    result := GetMainStyler else
    result := Styler;
  if (result=nil) or (Form=nil) then
    exit;
  for i := 0 to Form.ComponentCount-1 do begin
    C := Form.Components[i];
{    if C.InheritsFrom(TAdvToolBarOfficeStyler) then // buggy: TMS :(
      TAdvToolBarOfficeStyler(C).Assign(Style) else }
    if C.InheritsFrom(TAdvGlowButton) then
      TAdvGlowButton(C).Appearance := result.GlowButtonAppearance;
    SetStyle(C,result);
  end;
  if Form.InheritsFrom(TCustomForm) then
    TCustomForm(Form).Color := result.PageAppearance.Color;
  if Form.InheritsFrom(TAdvGlowButton) then
    TAdvGlowButton(Form).Appearance := result.GlowButtonAppearance;
end;

{$else}

procedure SetStyle(Form: TComponent);
var i, f: integer;
    C: TComponent;
    Ctrl: TControl absolute C;
    CL: TClass;
    P: PPropInfo;
    Obj: TObject;
begin
  if Form=nil then
    exit;
  // set form font to global Default Font
  if Form.InheritsFrom(TCustomForm) then
    TCustomForm(Form).Font := DefaultFont;
  // for any component with ParentFont=false, force Calibri if necessary
  if DefaultFont.Name='Calibri' then
  for i := 0 to Form.ComponentCount-1 do begin
    C := Form.Components[i];
    if C.InheritsFrom(TControl) then
    if not TButton(C).ParentFont then begin // trick to access TControl.FParentFont
      CL := PPointer(C)^;
      while (CL<>nil) and (CL<>TComponent) and (CL<>TObject) do begin
        for f := 1 to InternalClassPropInfo(CL,P) do begin
          with P^.PropType^{$ifndef HASDIRECTTYPEINFO}^{$endif} do
          if (Kind=tkClass) and ClassType^.InheritsFrom(TFont) then begin
            Obj := P^.GetObjProp(C);
            if Obj<>nil then
              with TFont(Obj) do
              if Name<>DefaultFont.Name then begin
                Name := DefaultFont.Name;
                Height := Height+(Height div 5);
              end;
          end;
          P := P^.Next;
        end;
        CL := CL.ClassParent; // handle parent published properties
      end;
    end;
  end;
end;

{$endif USETMSPACK}

procedure TLoginForm.FormShow(Sender: TObject);
begin
  if (Edit<>nil) and Edit.Enabled and (EditText='') then
    Edit.SetFocus else
    Edit2.SetFocus;
  SetStyle(self);
end;

function TLoginForm.EditText: string;
begin
  if Edit.InheritsFrom(TComboBox) then
    result := TComboBox(Edit).Text else
    result := TEdit(Edit).Text;
  result := SysUtils.trim(result);
end;

class procedure TLoginForm.HandleApplicationException(Sender: TObject; E: Exception);
begin // so that exception will be shown with new User Interface
  ShowMessage(E.Message,true);
end;

var
  OnIdleProcessCursor: TCursor = crNone;
  OnIdleProcessTempForm: TForm;

class procedure TLoginForm.OnIdleProcess(Sender: TSynBackgroundThreadAbstract; ElapsedMS: Integer);
begin
  Application.ProcessMessages;
  if (ElapsedMS>OnIdleProcessCursorChangeTimeout) and (OnIdleProcessCursor=crNone) then begin
    OnIdleProcessCursor := Screen.Cursor;
    Screen.Cursor := crHourGlass;
  end else
  if (ElapsedMS<0) and (OnIdleProcessCursor<>crNone) then begin
    Screen.Cursor := OnIdleProcessCursor;
    OnIdleProcessCursor := crNone;
  end;
end;

class procedure TLoginForm.OnIdleProcessForm(Sender: TSynBackgroundThreadAbstract; ElapsedMS: Integer);
var msg: string;
begin
  TLoginForm.OnIdleProcess(Sender,ElapsedMS);
  if (ElapsedMS>OnIdleProcessTemporaryFormTimeout) and
     not Assigned(OnIdleProcessTempForm) then begin
    if OnIdleProcessTemporaryFormMessage='' then
      msg := sOnIdleProcessFormMessage else
      msg := OnIdleProcessTemporaryFormMessage;
    OnIdleProcessTempForm := CreateTempForm(msg);
  end else
  if (ElapsedMS<0) and Assigned(OnIdleProcessTempForm) then
    FreeAndNil(OnIdleProcessTempForm);
end;

initialization
  Gdip.RegisterPictures; // will initialize the Gdip library if necessary
  Application.OnException := TLoginForm.HandleApplicationException;

end.


