unit connections;

{$mode delphi}{$H+}

// -------------------------------------
// Connections (start-window)
// -------------------------------------


interface

uses
  SysUtils, Classes, Controls, Forms, Dialogs, StdCtrls, ExtCtrls, ComCtrls,
  laz.VirtualTrees, Menus, Graphics, extra_controls, lazaruscompat,
  {$IFDEF Windows} ActiveX {$ELSE} laz.FakeActiveX {$ENDIF},
  dbconnection, RegExpr, Types, FileUtil, SpinEx,
  Math, ActnList, ComboEx, EditBtn, Buttons, ColorBox, extfiledialog, dbstructures;

type

  { Tconnform }

  Tconnform = class(TExtForm)
    btnCancel: TBitBtn;
    btnOpen: TBitBtn;
    btnSave: TBitBtn;
    btnNew: TBitBtn;
    btnDelete: TBitBtn;
    chkForceUnicode: TCheckBox;
    lblHelpPortable: TLabel;
    lblSshPassHint: TLabel;
    pnlBottom: TPanel;
    popupSessions: TPopupMenu;
    menuSave: TMenuItem;
    menuDelete: TMenuItem;
    menuSaveAs: TMenuItem;
    TimerStatistics: TTimer;
    PageControlDetails: TPageControl;
    tabSettings: TTabSheet;
    lblPort: TLabel;
    lblPassword: TLabel;
    lblHost: TLabel;
    lblUsername: TLabel;
    lblNetworkType: TLabel;
    chkCompressed: TCheckBox;
    spinPort: TSpinEditEx;
    editPassword: TEdit;
    editUsername: TEditButton;
    editHost: TEditButton;
    tabAdvanced: TTabSheet;
    tabStatistics: TTabSheet;
    lblLastConnectLeft: TLabel;
    lblCounterLeft: TLabel;
    lblCreatedLeft: TLabel;
    lblCreatedRight: TLabel;
    lblCounterRight1: TLabel;
    lblLastConnectRight: TLabel;
    tabSSHtunnel: TTabSheet;
    spinSSHlocalport: TSpinEditEx;
    editSSHUser: TEdit;
    editSSHPassword: TEdit;
    lblSSHLocalPort: TLabel;
    lblSSHUser: TLabel;
    lblSSHPassword: TLabel;
    lblSSHExe: TLabel;
    comboNetType: TComboBoxEx;
    lblSSHhost: TLabel;
    editSSHhost: TEdit;
    spinSSHport: TSpinEditEx;
    editSSHPrivateKey: TEditButton;
    lblSSHkeyfile: TLabel;
    editDatabases: TEditButton;
    lblDatabase: TLabel;
    chkLoginPrompt: TCheckBox;
    lblSSHTimeout: TLabel;
    spinSSHTimeout: TSpinEditEx;
    chkWindowsAuth: TCheckBox;
    chkCleartextPluginEnabled: TCheckBox;
    splitterMain: TSplitter;
    tabStart: TTabSheet;
    lblHelp: TLabel;
    btnImportSettings: TSpeedButton;
    timerSettingsImport: TTimer;
    popupNew: TPopupMenu;
    menuNewSessionInRoot: TMenuItem;
    menuNewFolderInRoot: TMenuItem;
    menuContextNewFolderInFolder: TMenuItem;
    menuContextNewSessionInFolder: TMenuItem;
    menuNewSessionInFolder: TMenuItem;
    menuNewFolderInFolder: TMenuItem;
    chkLocalTimeZone: TCheckBox;
    editStartupScript: TEditButton;
    lblStartupScript: TLabel;
    chkFullTableStatus: TCheckBox;
    btnMore: TBitBtn;
    popupMore: TPopupMenu;
    Checkforupdates1: TMenuItem;
    About1: TMenuItem;
    Preferences1: TMenuItem;
    Exportsettingsfile1: TMenuItem;
    Importsettingsfile1: TMenuItem;
    lblComment: TLabel;
    memoComment: TMemo;
    lblQueryTimeout: TLabel;
    spinQueryTimeout: TSpinEditEx;
    menuMoreGeneralHelp: TMenuItem;
    menuRename: TMenuItem;
    lblKeepAlive: TLabel;
    spinKeepAlive: TSpinEditEx;
    lblCounterRight2: TLabel;
    lblCounterLeft2: TLabel;
    TimerButtonAnimation: TTimer;
    lblBackgroundColor: TLabel;
    ColorBoxBackgroundColor: TColorBox;
    comboLibrary: TComboBox;
    lblLibrary: TLabel;
    pnlLeft: TPanel;
    ListSessions: TLazVirtualStringTree;
    editSearch: TEditButton;
    popupHost: TPopupMenu;
    menuFindDatabaseFiles: TMenuItem;
    menuAddDatabaseFiles: TMenuItem;
    lblIgnoreDatabasePattern: TLabel;
    editIgnoreDatabasePattern: TEdit;
    ActionListConnections: TActionList;
    actFilter: TAction;
    Filter1: TMenuItem;
    chkLogFileDdl: TCheckBox;
    editLogFilePath: TEditButton;
    tabSSL: TTabSheet;
    chkWantSSL: TCheckBox;
    lblSSLPrivateKey: TLabel;
    lblSSLCACertificate: TLabel;
    lblSSLCertificate: TLabel;
    lblSSLcipher: TLabel;
    editSSLcipher: TEdit;
    editSSLCertificate: TEditButton;
    editSSLCACertificate: TEditButton;
    editSSLPrivateKey: TEditButton;
    lblLogFile: TLabel;
    chkLogFileDml: TCheckBox;
    timerEditFilterDelay: TTimer;
    comboSSHExe: TComboBox;
    chkSSHActive: TCheckBox;
    comboSSLVerification: TComboBox;
    lblSSLVerification: TLabel;
    menuFoldersAtTop: TMenuItem;
    procedure btnNewDropdown(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure btnOpenClick(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure btnSaveClick(Sender: TObject);
    procedure btnSaveAsClick(Sender: TObject);
    procedure btnNewClick(Sender: TObject);
    procedure btnDeleteClick(Sender: TObject);
    procedure Modification(Sender: TObject);
    procedure ListSessionsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; TextType: TVSTTextType; var CellText: String);
    procedure ListSessionsFocusChanged(Sender: TBaseVirtualTree;
      Node: PVirtualNode; Column: TColumnIndex);
    procedure ListSessionsGetImageIndex(Sender: TBaseVirtualTree;
      Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
      var Ghosted: Boolean; var ImageIndex: Integer);
    procedure ListSessionsNewText(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; NewText: String);
    procedure ListSessionsFocusChanging(Sender: TBaseVirtualTree; OldNode,
      NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex;
      var Allowed: Boolean);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure TimerStatisticsTimer(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure ListSessionsCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex;
      out EditLink: IVTEditLink);
    procedure PickFile(Sender: TObject);
    procedure editHostChange(Sender: TObject);
    procedure editDatabasesRightButtonClick(Sender: TObject);
    procedure chkLoginPromptClick(Sender: TObject);
    procedure ListSessionsGetNodeDataSize(Sender: TBaseVirtualTree;
      var NodeDataSize: Integer);
    procedure comboNetTypeChange(Sender: TObject);
    procedure splitterMainMoved(Sender: TObject);
    procedure btnImportSettingsClick(Sender: TObject);
    procedure timerSettingsImportTimer(Sender: TObject);
    procedure ListSessionsStructureChange(Sender: TBaseVirtualTree;
      Node: PVirtualNode; Reason: TChangeReason);
    procedure ListSessionsDragOver(Sender: TBaseVirtualTree; Source: TObject;
      Shift: TShiftState; State: TDragState; const Pt: TPoint; Mode: TDropMode;
      var Effect: LongWord; var Accept: Boolean);
    procedure ListSessionsDragDrop(Sender: TBaseVirtualTree; Source: TObject;
      DataObject: IDataObject; Formats: TFormatArray; Shift: TShiftState;
      const Pt: TPoint; var Effect: LongWord; Mode: TDropMode);
    procedure btnMoreClick(Sender: TObject);
    procedure menuRenameClick(Sender: TObject);
    procedure TimerButtonAnimationTimer(Sender: TObject);
    procedure ColorBoxBackgroundColorGetColors(Sender: TCustomColorBox;
      Items: TStrings);
    procedure editTrim(Sender: TObject);
    procedure editSearchChange(Sender: TObject);
    procedure editSearchRightButtonClick(Sender: TObject);
    procedure editHostDblClick(Sender: TObject);
    procedure ListSessionsNodeDblClick(Sender: TBaseVirtualTree;
      const HitInfo: THitInfo);
    procedure FindAddDatabaseFilesClick(Sender: TObject);
    procedure FormResize(Sender: TObject);
    procedure ListSessionsBeforeCellPaint(Sender: TBaseVirtualTree;
      TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
      CellPaintMode: TVTCellPaintMode; CellRect: TRect; var ContentRect: TRect);
    procedure actFilterExecute(Sender: TObject);
    procedure timerEditFilterDelayTimer(Sender: TObject);
    procedure chkSSHActiveClick(Sender: TObject);
    procedure PageControlDetailsChange(Sender: TObject);
    procedure editUsernameRightButtonClick(Sender: TObject);
    procedure ListSessionsCompareNodes(Sender: TBaseVirtualTree; Node1,
      Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
    procedure menuFoldersAtTopClick(Sender: TObject);
  private
    { Private declarations }
    FLoaded: Boolean;
    FSessionModified, FOnlyPasswordModified: Boolean;
    FServerVersion: String;
    FSettingsImportWaitTime: Cardinal;
    FPopupDatabases: TPopupMenu;
    FPopupCiphers: TPopupMenu;
    FButtonAnimationStep: Integer;
    FLastSelectedNetTypeGroup: TNetTypeGroup;
    function GetSelectedNetType: TNetType;
    procedure SetSelectedNetType(Value: TNetType);
    procedure RefreshSessions(ParentNode: PVirtualNode);
    function SelectedSessionPath: String;
    function CurrentParams: TConnectionParameters;
    procedure FinalizeModifications(var CanProceed: Boolean);
    procedure ValidateControls;
    function NodeSessionNames(Node: PVirtualNode; var RegKey: String): TStringList;
    function GetWindowCaption: String;
    procedure MenuDatabasesClick(Sender: TObject);
    procedure MenuCiphersClick(Sender: TObject);
    procedure RefreshBackgroundColors;
    property SelectedNetType: TNetType read GetSelectedNetType write SetSelectedNetType;
  public
    { Public declarations }
  end;


implementation

uses Main, apphelpers, dbstructures.sqlite, grideditlinks;

{$I const.inc}

{$R *.lfm}


function Tconnform.GetWindowCaption: String;
begin
  Result := APPNAME + ' ' + MainForm.AppVersion + ' - ' + _('Session manager');
  if not SelectedSessionPath.IsEmpty then
    Result := Result + ': ' + SelectedSessionPath;
end;


procedure Tconnform.FormCreate(Sender: TObject);
var
  NetTypeStr, FilenameHint, ExePath, ExeFile: String;
  nt: TNetType;
  ntg: TNetTypeGroup;
  Params: TConnectionParameters;
  ComboItem: TComboExItem;
  Placeholders: TStringList;
  i: Integer;
  ExeFiles: TStringList;
begin
  Width := AppSettings.ReadInt(asSessionManagerWindowWidth);
  Height := AppSettings.ReadInt(asSessionManagerWindowHeight);
  // Move to visible area if window was on a now plugged off monitor previously
  MakeFullyVisible;
  pnlLeft.Width := AppSettings.ReadInt(asSessionManagerListWidth);
  splitterMain.OnMoved(Sender);

  Caption := GetWindowCaption;

  FixVT(ListSessions);
  ListSessions.OnHeaderClick := MainForm.AnyGridHeaderClick;
  ListSessions.OnHeaderDraggedOut := MainForm.AnyGridHeaderDraggedOut;
  btnImportSettings.Caption := MainForm.actImportSettings.Caption;
  FLoaded := False;
  menuFoldersAtTop.Checked := AppSettings.ReadBool(asSessionManagerListFoldersAtTop);

  comboNetType.Clear;
  Params := TConnectionParameters.Create;
  for ntg := Low(ntg) to High(ntg) do begin
    for nt:=Low(nt) to High(nt) do begin
      Params.NetType := nt;
      if Params.GetNetTypeGroup <> ntg then
        Continue;
      NetTypeStr := Params.NetTypeName(True);
      ComboItem := TComboExItem.Create(comboNetType.ItemsEx);
      ComboItem.Caption := NetTypeStr;
      ComboItem.ImageIndex := Params.ImageIndex;
      ComboItem.Data := Pointer(nt);
    end;
  end;
  Params.Free;

  // Create filename placeholders hint
  Placeholders := GetOutputFilenamePlaceholders;
  FilenameHint := _('Allows the following replacement patterns:');
  for i:=0 to Placeholders.Count-1 do begin
    FilenameHint := FilenameHint + CRLF + '%' + Placeholders.Names[i] + ': ' + Placeholders.ValueFromIndex[i];
  end;
  Placeholders.Free;
  editLogFilePath.Hint := FilenameHint;

  // Populate dropdown with supported SSH executables
  {$IFDEF WINDOWS}
  ExeFiles := FindAllFiles(GetAppDir, '*.exe', False);
  for ExePath in ExeFiles do begin
    ExeFile := ExtractFileName(ExePath);
    if ExecRegExprI('([pk]link|putty)', ExeFile) then begin
      comboSSHExe.Items.Add(ExeFile);
    end;
  end;
  ExeFiles.Free;
  comboSSHExe.Items.Add('ssh.exe');
  {$ELSE}
  // Linux and macOS go here, no executable shipped
  comboSSHExe.Items.Add('ssh');
  {$ENDIF}

  // Translate combo items
  for i:=0 to comboSSLVerification.Items.Count-1 do begin
    comboSSLVerification.Items[i] := _(comboSSLVerification.Items[i]);
  end;
end;

procedure Tconnform.btnNewDropdown(Sender: TObject);
begin
  ShowPopup(btnNew, popupNew);
end;


procedure Tconnform.RefreshSessions(ParentNode: PVirtualNode);
var
  SessionNames: TStringList;
  RegKey: String='';
  i: Integer;
  Params: TConnectionParameters;
  SessNode: PVirtualNode;
begin
  // Initialize session tree
  // And while we're at it, collect custom colors for background color selector
  if ParentNode=nil then begin
    ListSessions.Clear;
  end else begin
    ListSessions.DeleteChildren(ParentNode, True);
  end;
  SessionNames := NodeSessionNames(ParentNode, RegKey);
  for i:=0 to SessionNames.Count-1 do begin
    Params := TConnectionParameters.Create(RegKey+SessionNames[i]);
    SessNode := ListSessions.AddChild(ParentNode, PConnectionParameters(Params));
    if Params.IsFolder then begin
      SessNode.Dummy := 1; // We use this Byte value later in CompareNodes
      RefreshSessions(SessNode);
    end
    else begin
      SessNode.Dummy := 0;
    end;
  end;
  if not Assigned(ParentNode) then begin
    RefreshBackgroundColors;
    ListSessions.SortTree(ListSessions.Header.SortColumn, ListSessions.Header.SortDirection);
  end;
end;


procedure Tconnform.FormResize(Sender: TObject);
begin
  splitterMainMoved(splitterMain);
end;

procedure Tconnform.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  // Modifications? Ask if they should be saved.
  FinalizeModifications(CanClose);
end;


procedure Tconnform.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  // Suspend calculating statistics as long as they're not visible
  TimerStatistics.Enabled := False;
end;


procedure Tconnform.FormShow(Sender: TObject);
var
  LastActiveSession: String;
  LastSessions: TStringList;
  PSess: PConnectionParameters;
  Node: PVirtualNode;
begin
  RestoreListSetup(ListSessions);

  // Init sessions tree
  RefreshSessions(nil);

  // Focus last session
  SelectNode(ListSessions, nil);
  LastSessions := Explode(DELIM, AppSettings.ReadString(asLastSessions));
  LastActiveSession := AppSettings.ReadString(asLastActiveSession);
  if (LastActiveSession = '') and (LastSessions.Count > 0) then
    LastActiveSession := LastSessions[0];
  Node := ListSessions.GetFirst;
  while Assigned(Node) do begin
    PSess := ListSessions.GetNodeData(Node);
    if PSess.SessionPath = LastActiveSession then
      SelectNode(ListSessions, Node);
    Node := ListSessions.GetNext(Node);
  end;

  ListSessions.SetFocus;
  // Reactivate statistics
  TimerStatistics.Enabled := True;
  TimerStatistics.OnTimer(Sender);
  FLoaded := True;
end;


function Tconnform.GetSelectedNetType: TNetType;
begin
  Result := TNetType(comboNetType.ItemsEx[comboNetType.ItemIndex].Data);
end;


procedure Tconnform.SetSelectedNetType(Value: TNetType);
var
  i: Integer;
begin
  for i:=0 to comboNetType.ItemsEx.Count-1 do begin
    if TNetType(comboNetType.ItemsEx[i].Data) = Value then begin
      comboNetType.ItemIndex := i;
      Break;
    end;
  end;
end;


procedure Tconnform.btnOpenClick(Sender: TObject);
var
  Connection: TDBConnection;
  Params: TConnectionParameters;
begin
  // Connect to selected session
  Params := CurrentParams;

  if not btnOpen.Enabled then
    Exit;
  btnOpen.Enabled := False;
  FButtonAnimationStep := 0;
  TimerButtonAnimation.Enabled := True;
  Screen.Cursor := crHourglass;
  Connection := nil;
  if Mainform.InitConnection(Params, True, Connection) then
    ModalResult := mrOK
  else begin
    TimerStatistics.OnTimer(Sender);
    ModalResult := mrNone;
  end;
  TimerButtonAnimation.Enabled := False;
  btnOpen.Enabled := True;
  btnOpen.Caption := _('Open');
  Screen.Cursor := crDefault;
end;

procedure Tconnform.FormDestroy(Sender: TObject);
begin
  // Save GUI stuff
  AppSettings.WriteInt(asSessionManagerListWidth, ScaleFormToDesign(pnlLeft.Width));
  AppSettings.WriteInt(asSessionManagerWindowWidth, ScaleFormToDesign(Width));
  AppSettings.WriteInt(asSessionManagerWindowHeight, ScaleFormToDesign(Height));
  SaveListSetup(ListSessions);
end;


procedure Tconnform.btnSaveClick(Sender: TObject);
var
  Sess: PConnectionParameters;
  Conn: TDBConnection;
begin
  // Overtake edited values for current parameter object and save to registry
  if Assigned(ListSessions.FocusedNode) then begin
    Sess := ListSessions.GetNodeData(ListSessions.FocusedNode);
    Sess.Hostname := editHost.Text;
    Sess.Username := editUsername.Text;
    Sess.Password := editPassword.Text;
    Sess.LoginPrompt := chkLoginPrompt.Checked;
    Sess.WindowsAuth := chkWindowsAuth.Checked;
    Sess.CleartextPluginEnabled := chkCleartextPluginEnabled.Checked;
    Sess.ForceUnicode := chkForceUnicode.Checked;
    Sess.Port := spinPort.Value;
    Sess.NetType := SelectedNetType;
    Sess.Compressed := chkCompressed.Checked;
    Sess.QueryTimeout := spinQueryTimeout.Value;
    Sess.KeepAlive := spinKeepAlive.Value;
    Sess.LocalTimeZone := chkLocalTimeZone.Checked;
    Sess.FullTableStatus := chkFullTableStatus.Checked;
    Sess.SessionColor := ColorBoxBackgroundColor.Selected;
    Sess.LibraryOrProvider := comboLibrary.Text;
    Sess.AllDatabasesStr := editDatabases.Text;
    Sess.Comment := memoComment.Text;
    Sess.StartupScriptFilename := editStartupScript.Text;
    Sess.SSHActive := chkSSHActive.Enabled and chkSSHActive.Checked;
    Sess.SSHExe := comboSSHExe.Text;
    Sess.SSHHost := editSSHhost.Text;
    Sess.SSHPort := spinSSHport.Value;
    Sess.SSHUser := editSSHUser.Text;
    Sess.SSHPassword := editSSHPassword.Text;
    Sess.SSHTimeout := spinSSHTimeout.Value;
    Sess.SSHPrivateKey := editSSHPrivateKey.Text;
    Sess.SSHLocalPort := spinSSHlocalport.Value;
    Sess.WantSSL := chkWantSSL.Checked;
    Sess.SSLPrivateKey := editSSLPrivateKey.Text;
    Sess.SSLCertificate := editSSLCertificate.Text;
    Sess.SSLCACertificate := editSSLCACertificate.Text;
    Sess.SSLCipher := editSSLCipher.Text;
    Sess.SSLVerification := comboSSLVerification.ItemIndex;
    Sess.IgnoreDatabasePattern := editIgnoreDatabasePattern.Text;
    Sess.LogFileDdl := chkLogFileDdl.Checked;
    Sess.LogFileDml := chkLogFileDml.Checked;
    Sess.LogFilePath := editLogFilePath.Text;
    Sess.SaveToRegistry;

    // Apply session color (and othher settings) to opened connection(s)
    for Conn in MainForm.Connections do begin
      if Conn.Parameters.SessionPath = Sess.SessionPath then begin
        Conn.Parameters.SessionColor := Sess.SessionColor;
        MainForm.DBtree.Invalidate;
      end;
    end;
  end;

  FSessionModified := False;
  ListSessions.Invalidate;
  RefreshBackgroundColors;
  ValidateControls;
end;


procedure Tconnform.btnMoreClick(Sender: TObject);
begin
  ShowPopup(btnMore, popupMore);
end;


procedure Tconnform.btnSaveAsClick(Sender: TObject);
var
  newName, ParentKey: String;
  NameOK: Boolean;
  NewSess: TConnectionParameters;
  Node: PVirtualNode;
  SessionNames: TStringList;
begin
  // Save session as ...
  newName := _('Enter new session name ...');
  NameOK := False;
  ParentKey := '';
  SessionNames := NodeSessionNames(ListSessions.FocusedNode.Parent, ParentKey);
  while not NameOK do begin
    if not InputQuery(_('Clone session ...'), _('New session name:'), newName) then
      Exit; // Cancelled
    NameOK := SessionNames.IndexOf(newName) = -1;
    if not NameOK then
      ErrorDialog(f_('Session "%s" already exists!', [ParentKey+newName]))
    else begin
      // Create the key and save its values
      NewSess := CurrentParams;
      NewSess.SessionPath := ParentKey+newName;
      NewSess.SaveToRegistry;
      Node := ListSessions.InsertNode(ListSessions.FocusedNode, amInsertAfter, PConnectionParameters(NewSess));
      FSessionModified := False;
      SelectNode(ListSessions, Node);
    end;
  end;
  SessionNames.Free;
end;


procedure Tconnform.btnImportSettingsClick(Sender: TObject);
begin
  MainForm.actImportSettings.Execute;
  FSettingsImportWaitTime := 0;
  timerSettingsImport.Enabled := MainForm.ImportSettingsDone;
end;


procedure Tconnform.timerSettingsImportTimer(Sender: TObject);
begin
  Inc(FSettingsImportWaitTime, timerSettingsImport.Interval);
  RefreshSessions(nil);
  if ListSessions.RootNodeCount > 0 then
    timerSettingsImport.Enabled := False;
  if FSettingsImportWaitTime >= 10000 then begin
    timerSettingsImport.Enabled := False;
    MessageDialog(f_('Imported sessions could not be detected. Restarting %s may solve that.', [APPNAME]), mtWarning, [mbOK]);
  end;
end;


procedure Tconnform.btnNewClick(Sender: TObject);
var
  i: Integer;
  CanProceed, CreateInRoot: Boolean;
  NewSess: TConnectionParameters;
  ParentSess: PConnectionParameters;
  ParentNode, NewNode: PVirtualNode;
  ParentPath: String;
  SiblingSessionNames: TStringList;
begin
  // Create new session or folder
  CanProceed := False;
  FinalizeModifications(CanProceed);
  if not CanProceed then
    Exit;

  CreateInRoot := (Sender = menuNewSessionInRoot) or (Sender = menuNewFolderInRoot);
  if Assigned(ListSessions.FocusedNode) then
    ParentSess := ListSessions.GetNodeData(ListSessions.FocusedNode)
  else
    ParentSess := nil;
  if CreateInRoot then
    ParentNode := nil
  else begin
    if ParentSess = nil then
      ParentNode := nil
    else if ParentSess.IsFolder then
      ParentNode := ListSessions.FocusedNode
    else
      ParentNode := ListSessions.FocusedNode.Parent;
  end;
  ParentPath := '';
  SiblingSessionNames := NodeSessionNames(ParentNode, ParentPath);

  NewSess := TConnectionParameters.Create;
  NewSess.IsFolder := (Sender = menuNewFolderInRoot) or (Sender = menuContextNewFolderInFolder) or (Sender = menuNewFolderInFolder);
  NewSess.SessionPath := ParentPath + 'Unnamed';
  i := 0;
  while SiblingSessionNames.IndexOf(NewSess.SessionName) > -1 do begin
    inc(i);
    NewSess.SessionPath := ParentPath + 'Unnamed-' + IntToStr(i);
  end;
  NewSess.SaveToRegistry;
  SiblingSessionNames.Free;
  NewNode := ListSessions.AddChild(ParentNode, PConnectionParameters(NewSess));
  // Select it
  SelectNode(ListSessions, NewNode);
  ValidateControls;
  ListSessions.EditNode(NewNode, 0);
end;


procedure Tconnform.actFilterExecute(Sender: TObject);
begin
  editSearch.SetFocus;
end;

procedure Tconnform.btnDeleteClick(Sender: TObject);
var
  Sess: PConnectionParameters;
  Node, FocusNode: PVirtualNode;
begin
  Node := ListSessions.FocusedNode;
  Sess := ListSessions.GetNodeData(Node);
  if MessageDialog(f_('Delete session "%s"?', [Sess.SessionName]), mtConfirmation, [mbYes, mbCancel]) = mrYes then
  begin
    AppSettings.SessionPath := Sess.SessionPath;
    AppSettings.DeleteCurrentKey;
    if Assigned(Node.NextSibling) then
      FocusNode := Node.NextSibling
    else if Assigned(Node.PrevSibling) then
      FocusNode := Node.PrevSibling
    else
      FocusNode := Node.Parent;
    ListSessions.DeleteNode(Node);
    SelectNode(ListSessions, FocusNode);
    ListSessions.SetFocus;
  end;
end;


function Tconnform.SelectedSessionPath: String;
var
  Sess: PConnectionParameters;
begin
  if not Assigned(ListSessions.FocusedNode) then
    Result := ''
  else begin
    Sess := ListSessions.GetNodeData(ListSessions.FocusedNode);
    Result := Sess.SessionPath;
  end;
end;


function Tconnform.CurrentParams: TConnectionParameters;
var
  FromReg: PConnectionParameters;
begin
  // Return non-stored parameters
  if not Assigned(ListSessions.FocusedNode) then begin
    Result := nil;
    Exit;
  end;

  FromReg := ListSessions.GetNodeData(ListSessions.FocusedNode);
  if FromReg.IsFolder then begin
    Result := FromReg^;
  end else begin
    Result := TConnectionParameters.Create;
    Result.SessionPath := SelectedSessionPath;
    Result.Counter := FromReg.Counter;
    Result.SessionColor := ColorBoxBackgroundColor.Selected;
    Result.NetType := SelectedNetType;
    Result.ServerVersion := FServerVersion;
    Result.Hostname := editHost.Text;
    Result.Username := editUsername.Text;
    Result.Password := editPassword.Text;
    Result.LoginPrompt := chkLoginPrompt.Checked;
    Result.WindowsAuth := chkWindowsAuth.Checked;
    Result.CleartextPluginEnabled := chkCleartextPluginEnabled.Checked;
    Result.ForceUnicode := chkForceUnicode.Checked;
    if spinPort.Enabled then
      Result.Port := spinPort.Value
    else
      Result.Port := 0;
    Result.AllDatabasesStr := editDatabases.Text;
    Result.LibraryOrProvider := comboLibrary.Text;
    Result.Comment := memoComment.Text;
    Result.SSHActive := chkSSHActive.Enabled and chkSSHActive.Checked;
    Result.SSHHost := editSSHHost.Text;
    Result.SSHPort := spinSSHPort.Value;
    Result.SSHUser := editSSHuser.Text;
    Result.SSHPassword := editSSHpassword.Text;
    Result.SSHTimeout := spinSSHTimeout.Value;
    Result.SSHPrivateKey := editSSHPrivateKey.Text;
    Result.SSHLocalPort := spinSSHlocalport.Value;
    Result.SSHExe := comboSSHExe.Text;
    Result.WantSSL := chkWantSSL.Checked;
    Result.SSLPrivateKey := editSSLPrivateKey.Text;
    Result.SSLCertificate := editSSLCertificate.Text;
    Result.SSLCACertificate := editSSLCACertificate.Text;
    Result.SSLCipher := editSSLCipher.Text;
    Result.SSLVerification := comboSSLVerification.ItemIndex;
    Result.StartupScriptFilename := editStartupScript.Text;
    Result.Compressed := chkCompressed.Checked;
    Result.QueryTimeout := spinQueryTimeout.Value;
    Result.KeepAlive := spinKeepAlive.Value;
    Result.LocalTimeZone := chkLocalTimeZone.Checked;
    Result.FullTableStatus := chkFullTableStatus.Checked;
    Result.SessionColor := ColorBoxBackgroundColor.Selected;
    Result.IgnoreDatabasePattern := editIgnoreDatabasePattern.Text;
    Result.LogFileDdl := chkLogFileDdl.Checked;
    Result.LogFileDml := chkLogFileDml.Checked;
    Result.LogFilePath := editLogFilePath.Text;
  end;
end;


procedure Tconnform.ListSessionsGetImageIndex(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
  var Ghosted: Boolean; var ImageIndex: Integer);
var
  Sess: PConnectionParameters;
begin
  // An edited session gets an additional pencil symbol
  if Column > 0 then
    ImageIndex := -1
  else case Kind of
    ikNormal, ikSelected: begin
      Sess := Sender.GetNodeData(Node);
      ImageIndex := Sess.ImageIndex;
    end;

    ikOverlay:
      if (Node = Sender.FocusedNode) and FSessionModified then
        ImageIndex := 162;

  end;
end;


procedure Tconnform.ListSessionsGetNodeDataSize(Sender: TBaseVirtualTree;
  var NodeDataSize: Integer);
begin
  NodeDataSize := SizeOf(TConnectionParameters);
end;


procedure Tconnform.ListSessionsGetText(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
  var CellText: String);
var
  Sess: PConnectionParameters;
begin
  // Display session name cell
  Sess := Sender.GetNodeData(Node);
  if Sess.IsFolder then begin
    case Column of
      0: CellText := Sess.SessionName;
      else CellText := '';
    end;
  end else begin
    case Column of
      0: begin
        CellText := Sess.SessionName;
        if FSessionModified and (Node = Sender.FocusedNode) and (not Sender.IsEditing) then
          CellText := CellText + ' *';
      end;
      1: CellText := Sess.Hostname;
      2: CellText := Sess.Port.ToString;
      3: CellText := Sess.Username;
      4: CellText := Sess.ServerVersion;
      5: if Sess.LastConnect>0 then
          CellText := DateTimeToStr(Sess.LastConnect)
        else
          CellText := '';
      6: CellText := FormatNumber(Sess.Counter);
      7: CellText := Sess.Comment;
    end;
  end;
end;


function Tconnform.NodeSessionNames(Node: PVirtualNode; var RegKey: String): TStringList;
var
  Sess: PConnectionParameters;
  Folders: TStringList;
begin
  // Find sibling session names in a folder node

  if Node = nil then
    Node := ListSessions.RootNode;

  // Find registry sub path for given node
  RegKey := '';
  if Node <> ListSessions.RootNode then begin
    Sess := ListSessions.GetNodeData(Node);
    RegKey := AppSettings.AppendDelimiter(Sess.SessionPath);
  end;

  // Fetch from registry
  Folders := TStringList.Create;
  Result := AppSettings.GetSessionNames(RegKey, Folders);
  Result.AddStrings(Folders);
  Folders.Free;
end;


procedure Tconnform.ListSessionsBeforeCellPaint(Sender: TBaseVirtualTree;
  TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex;
  CellPaintMode: TVTCellPaintMode; CellRect: TRect; var ContentRect: TRect);
var
  Session: PConnectionParameters;
begin
  // Paint custom background color
  if CellPaintMode=cpmPaint then begin
    Session := Sender.GetNodeData(Node);
    if Session.SessionColor <> AppSettings.GetDefaultInt(asTreeBackground) then begin
      TargetCanvas.Brush.Color := Session.SessionColor;
      TargetCanvas.FillRect(CellRect);
    end;
  end;
end;

procedure Tconnform.ListSessionsCompareNodes(Sender: TBaseVirtualTree; Node1,
  Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
var
  VT: TVirtualStringTree;
  DirectionMarker: Integer;
begin
  VT := Sender as TLazVirtualStringTree;
  if Assigned(Node1) and Assigned(Node2) then begin
    // This marker when set to -1 ensures folders are at the top
    DirectionMarker := IfThen(VT.Header.SortDirection = sdAscending, 1, -1);
    if menuFoldersAtTop.Checked and (Node1.Dummy=1) and (Node2.Dummy<>1) then
      Result := -1 * DirectionMarker
    else if menuFoldersAtTop.Checked and (Node1.Dummy<>1) and (Node2.Dummy=1) then
      Result := 1 * DirectionMarker
    else
      Result := CompareAnyNode(VT.Text[Node1, Column], VT.Text[Node2, Column]);
  end;
end;

procedure Tconnform.ListSessionsCreateEditor(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Column: TColumnIndex; out EditLink: IVTEditLink);
begin
  // Use our own text editor to rename a session
  EditLink := TInplaceEditorLink.Create(Sender as TVirtualStringTree, True, nil);
end;


procedure Tconnform.ListSessionsDragDrop(Sender: TBaseVirtualTree;
  Source: TObject; DataObject: IDataObject; Formats: TFormatArray;
  Shift: TShiftState; const Pt: TPoint; var Effect: LongWord; Mode: TDropMode);
var
  TargetNode, ParentNode: PVirtualNode;
  AttachMode: TVTNodeAttachMode;
  TargetSess, FocusedSess: PConnectionParameters;
  ParentKey: String;
  SiblingSessions: TStringList;
begin
  TargetNode := Sender.GetNodeAt(Pt.X, Pt.Y);
  if not Assigned(TargetNode) then begin
    Beep;
    Exit;
  end;
  TargetSess := Sender.GetNodeData(TargetNode);
  FocusedSess := Sender.GetNodeData(ListSessions.FocusedNode);
  case Mode of
    dmAbove:
      AttachMode := amInsertBefore;
    dmOnNode:
      if TargetSess.IsFolder then
        AttachMode := amAddChildFirst
      else
        AttachMode := amInsertBefore;
    dmBelow:
      AttachMode := amInsertAfter;
    else
      AttachMode := amInsertAfter;
  end;
  if AttachMode in [amInsertBefore, amInsertAfter] then
    ParentNode := TargetNode.Parent
  else
    ParentNode := TargetNode;

  ParentKey := '';
  SiblingSessions := NodeSessionNames(ParentNode, ParentKey);
  // Test if target folder has an equal named node
  if SiblingSessions.IndexOf(FocusedSess.SessionName) > -1 then
    ErrorDialog(f_('Session "%s" already exists!', [ParentKey+FocusedSess.SessionName]))
  else begin
    try
      AppSettings.SessionPath := FocusedSess.SessionPath;
      AppSettings.MoveCurrentKey(AppSettings.AppendDelimiter(REGKEY_SESSIONS)+ParentKey+FocusedSess.SessionName);
      ListSessions.MoveTo(ListSessions.FocusedNode, TargetNode, AttachMode, False);
      FocusedSess.SessionPath := ParentKey+FocusedSess.SessionName;
    except
      on E:Exception do
        ErrorDialog(f_('Error while moving registry key: %s', [E.Message]));
    end;
  end;
  SiblingSessions.Free;
end;


procedure Tconnform.ListSessionsDragOver(Sender: TBaseVirtualTree;
  Source: TObject; Shift: TShiftState; State: TDragState; const Pt: TPoint;
  Mode: TDropMode; var Effect: LongWord; var Accept: Boolean);
var
  TargetNode, ParentNode: PVirtualNode;
  TargetSess: PConnectionParameters;
begin
  // Allow node dragging everywhere except within the current folder
  TargetNode := Sender.GetNodeAt(Pt.X, Pt.Y);
  TargetSess := Sender.GetNodeData(TargetNode);
  Accept := (Source = Sender)
    and Assigned(TargetSess)
    and (Mode <> dmNowhere);
  if Accept and (Mode = dmOnNode) and (TargetNode = ListSessions.FocusedNode.Parent) then
    Accept := False;
  if Accept and (Mode in [dmAbove, dmBelow]) and (TargetNode.Parent = ListSessions.FocusedNode.Parent) then
    Accept := False;
  // Moving a folder into itself would create an infinite folder structure
  if Accept and TargetSess.IsFolder then
    Accept := Accept and (TargetNode <> ListSessions.FocusedNode);
  if Accept and (not TargetSess.IsFolder) then
    Accept := Accept and (TargetNode.Parent <> ListSessions.FocusedNode.Parent);

  if Accept then begin
    // Do not allow focused node to be moved somewhere below itself
    ParentNode := TargetNode.Parent;
    while Assigned(ParentNode) do begin
      Accept := Accept and (ParentNode <> ListSessions.FocusedNode);
      if not Accept then
        Break;
      ParentNode := ParentNode.Parent;
    end;
    // Shows the right tooltip on Aero GUI
    Effect := DROPEFFECT_MOVE;
  end;
end;


procedure Tconnform.ListSessionsFocusChanged(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex);
var
  SessionFocused, InFolder: Boolean;
  Sess: PConnectionParameters;
begin
  // select one connection!
  Screen.Cursor := crHourglass;
  TimerStatistics.Enabled := False;
  SessionFocused := False;
  InFolder := False;
  Sess := nil;
  if Assigned(Node) then begin
    Sess := Sender.GetNodeData(Node);
    SessionFocused := not Sess.IsFolder;
    InFolder := (ListSessions.GetNodeLevel(Node) > 0) or Sess.IsFolder;
  end;
  FLoaded := False;
  tabStart.TabVisible := not SessionFocused;
  tabSettings.TabVisible := SessionFocused;
  tabSSHtunnel.TabVisible := SessionFocused;
  tabAdvanced.TabVisible := SessionFocused;
  tabSSL.TabVisible := SessionFocused;
  tabStatistics.TabVisible := SessionFocused;
  menuRename.Enabled := Assigned(Node);
  menuNewSessionInFolder.Enabled := InFolder;
  menuNewFolderInFolder.Enabled := InFolder;
  FreeAndNil(FPopupDatabases);
  FreeAndNil(FPopupCiphers);

  if not SessionFocused then begin
    PageControlDetails.ActivePage := tabStart;
    if ListSessions.RootNodeCount = 0 then begin
      lblHelp.Caption := f_('New here? In order to connect to a server, you have to create a so called '+
        '"session" at first. Just click the "New" button on the bottom left to create your first session. '+
        'Give it a friendly name (e.g. "Local DB server") so you''ll recall it the next time you start %s.', [APPNAME]);
      lblHelpPortable.Visible := AppSettings.PortableMode;
    end
    else begin
      lblHelp.Caption := _('Please click a session on the left list to edit parameters, doubleclick to open it.');
      lblHelpPortable.Visible := False;
    end;
  end else begin
    PageControlDetails.ActivePage := tabSettings;

    SelectedNetType := Sess.NetType;
    FLastSelectedNetTypeGroup := Sess.NetTypeGroup;
    editHost.Text := Sess.Hostname;
    editUsername.Text := Sess.Username;
    editPassword.Text := Sess.Password;
    chkLoginPrompt.Checked := Sess.LoginPrompt;
    chkWindowsAuth.Checked := Sess.WindowsAuth;
    chkCleartextPluginEnabled.Checked := Sess.CleartextPluginEnabled;
    chkForceUnicode.Checked := Sess.ForceUnicode;
    spinPort.Value := Sess.Port;
    chkCompressed.Checked := Sess.Compressed;
    spinQueryTimeout.Value := Sess.QueryTimeout;
    spinKeepAlive.Value := Sess.KeepAlive;
    chkLocalTimeZone.Checked := Sess.LocalTimeZone;
    chkFullTableStatus.Checked := Sess.FullTableStatus;
    ColorBoxBackgroundColor.Items.Objects[0] := TObject(Sess.SessionColor);
    if Sess.SessionColor = clNone then
      ColorBoxBackgroundColor.Selected := Sess.SessionColor
    else
      ColorBoxBackgroundColor.ItemIndex := 0;
    editDatabases.Text := Sess.AllDatabasesStr;
    comboLibrary.Items := Sess.GetLibraries;
    comboLibrary.ItemIndex := comboLibrary.Items.IndexOf(Sess.LibraryOrProvider);
    if (comboLibrary.ItemIndex = -1) and (comboLibrary.Items.Count > 0) then begin
      comboLibrary.ItemIndex := 0;
    end;
    memoComment.Text := Sess.Comment;
    editStartupScript.Text := Sess.StartupScriptFilename;
    chkSSHActive.Checked := Sess.SSHActive;
    comboSSHExe.Text := Sess.SSHExe;
    editSSHHost.Text := Sess.SSHHost;
    spinSSHport.Value := Sess.SSHPort;
    editSSHUser.Text := Sess.SSHUser;
    editSSHPassword.Text := Sess.SSHPassword;
    spinSSHTimeout.Value := Sess.SSHTimeout;
    editSSHPrivateKey.Text := Sess.SSHPrivateKey;
    spinSSHlocalport.Value := Sess.SSHLocalPort;
    chkWantSSL.Checked := Sess.WantSSL;
    editSSLPrivateKey.Text := Sess.SSLPrivateKey;
    editSSLCertificate.Text := Sess.SSLCertificate;
    editSSLCACertificate.Text := Sess.SSLCACertificate;
    editSSLCipher.Text := Sess.SSLCipher;
    comboSSLVerification.ItemIndex := Sess.SSLVerification;
    editIgnoreDatabasePattern.Text := Sess.IgnoreDatabasePattern;
    chkLogFileDdl.Checked := Sess.LogFileDdl;
    chkLogFileDml.Checked := Sess.LogFileDml;
    editLogFilePath.Text := Sess.LogFilePath;
    FServerVersion := Sess.ServerVersion;
  end;

  FLoaded := True;
  FSessionModified := False;
  ListSessions.Repaint;
  ValidateControls;
  TimerStatistics.Enabled := True;
  TimerStatistics.OnTimer(Sender);

  Screen.Cursor := crDefault;
end;


procedure Tconnform.RefreshBackgroundColors;
begin
  // Trigger OnGetColors event
  ColorBoxBackgroundColor.Style := ColorBoxBackgroundColor.Style - [cbCustomColors];
  ColorBoxBackgroundColor.Style := ColorBoxBackgroundColor.Style + [cbCustomColors];
end;


procedure Tconnform.TimerStatisticsTimer(Sender: TObject);
var
  LastConnect, Created: TDateTime;
begin
  // Continuously update statistics labels
  lblLastConnectRight.Caption := _('unknown or never');
  lblLastConnectRight.Hint := '';
  lblLastConnectRight.Enabled := False;
  lblCreatedRight.Caption := _('unknown');
  lblCreatedRight.Hint := '';
  lblCreatedRight.Enabled := False;
  lblCounterRight1.Caption := '';
  lblCounterRight2.Caption := '';

  if not Assigned(ListSessions.FocusedNode) then
    Exit;

  AppSettings.SessionPath := SelectedSessionPath;
  if AppSettings.SessionPath.IsEmpty then
    Exit;

  LastConnect := StrToDateTimeDef(AppSettings.ReadString(asLastConnect), DateTimeNever);
  if LastConnect <> DateTimeNever then begin
    lblLastConnectRight.Hint := DateTimeToStr(LastConnect);
    lblLastConnectRight.Caption := DateBackFriendlyCaption(LastConnect);
    lblLastConnectRight.Enabled := True;
  end;
  Created := StrToDateTimeDef(AppSettings.ReadString(asSessionCreated), DateTimeNever);
  if Created <> DateTimeNever then begin
    lblCreatedRight.Hint := DateTimeToStr(Created);
    lblCreatedRight.Caption := DateBackFriendlyCaption(Created);
    lblCreatedRight.Enabled := True;
  end;
  lblCounterRight1.Caption := FormatNumber(AppSettings.ReadInt(asConnectCount));
  lblCounterRight2.Caption := FormatNumber(AppSettings.ReadInt(asRefusedCount));
  Invalidate;
end;


procedure Tconnform.ListSessionsFocusChanging(Sender: TBaseVirtualTree; OldNode,
  NewNode: PVirtualNode; OldColumn, NewColumn: TColumnIndex;
  var Allowed: Boolean);
begin
  if NewNode <> OldNode then
    FinalizeModifications(Allowed)
  else
    Allowed := False;
end;


procedure Tconnform.ListSessionsNewText(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Column: TColumnIndex; NewText: String);
var
  ParentKey: String;
  Connection: TDBConnection;
  Sess: PConnectionParameters;
  SiblingSessions: TStringList;
begin
  // Rename session
  // Note that this is triggered only if the text was effectively changed
  Sess := Sender.GetNodeData(Node);

  ParentKey := '';
  SiblingSessions := NodeSessionNames(Node.Parent, ParentKey);

  // Safety replacement for folder separator, see issue #682
  NewText := StringReplace(NewText, AppSettings.PathDelimiter, '-', [rfReplaceAll]);

  if SiblingSessions.IndexOf(NewText) > -1 then begin
    ErrorDialog(
      f_('Session "%s" already exists!', [ParentKey+NewText])
      + sLineBreak + sLineBreak
      + _('If you want to change the case of a session name, you need to rename it before doing the actual case change.')
      );
    NewText := Sess.SessionName;
  end else begin
    AppSettings.SessionPath := Sess.SessionPath;
    AppSettings.MoveCurrentKey(AppSettings.AppendDelimiter(REGKEY_SESSIONS)+ParentKey+NewText);
    // Also fix internal session names in main form, which gets used to store e.g. "lastuseddb" later
    for Connection in MainForm.Connections do begin
      if Connection.Parameters.SessionPath = Sess.SessionPath then
        Connection.Parameters.SessionPath := ParentKey+NewText;
    end;
    MainForm.SetWindowCaption;
    Sess.SessionPath := ParentKey+NewText;
  end;
  SiblingSessions.Free;
end;


procedure Tconnform.ListSessionsNodeDblClick(Sender: TBaseVirtualTree;
  const HitInfo: THitInfo);
const
  AllowedPos: THitPositions=[hiOnItemLabel, hiOnItemLeft, hiOnItemRight, hiOnNormalIcon];
var
  HitPos: THitPosition;
begin
  // Doubleclick to open a connection, only if mouse is really on a node,
  // not e.g. on the expand/collapse icon (see issue #820)
  for HitPos in HitInfo.HitPositions do begin
    if HitPos in AllowedPos then begin
      btnOpen.OnClick(Sender);
      Break;
    end;
  end;
end;


procedure Tconnform.ListSessionsStructureChange(Sender: TBaseVirtualTree;
  Node: PVirtualNode; Reason: TChangeReason);
begin
  // Node added or removed. Tree needs a repaint in some cases.
  // TODO: does not work
  Sender.Repaint;
end;


procedure Tconnform.editHostChange(Sender: TObject);
begin
  editSSHhost.TextHint := editHost.Text;
  Modification(Sender);
end;


procedure Tconnform.editHostDblClick(Sender: TObject);
begin
  if CurrentParams.NetType = ntSQLite then
    PickFile(Sender);
end;


procedure Tconnform.editTrim(Sender: TObject);
var
  Edit: TCustomEdit;
  EditButton: TEditButton;
  Trimmed: String;
begin
  // Trim input
  if Sender is TCustomEdit then begin
    Edit := Sender as TCustomEdit;
    Trimmed := Edit.Text;
    Trimmed := Trimmed.Trim([' ', #9]);
    if Edit.Text <> Trimmed then
      Edit.Text := Trimmed;
  end
  else if Sender is TEditButton then begin
    EditButton := Sender as TEditButton;
    Trimmed := EditButton.Text;
    Trimmed := Trimmed.Trim([' ', #9]);
    if EditButton.Text <> Trimmed then
      EditButton.Text := Trimmed;
  end;
end;


procedure Tconnform.chkLoginPromptClick(Sender: TObject);
var
  Checked: Boolean;
begin
  // Login prompt and SQL Server integrated Windows Auth are mutually exclusive
  Checked := TCheckBox(Sender).Checked;
  if Checked and (Sender = chkWindowsAuth) then
    chkLoginPrompt.Checked := False;
  if Checked and (Sender = chkLoginPrompt) then
    chkWindowsAuth.Checked := False;
  Modification(Sender);
end;


procedure Tconnform.chkSSHActiveClick(Sender: TObject);
begin
  if (comboSSHExe.Text = '') and (comboSSHExe.Items.Count > 0) then
    comboSSHExe.ItemIndex := 0;
  Modification(Sender);
end;

procedure Tconnform.ColorBoxBackgroundColorGetColors(Sender: TCustomColorBox;
  Items: TStrings);
var
  Node: PVirtualNode;
  PParams: PConnectionParameters;
  ColorName,
  ColorNamePrefix: String;
begin
  // Collect custom session colors into color selector
  ColorNamePrefix := _('Custom color:') + ' ';
  Node := ListSessions.GetFirst;
  while Assigned(Node) do begin
    PParams := ListSessions.GetNodeData(Node);
    ColorName := ColorNamePrefix + ColorToString(PParams.SessionColor);
    if (PParams.SessionColor <> clNone) and (Items.IndexOf(ColorName) = -1) then begin
      Items.AddObject(ColorName, TObject(PParams.SessionColor));
    end;
    Node := ListSessions.GetNext(Node);
  end;
end;


procedure Tconnform.editDatabasesRightButtonClick(Sender: TObject);
var
  Connection: TDBConnection;
  Params: TConnectionParameters;
  Item: TMenuItem;
  DB: String;
  Databases: TStringList;
begin
  if FPopupDatabases = nil then begin
    // Try to connect and lookup database names
    Params := CurrentParams;
    Connection := Params.CreateConnection(Self);
    Connection.Parameters.AllDatabasesStr := '';
    Connection.LogPrefix := SelectedSessionPath;
    Connection.OnLog := Mainform.LogSQL;
    FPopupDatabases := TPopupMenu.Create(Self);
    //FPopupDatabases.AutoHotkeys := maManual;
    Screen.Cursor := crHourglass;
    try
      Connection.Active := True;
      if Params.NetTypeGroup = ngPgSQL then
        Databases := Connection.GetCol('SELECT datname FROM pg_database WHERE datistemplate=FALSE')
      else
        Databases := Connection.AllDatabases;
      for DB in Databases do begin
        Item := TMenuItem.Create(FPopupDatabases);
        Item.Caption := DB;
        Item.OnClick := MenuDatabasesClick;
        Item.AutoCheck := True;
        Item.RadioItem := Params.IsAnyPostgreSQL;
        FPopupDatabases.Items.Add(Item);
      end;
      Databases.Free;
    except
      // Silence connection errors here - should be sufficient to log them
    end;
    FreeAndNil(Connection);
  end;

  // Check/uncheck items, based on semicolon list
  Databases := Explode(';', editDatabases.Text);
  for Item in FPopupDatabases.Items do begin
    Item.Checked := Databases.IndexOf(Item.Caption) > -1;
  end;
  Databases.Free;

  ShowPopup(editDatabases.Button, FPopupDatabases);
  Screen.Cursor := crDefault;
end;


procedure Tconnform.MenuDatabasesClick(Sender: TObject);
var
  Item: TMenuItem;
  Databases: TStringList;
  SelStart: Integer;
begin
  Databases := TStringList.Create;
  for Item in FPopupDatabases.Items do begin
    if Item.Checked then
      Databases.Add(Item.Caption);
  end;
  SelStart := editDatabases.SelStart;
  editDatabases.Text := Implode(';', Databases);
  editDatabases.SelStart := SelStart;
end;


procedure Tconnform.MenuCiphersClick(Sender: TObject);
begin
  editUsername.Text := TMenuItem(Sender).Caption;
end;


procedure Tconnform.editUsernameRightButtonClick(Sender: TObject);
var
  Params: TConnectionParameters;
  Item: TMenuItem;
  LibraryPath: String;
  Lib: TSQLiteLib;
  p: TPoint;
  i: Integer;
begin
  // Provide supported cipher names
  if FPopupCiphers = nil then begin
    FPopupCiphers := TPopupMenu.Create(Self);
    //FPopupCiphers.AutoHotkeys := maManual;
    Params := CurrentParams;
    LibraryPath := GetAppDir + Params.LibraryOrProvider;
    // Throws EDbError on any failure:
    Lib := TSQLiteLib.CreateWithMultipleCipherFunctions(LibraryPath, Params.DefaultLibrary);
    for i:=1 to Lib.sqlite3mc_cipher_count() do begin
      Item := TMenuItem.Create(FPopupCiphers);
      Item.Caption := AnsiToUtf8(Lib.sqlite3mc_cipher_name(i));
      Item.OnClick := MenuCiphersClick;
      FPopupCiphers.Items.Add(Item);
    end;
    Lib.Free;

  end;

  p := editUsername.ClientToScreen(editUsername.ClientRect.BottomRight);
  FPopupCiphers.Popup(p.X-editUsername.Images.Width, p.Y);
end;


procedure Tconnform.menuFoldersAtTopClick(Sender: TObject);
begin
  AppSettings.WriteBool(asSessionManagerListFoldersAtTop, menuFoldersAtTop.Checked);
  ListSessions.SortTree(ListSessions.Header.SortColumn, ListSessions.Header.SortDirection);
  if ListSessions.SelectedCount > 0 then
    ListSessions.ScrollIntoView(ListSessions.GetFirstSelected, False);
end;

procedure Tconnform.menuRenameClick(Sender: TObject);
begin
  // Start node editor to rename a session
  ListSessions.EditNode(ListSessions.FocusedNode, ListSessions.Header.MainColumn);
end;


procedure Tconnform.comboNetTypeChange(Sender: TObject);
var
  Params: TConnectionParameters;
  Libs: TStringList;
begin
  // Autoset default connection data as long as that was not modified by user
  // and only if net type group has now changed
  if not FLoaded then
    Exit;

  Params := CurrentParams;

  if Params.NetTypeGroup <> FLastSelectedNetTypeGroup then begin
    if not spinPort.Modified then
      spinPort.Value := Params.DefaultPort;
    if not editUsername.Modified then
      editUsername.Text := Params.DefaultUsername;
    if not editIgnoreDatabasePattern.Modified then
      editIgnoreDatabasePattern.Text := Params.DefaultIgnoreDatabasePattern;
    if not editHost.Modified then
      editHost.Text := Params.DefaultHost;
    chkSSHActive.Checked := Params.DefaultSshActive;
  end;

  // Populate libraries combobox. Required on each net group change, and also between
  // SQLite and SQLite-encrypted.
  Libs := Params.GetLibraries;
  mainform.LogSQL(Libs.CommaText);
  if Libs.Text <> comboLibrary.Items.Text then begin
    comboLibrary.Items := Libs;
    comboLibrary.ItemIndex := comboLibrary.Items.IndexOf(Params.DefaultLibrary);
  end;

  FLastSelectedNetTypeGroup := Params.NetTypeGroup;
  FreeAndNil(Params);
  Modification(Sender);
end;


procedure Tconnform.Modification(Sender: TObject);
var
  PasswordModified: Boolean;
  Sess: PConnectionParameters;
begin
  // Some modification -
  if FLoaded then begin
    Sess := ListSessions.GetNodeData(ListSessions.FocusedNode);
    if Sess = nil then
      Exit;

    FSessionModified := (Sess.Hostname <> editHost.Text)
      or (Sess.Username <> editUsername.Text)
      or (Sess.LoginPrompt <> chkLoginPrompt.Checked)
      or (Sess.WindowsAuth <> chkWindowsAuth.Checked)
      or (Sess.CleartextPluginEnabled <> chkCleartextPluginEnabled.Checked)
      or (Sess.ForceUnicode <> chkForceUnicode.Checked)
      or (Sess.Port <> spinPort.Value)
      or (Sess.Compressed <> chkCompressed.Checked)
      or (Sess.QueryTimeout <> spinQueryTimeout.Value)
      or (Sess.KeepAlive <> spinKeepAlive.Value)
      or (Sess.LocalTimeZone <> chkLocalTimeZone.Checked)
      or (Sess.FullTableStatus <> chkFullTableStatus.Checked)
      or (Sess.SessionColor <> ColorBoxBackgroundColor.Selected)
      or (Sess.NetType <> SelectedNetType)
      or (Sess.StartupScriptFilename <> editStartupScript.Text)
      or (Sess.LibraryOrProvider <> comboLibrary.Text)
      or (Sess.AllDatabasesStr <> editDatabases.Text)
      or (Sess.Comment <> memoComment.Text)
      or (Sess.SSHActive <> chkSSHActive.Checked)
      or (Sess.SSHHost <> editSSHHost.Text)
      or (Sess.SSHPort <> spinSSHPort.Value)
      or (Sess.SSHExe <> comboSSHExe.Text)
      or (Sess.SSHLocalPort <> spinSSHlocalport.Value)
      or (Sess.SSHUser <> editSSHUser.Text)
      or (Sess.SSHPassword <> editSSHPassword.Text)
      or (Sess.SSHTimeout <> spinSSHTimeout.Value)
      or (Sess.SSHPrivateKey <> editSSHPrivateKey.Text)
      or (Sess.WantSSL <> chkWantSSL.Checked)
      or (Sess.SSLPrivateKey <> editSSLPrivateKey.Text)
      or (Sess.SSLCertificate <> editSSLCertificate.Text)
      or (Sess.SSLCACertificate <> editSSLCACertificate.Text)
      or (Sess.SSLCipher <> editSSLCipher.Text)
      or (Sess.SSLVerification <> comboSSLVerification.ItemIndex)
      or (Sess.IgnoreDatabasePattern <> editIgnoreDatabasePattern.Text)
      or (Sess.LogFileDdl <> chkLogFileDdl.Checked)
      or (Sess.LogFileDml <> chkLogFileDml.Checked)
      or (Sess.LogFilePath <> editLogFilePath.Text)
      ;
    PasswordModified := Sess.Password <> editPassword.Text;
    FOnlyPasswordModified := PasswordModified and (not FSessionModified);
    FSessionModified := FSessionModified or PasswordModified;
    if (Sender=editHost) or (Sender=editUsername) or (Sender=editPassword) or
      (Sender=comboNetType) or (Sender=chkWindowsAuth) or (Sender=spinPort) or
      (Sender=chkCleartextPluginEnabled) or (Sender=chkForceUnicode)
      then begin
      // Be sure to use the modified connection params next time the user clicks the "Databases" pulldown
      FreeAndNil(FPopupDatabases);
    end;

    ListSessions.Repaint;
    ValidateControls;
  end;
end;


procedure Tconnform.FinalizeModifications(var CanProceed: Boolean);
begin
  if FSessionModified and (not FOnlyPasswordModified) then begin
    case MessageDialog(_('Save modifications?'), f_('Settings for "%s" were changed.', [SelectedSessionPath]), mtConfirmation, [mbYes, mbNo, mbCancel]) of
      mrYes: begin
          btnSave.OnClick(Self);
          CanProceed := True;
        end;
      mrNo: begin
          CanProceed := True;
        end;
      mrCancel: CanProceed := False;
    end;
  end else
    CanProceed := True;
end;


procedure Tconnform.FindAddDatabaseFilesClick(Sender: TObject);
var
  PrevText: String;
begin
  // Append or replace filenames
  PrevText := editHost.Text;
  PickFile(editHost);
  if (Sender = menuAddDatabaseFiles)
    and (not PrevText.IsEmpty)
    and (editHost.Text <> PrevText)
    and (editHost.Text <> '') then begin
    editHost.Text := PrevText + DELIM + editHost.Text;
  end;
end;

procedure Tconnform.ValidateControls;
var
  SessionFocused, FolderFocused: Boolean;
  Params: TConnectionParameters;
begin
  SessionFocused := False;
  FolderFocused := False;
  if Assigned(ListSessions.FocusedNode) then begin
    Params := CurrentParams;
    SessionFocused := not Params.IsFolder;
    FolderFocused := Params.IsFolder;

    if SessionFocused then begin
      // Validate session GUI stuff on "Settings" tab:
      lblHost.Caption := _('Hostname / IP:');
      lblUsername.Caption := _('User')+':';
      lblPassword.Caption := _('Password:');
      lblDatabase.Caption := _('Databases')+':';
      editDatabases.TextHint := _('Separated by semicolon');
      case Params.NetType of
        ntMySQL_NamedPipe: begin
          lblHost.Caption := _('Socket name:');
        end;
        ntPgSQL_TCPIP, ntPgSQL_SSHtunnel: begin
          lblDatabase.Caption := _('Database')+':';
          editDatabases.TextHint := _('Single database name');
        end;
        ntSQLite, ntSQLiteEncrypted: begin
          lblHost.Caption := _('Database filename(s)')+':';
          lblUsername.Caption := _('Cipher')+':';
          lblPassword.Caption := _('Key:');
          lblDatabase.Caption := _('Encryption parameters')+':';
          editDatabases.TextHint := _('Example:') + ' kdf_iter=4000;legacy=1;...';
        end
      end;
      editHost.Button.Enabled := Params.IsAnySQLite;
      chkLoginPrompt.Enabled := Params.NetTypeGroup in [ngMySQL, ngMSSQL, ngPgSQL];
      chkWindowsAuth.Enabled := Params.IsAnyMSSQL or Params.IsAnyMySQL;
      lblUsername.Enabled := (Params.NetTypeGroup in [ngMySQL, ngMSSQL, ngPgSQL, ngInterbase])
        and ((not chkLoginPrompt.Checked) or (not chkLoginPrompt.Enabled))
        and ((not chkWindowsAuth.Checked) or (not chkWindowsAuth.Enabled));
      lblUsername.Enabled := lblUsername.Enabled or (Params.NetType = ntSQLiteEncrypted);
      editUsername.Enabled := lblUsername.Enabled;
      editUsername.Button.Enabled := Params.NetType = ntSQLiteEncrypted;
      lblPassword.Enabled := lblUsername.Enabled;
      editPassword.Enabled := lblUsername.Enabled;
      lblPort.Enabled := Params.NetType in [ntMySQL_TCPIP, ntMySQL_SSHtunnel, ntMySQL_ProxySQLAdmin, ntMySQL_RDS, ntMSSQL_TCPIP, ntPgSQL_TCPIP, ntPgSQL_SSHtunnel, ntInterbase_TCPIP, ntFirebird_TCPIP];
      spinPort.Enabled := lblPort.Enabled;
      chkCompressed.Enabled := Params.IsAnyMySQL;
      lblDatabase.Enabled := Params.NetTypeGroup in [ngMySQL, ngMSSQL, ngPgSQL, ngInterbase];
      lblDatabase.Enabled := lblDatabase.Enabled or (Params.NetType = ntSQLiteEncrypted);
      editDatabases.Enabled := lblDatabase.Enabled;
      editDatabases.Button.Enabled := Params.NetTypeGroup in [ngMySQL, ngMSSQL, ngPgSQL, ngInterbase];
      // SSH tunnel tab:
      chkSSHActive.Enabled := Params.SshSupport;
      lblSSHExe.Enabled := Params.SSHActive;
      comboSSHExe.Enabled := Params.SSHActive;
      lblSSHhost.Enabled := Params.SSHActive;
      editSSHhost.Enabled := Params.SSHActive;
      spinSSHport.Enabled := Params.SSHActive;
      lblSSHUser.Enabled := Params.SSHActive;
      editSSHUser.Enabled := Params.SSHActive;
      lblSSHPassword.Enabled := Params.SSHActive;
      editSSHPassword.Enabled := Params.SSHActive;
      lblSshPassHint.Enabled := Params.SSHActive and (editSSHPassword.Text <> '');
      lblSshPassHint.Visible := not Params.SshIsPlink;
      lblSshPassHint.Caption := f_('Make sure %s is available when using a password', [TSecureShellCmd.SshpassPath]);
      lblSSHTimeout.Enabled := Params.SSHActive;
      spinSSHTimeout.Enabled := Params.SSHActive;
      lblSSHkeyfile.Enabled := Params.SSHActive;
      editSSHPrivateKey.Enabled := Params.SSHActive;
      lblSSHLocalPort.Enabled := Params.SSHActive;
      spinSSHlocalport.Enabled := Params.SSHActive;
      // Advanced tab:
      lblQueryTimeout.Enabled := True;
      spinQueryTimeout.Enabled := lblQueryTimeout.Enabled;
      chkLocalTimeZone.Enabled := Params.NetTypeGroup = ngMySQL;
      chkFullTableStatus.Enabled := (Params.NetTypeGroup in [ngMySQL, ngPgSQL, ngSQLite]) and (Params.NetType <> ntMySQL_ProxySQLAdmin);
      chkCleartextPluginEnabled.Enabled := Params.NetTypeGroup = ngMySQL;
      chkForceUnicode.Enabled := Params.NetTypeGroup = ngMySQL;
      editLogFilePath.Enabled := Params.LogFileDdl or Params.LogFileDml;
      // SSL tab:
      chkWantSSL.Enabled := Params.NetType in [ntMySQL_TCPIP, ntMySQL_SSHtunnel, ntMySQL_ProxySQLAdmin, ntMySQL_RDS, ntPgSQL_TCPIP, ntPgSQL_SSHtunnel];
      lblSSLPrivateKey.Enabled := Params.WantSSL;
      editSSLPrivateKey.Enabled := Params.WantSSL;
      lblSSLCACertificate.Enabled := Params.WantSSL;
      editSSLCACertificate.Enabled := Params.WantSSL;
      lblSSLCertificate.Enabled := Params.WantSSL;
      editSSLCertificate.Enabled := Params.WantSSL;
      lblSSLcipher.Enabled := Params.WantSSL;
      editSSLcipher.Enabled := Params.WantSSL;
      lblSSLVerification.Enabled := Params.WantSSL;
      comboSSLVerification.Enabled := Params.WantSSL;

      Params.Free;
    end;
  end;

  // Main buttons
  btnOpen.Enabled := SessionFocused;
  btnSave.Enabled := SessionFocused and FSessionModified;
  btnDelete.Enabled := SessionFocused or FolderFocused;
  menuSave.Enabled := btnSave.Enabled;
  menuSaveAs.Enabled := SessionFocused;
  menuDelete.Enabled := btnDelete.Enabled;
  TExtForm.PageControlTabHighlight(PageControlDetails);

  Caption := GetWindowCaption;
end;


procedure Tconnform.splitterMainMoved(Sender: TObject);
var
  HorizSpace, ButtonWidth: Integer;
begin
  // Splitter resized - adjust width of bottom left buttons
  ButtonWidth := Round((pnlLeft.Width - 2 * BorderWidth) / 3);
  btnNew.Width := ButtonWidth;
  btnSave.Width := ButtonWidth;
  btnDelete.Width := ButtonWidth;
  btnNew.Left := 0;
  btnSave.Left := btnNew.Left + btnNew.Width + BorderWidth;
  btnDelete.Left := btnSave.Left + btnSave.Width + BorderWidth;

  // Resize bottom right buttons
  HorizSpace := PageControlDetails.Width - 2 * BorderWidth;
  ButtonWidth := Round(HorizSpace / 3);
  ButtonWidth := Max(ButtonWidth, ScaleSize(50));
  ButtonWidth := Min(ButtonWidth, ScaleSize(100));
  btnMore.Width := ButtonWidth;
  btnCancel.Width := ButtonWidth;
  btnOpen.Width := ButtonWidth;
  btnmore.Left := pnlBottom.Width - btnMore.Width;
  btnCancel.Left := btnMore.Left - btnMore.Width - BorderWidth;
  btnOpen.Left := btnCancel.Left - btnCancel.Width - BorderWidth;
end;


procedure Tconnform.TimerButtonAnimationTimer(Sender: TObject);
const
  MaxAnimationSteps = 3;
begin
  // Animate "Open" button
  btnOpen.Caption := _('Open') + StringOfChar(' ', FButtonAnimationStep) +
    '.' +
    StringOfChar(' ', MaxAnimationSteps-FButtonAnimationStep);
  btnOpen.Repaint;
  Inc(FButtonAnimationStep);
  if FButtonAnimationStep >= MaxAnimationSteps then
    FButtonAnimationStep := 0;
end;

procedure Tconnform.PageControlDetailsChange(Sender: TObject);
begin
  ValidateControls;
end;

procedure Tconnform.PickFile(Sender: TObject);
var
  Selector: TExtFileOpenDialog;
  Edit: TEditButton;
  i: Integer;
  Control: TControl;
  FileNames: TStringList;
begin
  // Select startup SQL file, SSL file or whatever button clicked
  Edit := Sender as TEditButton;
  Selector := TExtFileOpenDialog.Create(Self);
  if Edit = editHost then begin
    Selector.AddFileType(FILEFILTER_SQLITEDB, 'SQLite databases');
    Selector.Options := Selector.Options - [ofFileMustExist];
    Selector.Options := Selector.Options + [ofAllowMultiSelect];
    Selector.DefaultExt := FILEEXT_SQLITEDB;
  end else if (Edit = editStartupScript) or (Edit = editLogFilePath) then
    Selector.AddFileType('*.sql', _('SQL files'))
  else if Edit = editSSHPrivateKey then begin
  end
  else begin
    Selector.AddFileType('*.pem', _('Privacy Enhanced Mail certificates'));
    Selector.AddFileType('*.crt', _('Certificates'));
  end;
  Selector.AddFileType('*.*', _('All files'));
  // Find relevant label and set open dialog's title
  for i:=0 to Edit.Parent.ControlCount - 1 do begin
    Control := Edit.Parent.Controls[i];
    if (Control is TLabel) and ((Control as TLabel).FocusControl = Edit) then begin
      Selector.Title := 'Select ' + (Control as TLabel).Caption;
      break;
    end;
  end;
  // Set initial directory to the one from the edit's file
  Selector.InitialDir := ExtractFilePath(Edit.Text);
  if Selector.Execute then begin
    FileNames := TStringList.Create;
    FileNames.Assign(Selector.Files);
    for i:=0 to FileNames.Count-1 do begin
      // Remove path if it's the application directory
      if ExtractFilePath(FileNames[i]) = GetAppDir then
        FileNames[i] := ExtractFileName(FileNames[i]);
    end;
    Edit.Text := Implode(DELIM, FileNames);
    Modification(Selector);
  end;
  Selector.Free;
end;


procedure Tconnform.editSearchChange(Sender: TObject);
begin
  // Filter session nodes - start delay
  timerEditFilterDelay.Enabled := False;
  timerEditFilterDelay.Enabled := True;
end;

procedure Tconnform.timerEditFilterDelayTimer(Sender: TObject);
begin
  // Filter session nodes
  FilterNodesByEdit(editSearch, ListSessions);
  timerEditFilterDelay.Enabled := False;
end;

procedure Tconnform.editSearchRightButtonClick(Sender: TObject);
begin
  editSearch.Clear;
end;

end.
