unit FormState;

{
[FormState] [1.8]
Delphi 2005
January 2009


LICENSE

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 "[FormState.pas]".

The Initial Developer of the Original Code is Martin Holmes (Victoria,
BC, Canada, "http://www.mholmes.com/"). Copyright (C) 2005 Martin Holmes 
and the University of Victoria Computing and Media Centre. The code was 
co-developed for university and personal projects, and rights are shared
by Martin Holmes and the University of Victoria. All Rights Reserved.
}

{
 Class initially created by Martin Holmes Fall 2005, using Delpi 2005, and only tested with
 Delphi 2005. Extended April 2006 to handle JEDI JVCL Docking components. Extended again
 January 2008 to allow saving/reloading of the items of combo boxes as well as
 the index.

 This enables the saving and loading of
 form position and size, and the size of
 any controls with an Align setting,
 and the settings of various other controls,
 in an XML or an INI file in the AppData folder.

 The object also publishes its AppDirPath property so any other code using
 it can use the same application data folder.

 Whether XML or INI format is used depends on the second parameter  of
 the Create function, the boolean UseXML.

 Use it like this:

 Create a private variable in the form:

      FFormStateSaver: TFormStateSaver;

 In the FormShow, do this:

      FFormStateSaver := TFormStateSaver.Create(Self, True);

 (This is the simplest constructor; the boolean  parameter specifies that
 XML should be used for storing the data, as opposed to INI file format.
 A more complex constructor is available, which enables you to set parameters
 for storing the states of a range of controls. See the code for details.)
 In the FormClose or the FormDestroy, do this:

      FreeAndNil(FFormStateSaver);

 This is specialized for TTntForms (from Troy Wolbrink's Unicode Controls)
 because that's what I use for all my forms in Delphi 2005.

 For this library to work properly:

  1. Form Position should be DefaultPosOnly. (However, if you are going to show the
  form modally, set it to poDesigned. If you don't, it will be positioned
  automatically by the system in the wrong place.)

  2. Application Title must be specified (in the project options).

  3. The application must include version info.

The latter two are required in order to save and load the file reliably.

Other properties allow the saving of other data, including the values of edit
boxes and combo boxes, and the current directories of dialog boxes.

Additional feature:

  This class can also be used to manage prompting the user to check for
  application updates. In order to do this, simple set the UpdateURL property
  to a valid URL after creating the object, then call IsUpdatePromptDue; if
  it returns True, you can ask the user if they want to check for updates, then
  send a browser to the URL if they agree. The current version number will be
  appended as a GET parameter to the URL: [URL]/blah.php?version=1.2.3.4. A
  server-side script can then do whatever it likes with that info. Once you have
  prompted the user, set the UpdatePrompted property to True.

  IsUpdatePromptDue will return True if the last date prompted is more than 30
  days ago, AND there is a string in the UpdateURL field.

 Dependencies:

 XDOM_4_1 (Dieter Kohler) for reading and writing XML files.

 VersionInfo (this has a class for getting version info
 about the running application). It would be feasible to use an instantiated
 TAppVersionInfo belonging to an instance of TSplashAbout owned by the main
 form, but then this library wouldn't be portable.

 GenFunctions (my library containing, among other things, code for getting
 Special Folder info from Windows).

 mdhSpin (My SpinEdit control with Unicode Hint property).

 IniFiles (this could easily be replaced with RegIni or an XML equivalent).

 TntUnicode libraries (Troy Wolbrink).

}
interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Menus, ExtCtrls, StdCtrls, TntForms, TntStdCtrls, TntExtCtrls,
  VersionInfo, GenFunctions, IniFiles, XDOM_4_1, TntComCtrls, TntSysUtils,
  Spin, mdhSpin, TntDialogs, TntCheckLst;

type
  TFormStateSaver = class(TObject)

  private
    FOwnerForm: TTntForm;
    FVersionInfo: TAppVersionInfo;
    FProductID: WideString;
    FDirPath: WideString;
    FFilePath: WideString;
    FXMLFilePath: WideString;
    FControlList: TList;
    FDialogList: TList;
    FUseXML: Boolean;
    FSaveFormPos: Boolean;
    FSaveCheckboxStates: Boolean;
    FSaveSpinEditValues: Boolean;
    FSaveComboBoxItems: Boolean;
    FSaveComboBoxStates: Boolean;
    FSaveRadioGroupStates: Boolean;
    FSaveEditContents: Boolean;
    FSaveDialogDirs: Boolean;
    FPortableMode: Boolean;
    FUpdateURL: WideString;
    FdtLastUpdatePrompt: integer; //Set to Date - 30 days on startup; overridden if
                                 //there's a setting in the storage file.
    function FirstRunOfApp: Boolean;

    procedure SetUpdateURL(inURL: WideString);

    procedure ReadStateFromINIFile;
    procedure ReadStateFromXMLFile;

    procedure WriteStateToINIFile;
    procedure WriteStateToXMLFile;
    procedure EnumerateControls(Parent: TComponent);

  //Utility function for getting dialog box directory to save
    function GetDialogDir(dlgBox: TOpenDialog): WideString;
  //Set the dialog box initial dir
    procedure SetDialogDir(dlgBox: TOpenDialog; wsDir: WideString);
    procedure GetDirPaths;

  //Called to find out whether it's time to ask the user whether to update or not.
    function UpdateCheckDue: Boolean;
    procedure SetUpdatePrompted(const Value: Boolean);

  public
    constructor Create(OwnerForm: TTntForm; UseXML: Boolean); overload;
    constructor Create(OwnerForm: TTntForm; UseXML: Boolean;
                      inSaveFormPos, inSaveCheckboxStates, inSaveSpinEditValues,
                      inSaveComboBoxItems, inSaveComboBoxStates,
                      inSaveRadioGroupStates,
                      inSaveEditContents,
                      inSaveDialogDirs: Boolean); overload;
    destructor Destroy; override;
    procedure ReadStateFromFile;
    procedure WriteStateToFile;
  published
    property AppDirPath: WideString read FDirPath;
    property UseXML: Boolean read FUseXML write FUseXML default True;
    property SaveFormPos: Boolean read FSaveFormPos write FSaveFormPos default True;
    property SaveCheckboxStates: Boolean read FSaveCheckboxStates write FSaveCheckboxStates default False;
    property SaveSpinEditValues: Boolean read FSaveSpinEditValues write FSaveSpinEditValues default False;
    property SaveComboBoxItems: Boolean read FSaveComboBoxItems write FSaveComboBoxItems default False;
    property SaveComboBoxStates: Boolean read FSaveComboBoxStates write FSaveComboBoxStates default False;
    property SaveRadioGroupStates: Boolean read FSaveRadioGroupStates write FSaveRadioGroupStates default False;
    property SaveEditContents: Boolean read FSaveEditContents write FSaveEditContents default False;
    property SaveDialogDirs: Boolean read FSaveDialogDirs write FSaveDialogDirs default False;
    property IsFirstRunOfApp: Boolean read FirstRunOfApp;
    property IsUpdateCheckDue: Boolean read UpdateCheckDue;
    property PortableMode: Boolean read FPortableMode write FPortableMode;
    property UpdateURL: WideString read FUpdateURL write SetUpdateURL;
    property UpdatePrompted: Boolean write SetUpdatePrompted;
  end;

implementation

{ TFormStateSaver }

procedure TFormStateSaver.GetDirPaths;
var
i: integer;
wsAppDir: WideString;

begin
{We need to detect if this application is being run in "portable" mode, of in a
regular install. In portable mode, the state info is stored in a directory
parallel with the one in which the app is running. If it's in normal mode,
we use the regular AppData directory.  }
  PortableMode := False;
  if ParamCount > 0 then
    for i := 0 to ParamCount do
      if ParamStr(i) = '-portable' then
        PortableMode := True;

  if (PortableMode = True) then
    begin

      FDirPath := WideExpandFileName(Application.ExeName + '\..\..\..\Data');
      if not (WideDirectoryExists(FDirPath)) then
        begin
          try
            WideForceDirectories(FDirPath);
          except
          //Just silently fail
          end;
        end;
      if not(WideDirectoryExists(FDirPath)) then
        FDirPath := AppDataFolder + '\' + FVersionInfo.CompanyName + '\' +
               FProductID;;
    end
  else
    begin
      FDirPath := AppDataFolder + '\' + FVersionInfo.CompanyName + '\' +
               FProductID;
    end;
  if not (WideDirectoryExists(FDirPath)) then
    WideForceDirectories(FDirPath);

  FFilePath := FDirPath + '\' + FOwnerForm.Name + '.ini';
  FXMLFilePath := FDirPath + '\' + FOwnerForm.Name + '.xml';
end;

constructor TFormStateSaver.Create(OwnerForm: TTntForm; UseXML: Boolean);
begin
  FOwnerForm := OwnerForm;
  FUseXML := True;
  FSaveFormPos := True; //this is the normal behaviour
//Other option properties default to false in this constructor.
  FSaveCheckboxStates := False;
  FSaveSpinEditValues := False;
  FSaveComboBoxItems := False;
  FSaveComboBoxStates := False;
  FSaveRadioGroupStates := False;
  FSaveEditContents := False;
  FSaveDialogDirs := False;

//Build the application data path for storing the info
  FVersionInfo := TAppVersionInfo.Create;
  FProductID := FVersionInfo.ProductName + ' v.' + IntToStr(FVersionInfo.V1) {+ '.' +
                IntToStr(FVersionInfo.V2)};

  GetDirPaths;
  FControlList := TList.Create;
  FDialogList := TList.Create;
  EnumerateControls(FOwnerForm);

  FUpdateURL := '';
  if FirstRunOfApp then
    FdtLastUpdatePrompt := Integer(Trunc(Date))
  else
    FdtLastUpdatePrompt := Integer(Trunc(Date)) - 30;

  ReadStateFromFile;
end;

constructor TFormStateSaver.Create(OwnerForm: TTntForm; UseXML: Boolean;
                                  inSaveFormPos,
                                  inSaveCheckboxStates, inSaveSpinEditValues,
                                  inSaveComboBoxItems, inSaveComboBoxStates,
                                  inSaveRadioGroupStates,
                                  inSaveEditContents,
                                  inSaveDialogDirs: Boolean);
begin
  FOwnerForm := OwnerForm;
  FUseXML := UseXML;
  FSaveFormPos := inSaveFormPos;
  FSaveCheckboxStates := inSaveCheckboxStates;
  FSaveSpinEditValues := inSaveSpinEditValues;
  FSaveComboBoxItems := inSaveComboBoxItems;
  FSaveComboBoxStates := inSaveComboBoxStates;
  FSaveRadioGroupStates := inSaveRadioGroupStates;
  FSaveEditContents := inSaveEditContents;
  FSaveDialogDirs := inSaveDialogDirs;
//Build the application data path for storing the info
  FVersionInfo := TAppVersionInfo.Create;
  FProductID := FVersionInfo.ProductName + ' v.' + IntToStr(FVersionInfo.V1) {+ '.' +
                IntToStr(FVersionInfo.V2)};

  GetDirPaths;

  FControlList := TList.Create;
  FDialogList := TList.Create;
  EnumerateControls(FOwnerForm);

  FUpdateURL := '';
  if FirstRunOfApp then
    FdtLastUpdatePrompt := Integer(Trunc(Date))
  else
    FdtLastUpdatePrompt := Integer(Trunc(Date)) - 30;

  ReadStateFromFile;
end;

destructor TFormStateSaver.Destroy;
begin
//These lines added in attempt to find a bug which might result from
//controls destroyed during the run of the app.
  FControlList.Clear;
  FDialogList.Clear;
  EnumerateControls(FOwnerForm);

  WriteStateToFile;
  FDialogList.Free;
  FControlList.Free;
  inherited;
end;

procedure TFormStateSaver.WriteStateToFile;
begin
  if UseXML then
    WriteStateToXMLFile
  else
    WriteStateToINIFile;
end;

procedure TFormStateSaver.WriteStateToINIFile;
var
Ini: TIniFile;
i: integer;
j: integer;

begin
//Save the data to an INI file if the app has a directory to put it in.
  if not DirectoryExists(FDirPath) then
    Exit;
  Ini := TIniFile.Create(FFilePath);
  try
    if SaveFormPos then
      begin
//Only write position and size if not maximized
        if not (FOwnerForm.WindowState = wsMaximized) then
          begin
            Ini.WriteInteger('Form', 'Left', FOwnerForm.Left);
            Ini.WriteInteger('Form', 'Top', FOwnerForm.Top);
            Ini.WriteInteger('Form', 'Width', FOwnerForm.Width);
            Ini.WriteInteger('Form', 'Height', FOwnerForm.Height);
          end;
        Ini.WriteBool('Form', 'Maximized', FOwnerForm.WindowState = wsMaximized);
    //Store the monitor on which the form is located
        if Screen.MonitorCount > 0 then
          Ini.WriteInteger('Form', 'Monitor', FOwnerForm.Monitor.MonitorNum);
      end;

//We only care (at the moment, at any rate) about controls which have an Align
//setting which indicates that they're resizable.
    if FControlList.Count > 0 then
      for i := 0 to FControlList.Count - 1 do
        begin
          if SaveFormPos then
            begin
//List view control needs to save its column widths. Don't save the last one;
//that will default to what remains
              if TControl(FControlList[i]) is TTntListView then
                with TControl(FControlList[i]) as TTntListView do
                  begin
                    if Columns.Count > 1 then
                      for j := 0 to Columns.Count - 2 do
                        Ini.WriteInteger('Controls', Name + '_Col_' + IntToStr(j), Columns[j].Width);
                  end;
    //Most panel sizes should be saved, but toolbars must be able to wrap, and status bars should be ignored
              if not (TControl(FControlList[i]) is TTntToolbar) then
                with TControl(FControlList[i]) do
                  begin
                    Case Align of
                      alLeft: Ini.WriteInteger('Controls', Name, Width);
                      alTop: Ini.WriteInteger('Controls', Name, Height);
    //Addition to handle docking panels 18/04/06
                      alRight: Ini.WriteInteger('Controls', Name, Width);
                      alBottom: Ini.WriteInteger('Controls', Name, Height);
                    end;
                  end;
            end;
//If the property is set, save the state of checkboxes on the form
          if (TControl(FControlList[i]) is TTntCheckbox) and SaveCheckboxStates then
            with TTntCheckBox(FControlList[i]) do
              Ini.WriteBool('Controls', Name, Checked);

//If the same property is set, save the state of items in checklistboxes on the form.
          if (TControl(FControlList[i]) is TTntCheckListBox) and SaveCheckboxStates then
            with TControl(FControlList[i]) as TTntCheckListBox do
              begin
                if Items.Count > 0 then
                  for j := 0 to Items.Count - 1 do
                    Ini.WriteBool('Controls', Name + '_Item_' + IntToStr(j), Checked[j]);
              end;

//If the property is set, save the value of spinedit controls on the form
          if (TControl(FControlList[i]) is TSpinEdit) and SaveSpinEditValues then
            with TSpinEdit(FControlList[i]) do
              Ini.WriteInteger('Controls', Name, Value);

//My Unicode spinedit control also needs to be handled
          if (TControl(FControlList[i]) is TMdhSpinEdit) and SaveSpinEditValues then
            with TMdhSpinEdit(FControlList[i]) do
              Ini.WriteInteger('Controls', Name, Value);

//If the property is set, save the individual items of the comboboxes on the form
          if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxItems then
            with TTntComboBox(FControlList[i]) do
              if Items.Count > 0 then
                for j := 0 to Items.Count-1 do
                  Ini.WriteString('Controls', Name + '_Item_' + IntToStr(j), Items[j]);
//If the property is set, save the itemindex of comboboxes on the form
          if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxStates then
            with TTntComboBox(FControlList[i]) do
              Ini.WriteInteger('Controls', Name, ItemIndex);
//If the property is set, save the itemindex of radiogroups on the form
          if (TControl(FControlList[i]) is TTntRadioGroup) and SaveRadioGroupStates then
            with TTntRadioGroup(FControlList[i]) do
              Ini.WriteInteger('Controls', Name, ItemIndex);
//If the property is set, save the contents of edit controls on the form
          if (TControl(FControlList[i]) is TTntEdit) and SaveEditContents then
            with TTntEdit(FControlList[i]) do
              Ini.WriteString('Controls', Name, Text);
        end;
//Dialog box initial directories, if appropriate
    if SaveDialogDirs then
      if FDialogList.Count > 0 then
        for i := 0 to FDialogList.Count - 1 do
          Ini.WriteString('Controls', TOpenDialog(FDialogList[i]).Name,
                          GetDialogDir(FDialogList[i]));

//Save last update prompt date, if appropriate
    if Length(FUpdateURL) > 0 then
      Ini.WriteInteger('App', 'LastUpdatePrompt', FdtLastUpdatePrompt);
  finally
    Ini.Free;
  end;
end;

procedure TFormStateSaver.WriteStateToXMLFile;
var
DomImpl: TDomImplementation;
DomDoc: TDomDocument;
DomToXMLParser: TDomToXMLParser;
RootNode: TDomElement;
FormNode: TDomElement;
ControlsNode: TDomElement;
AppNode: TDomElement;
ChildNode: TDomElement;
i: integer;
j: integer;
Stream: TFileStream;

  procedure AppendChildNode(NodeName: WideString; NodeValue: WideString; TheParent: TDomElement);
  var
  TextNode: TDomText;
  begin
    ChildNode := TDomElement.Create(DomDoc, NodeName);
    TextNode := TDomText.Create(DomDoc);
    TextNode.NodeValue := NodeValue;
    ChildNode.AppendChild(TextNode);
    TheParent.AppendChild(ChildNode);
  end;

begin
//Save the data to an XML file if the app has a directory to put it in.
  if not DirectoryExists(FDirPath) then
    Exit;

  DomImpl := TDomImplementation.Create(nil);
  try
    DomDoc := TDomDocument.Create(DomImpl);
    DomToXMLParser := TDomToXMLParser.Create(nil);
    try
      DomToXMLParser.DomImpl := DomImpl;
      RootNode := TDomElement.Create(DomDoc, 'formState');
      DomDoc.AppendChild(RootNode);
      FormNode := TDomElement.Create(DomDoc, 'form');
      RootNode.AppendChild(FormNode);
      ControlsNode := TDomElement.Create(DomDoc, 'controls');
      RootNode.AppendChild(ControlsNode);
      AppNode := TDomElement.Create(DomDoc, 'app');
      RootNode.AppendChild(AppNode);
//Add the form data
      if SaveFormPos then
        begin
//Only write position and size if not maximized
          if not (FOwnerForm.WindowState = wsMaximized) then
            begin
              AppendChildNode('left', IntToStr(FOwnerForm.Left), FormNode);
              AppendChildNode('top', IntToStr(FOwnerForm.Top), FormNode);
              AppendChildNode('width', IntToStr(FOwnerForm.Width), FormNode);
              AppendChildNode('height', IntToStr(FOwnerForm.Height), FormNode);
            end;
          AppendChildNode('maximized', BoolToStr(FOwnerForm.WindowState = wsMaximized), FormNode);
    //Store the monitor on which the form is located
          if Screen.MonitorCount > 0 then
            AppendChildNode('monitor', IntToStr(FOwnerForm.Monitor.MonitorNum), FormNode);
        end;

//Now add data about individual controls.
//We only care (at the moment, at any rate) about controls which have an Align
//setting which indicates that they're resizable.
      if FControlList.Count > 0 then
        for i := 0 to FControlList.Count - 1 do
          begin
            if SaveFormPos then
              begin
//List view control needs to save its column widths. Don't save the last one;
//that will default to what remains
                if TControl(FControlList[i]) is TTntListView then
                  with TControl(FControlList[i]) as TTntListView do
                    begin
                      if Columns.Count > 1 then
                        for j := 0 to Columns.Count - 2 do
                          AppendChildNode(Name + '_Col_' + IntToStr(j), IntToStr(Columns[j].Width), ControlsNode);
                    end;
                if not (TControl(FControlList[i]) is TTntToolbar) then
                  with TControl(FControlList[i]) do
                    begin
                      Case Align of
                        alLeft: AppendChildNode(Name, IntToStr(Width), ControlsNode);
                        alTop: AppendChildNode(Name, IntToStr(Height), ControlsNode);
    //Addition to handle docking panels 18/04/06
                        alRight: AppendChildNode(Name, IntToStr(Width), ControlsNode);
                        alBottom: AppendChildNode(Name, IntToStr(Height), ControlsNode);
                      end;
                    end;
              end;
//If the property is set, save the state of checkboxes on the form
            if (TControl(FControlList[i]) is TTntCheckbox) and SaveCheckboxStates then
              with TTntCheckBox(FControlList[i]) do
                AppendChildNode(Name, BoolToStr(Checked), ControlsNode);

//If the same property is set, save the state of items in checklistboxes on the form.
          if (TControl(FControlList[i]) is TTntCheckListBox) and SaveCheckboxStates then
            with TControl(FControlList[i]) as TTntCheckListBox do
              begin
                if Items.Count > 0 then
                  for j := 0 to Items.Count - 1 do
                    AppendChildNode(Name + '_Item_' + IntToStr(j), BoolToStr(Checked[j]), ControlsNode);
              end;

//If the property is set, save the values of spinedits on the form
            if (TControl(FControlList[i]) is TSpinEdit) and SaveSpinEditValues then
              with TSpinEdit(FControlList[i]) do
                AppendChildNode(Name, IntToStr(Value), ControlsNode);

//Also do my own MdhSpinEdit control
            if (TControl(FControlList[i]) is TMdhSpinEdit) and SaveSpinEditValues then
              with TMdhSpinEdit(FControlList[i]) do
                AppendChildNode(Name, IntToStr(Value), ControlsNode);

//If the property is set, save the individual items of the comboboxes on the form
            if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxItems then
              with TTntComboBox(FControlList[i]) do
                if Items.Count > 0 then
                  for j := 0 to Items.Count-1 do
                    AppendChildNode(Name + '_Item', Items[j], ControlsNode);
//If the property is set, save the itemindex of comboboxes on the form
            if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxStates then
              with TTntComboBox(FControlList[i]) do
                AppendChildNode(Name, IntToStr(ItemIndex), ControlsNode);

//If the property is set, save the itemindex of radiogroups on the form
            if (TControl(FControlList[i]) is TTntRadioGroup) and SaveRadioGroupStates then
              with TTntRadioGroup(FControlList[i]) do
                AppendChildNode(Name, IntToStr(ItemIndex), ControlsNode);
//If the property is set, save the contents of edit controls on the form
            if (TControl(FControlList[i]) is TTntEdit) and SaveEditContents then
              with TTntEdit(FControlList[i]) do
                AppendChildNode(Name, Text, ControlsNode);

          end;
//Now save any dialog box directories, if appropriate.
      if SaveDialogDirs then
        if FDialogList.Count > 0 then
          for i := 0 to FDialogList.Count - 1 do
            AppendChildNode(TOpenDialog(FDialogList[i]).Name,
                            GetDialogDir(FDialogList[i]), ControlsNode);

//Save the last update prompt date, if appropriate.

      if Length(FUpdateURL) > 0 then
        AppendChildNode('lastUpdatePrompt', IntToStr(FdtLastUpdatePrompt), AppNode);

//Now save the file to disk
      Stream := TFileStream.Create(FXMLFilePath, fmCreate);
      try
        DomToXMLParser.WriteToStream(DomDoc, 'UTF-8', Stream);
      finally
        Stream.Free;
      end;
    finally
      DomToXMLParser.Free;
    end;
  finally
    DomImpl.Free;
  end;
end;

procedure TFormStateSaver.ReadStateFromFile;
begin
  if UseXML then
    ReadStateFromXMLFile
  else
    ReadStateFromINIFile;
end;

procedure TFormStateSaver.ReadStateFromINIFile;
var
Ini: TIniFile;
i: integer;
j: integer;
Mon: integer;
wsTemp: WideString;

begin
//If there's an INI file, read it and set the appropriate properties of the
//form and its components.
  if FileExists(FFilePath) then
    begin
      Ini := TIniFile.Create(FFilePath);
      try
        if SaveFormPos then
          begin
//First set Window size and position, to place it on the correct monitor
//and provide the correct bounds for a splash screen to display if it's
//using the form BoundsRect.
            FOwnerForm.Left := Ini.ReadInteger('Form', 'Left', FOwnerForm.Left);
            FOwnerForm.Top := Ini.ReadInteger('Form', 'Top', FOwnerForm.Top);
            FOwnerForm.Width := Ini.ReadInteger('Form', 'Width', FOwnerForm.Width);
            FOwnerForm.Height := Ini.ReadInteger('Form', 'Height', FOwnerForm.Height);
            FOwnerForm.Update;
    //Now handle the situation where the bounds are outside the display available
    //(which might happen if a second monitor was removed from the system.)
            if FOwnerForm.Left > (Screen.DesktopWidth - 50) then
              FOwnerForm.Left := 0;
            if FOwnerForm.Top > (Screen.DesktopHeight - 50) then
              FOwnerForm.Top := 0;

            if Ini.ReadBool('Form', 'Maximized', False) = True then
              begin
    //Check which monitor it was on, if multi-monitor
                Mon := 0;
                if Screen.MonitorCount > 0 then
                  Mon := Ini.ReadInteger('Form', 'Monitor', 0);
                if (Mon > 0) and (Mon < Screen.MonitorCount) then
                  FOwnerForm.MakeFullyVisible(Screen.Monitors[Mon]);
    //Now maximize the window
                FOwnerForm.WindowState := wsMaximized;
                FOwnerForm.Update;
              end;
          end;
        if FControlList.Count > 0 then
          for i := 0 to FControlList.Count - 1 do
            begin
              if SaveFormPos then
                begin
                  if not (TControl(FControlList[i]) is TTntToolbar) then
                    with TControl(FControlList[i]) do
                      begin
      //We only care (at the moment, at any rate) about controls which have an Align
      //setting which indicates that they're resizable.
                        Case Align of
                          alLeft: Width := Ini.ReadInteger('Controls', Name, Width);
                          alTop: Height := Ini.ReadInteger('Controls', Name, Height);
    //Addition to handle docking panels 18/04/06
                          alRight: Width := Ini.ReadInteger('Controls', Name, Width);
                          alBottom: Height := Ini.ReadInteger('Controls', Name, Height);
                        end;
                      end;
    //List view control needs to reload its column widths. Don't bother with the last one;
    //that will default to what remains
                  if TControl(FControlList[i]) is TTntListView then
                    with TControl(FControlList[i]) as TTntListView do
                      begin
                        if Columns.Count > 1 then
                          for j := 0 to Columns.Count - 2 do
                            Columns[j].Width := Ini.ReadInteger('Controls', Name + '_Col_' + IntToStr(j), Columns[j].Width);
    //Trigger a resize event -- usually needed to shuffle the column widths to avoid scrollbars
                        if Assigned(onResize) then
                          begin
                            onResize(nil);
                            Application.ProcessMessages;
                          end;
                      end;
                end;
//If the property is set, read the state of checkboxes on the form
              if (TControl(FControlList[i]) is TTntCheckbox) and SaveCheckboxStates then
                with TTntCheckBox(FControlList[i]) do
                  Checked := Ini.ReadBool('Controls', Name, Checked);

//If the same property is set, reload the state of checklistbox items
              if (TControl(FControlList[i]) is TTntCheckListbox) and SaveCheckboxStates then
                with TTntCheckListBox(FControlList[i]) do
                  if Items.Count > 0 then
                    for j := 0 to Items.Count-1 do
                      Checked[j] := Ini.ReadBool('Controls', Name + '_Item_' + IntToStr(j), Checked[j]);

//If the property is set, read the values of spinedits on the form
              if (TControl(FControlList[i]) is TSpinEdit) and SaveSpinEditValues then
                with TSpinEdit(FControlList[i]) do
                  Value := Ini.ReadInteger('Controls', Name, Value);

//Also do my own MdhSpinEdit control
              if (TControl(FControlList[i]) is TMdhSpinEdit) and SaveSpinEditValues then
                with TMdhSpinEdit(FControlList[i]) do
                  Value := Ini.ReadInteger('Controls', Name, Value);

//If the property is set, read the values of items of combo boxes on the form
              if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxItems then
                with TTntComboBox(FControlList[i]) do
                  begin
                    Items.Clear;
                    j := 0;
                    wsTemp := Ini.ReadString('Controls', Name+'_Item_' + IntToStr(j), '');
                    while wsTemp <> '' do
                      begin
                        Items.Add(wsTemp);
                        wsTemp := Ini.ReadString('Controls', Name+'_Item_' + IntToStr(j), '');
                      end;
                  end;
//If the property is set, read the itemindex of combo boxes on the form
              if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxStates then
                with TTntComboBox(FControlList[i]) do
                  ItemIndex := Ini.ReadInteger('Controls', Name, ItemIndex);
//If the property is set, read the itemindex of radiogroups on the form
              if (TControl(FControlList[i]) is TTntRadioGroup) and SaveRadioGroupStates then
                with TTntRadioGroup(FControlList[i]) do
                  ItemIndex := Ini.ReadInteger('Controls', Name, ItemIndex);
//If the property is set, read the text of edit controls on the form
              if (TControl(FControlList[i]) is TTntEdit) and SaveEditContents then
                with TTntEdit(FControlList[i]) do
                  Text := Ini.ReadString('Controls', Name, Text);
            end;
//Dialog box initial directories, if appropriate
        if SaveDialogDirs then
          if FDialogList.Count > 0 then
            for i := 0 to FDialogList.Count - 1 do
              begin
                wsTemp := Ini.ReadString('Controls', TOpenDialog(FDialogList[i]).Name, '');
                SetDialogDir(TOpenDialog(FDialogList[i]), wsTemp);
              end;

//Always try to read last update prompt date.
        FdtLastUpdatePrompt := Ini.ReadInteger('App', 'LastUpdatePrompt', FdtLastUpdatePrompt);

      finally
        Ini.Free;
      end;
    end;
end;

procedure TFormStateSaver.ReadStateFromXMLFile;
var
i: integer;
j: integer;
Path: TFileName;
DomImpl: TDomImplementation;
DomDoc: TDomDocument;
XMLToDomParser: TXMLToDomParser;
El: TDomElement;
boolTemp: Boolean;
wsTemp: WideString;
wsBool: WideString;
wsInt: WideString;
Mon: integer;

begin
  if not FileExists(FXMLFilePath) then
    Exit;
  Path := TFileName(FXMLFilePath);

  DomImpl := TDomImplementation.Create(nil);
  try
    XMLToDomParser := TXMLToDomParser.Create(nil);
    try
      XMLToDomParser.DOMImpl := DomImpl;
//Updated code for XDOM 4.1
      DomDoc := XMLToDomParser.ParseFile(Path, False);
//      DomDoc := XMLToDomParser.FileToDom(Path);
    finally
      XMLToDomParser.Free;
    end;
//Now read the data from the dom. First get the form element.
    if SaveFormPos then
      begin
        if DomDoc.GetElementsByTagName('form').Length > 0 then
          begin
            El := TDomElement(DomDoc.GetElementsByTagName('form').Item(0));
            with FOwnerForm do
              begin
                if El.GetElementsByTagName('left').Length > 0 then
                  Left := StrToIntDef(El.GetElementsByTagName('left').Item(0).textContent, Left);
                if El.GetElementsByTagName('top').Length > 0 then
                  Top := StrToIntDef(El.GetElementsByTagName('top').Item(0).textContent, Top);
                if El.GetElementsByTagName('width').Length > 0 then
                  Width := StrToIntDef(El.GetElementsByTagName('width').Item(0).textContent, Width);
                if El.GetElementsByTagName('height').Length > 0 then
                  Height := StrToIntDef(El.GetElementsByTagName('height').Item(0).textContent, Height);
                if El.GetElementsByTagName('maximized').Length > 0 then
                  begin
                    BoolTemp := StrToBoolDef(El.GetElementsByTagName('maximized').Item(0).textContent, False);
                    if BoolTemp then
                      begin
    //Check which monitor it was on, if multi-monitor
                        Mon := 0;
                        if Screen.MonitorCount > 0 then
                          begin
                            if El.GetElementsByTagName('monitor').Length > 0 then
                              Mon := StrToIntDef(El.GetElementsByTagName('monitor').Item(0).textContent, 0);
                          end;
                        if (Mon > 0) and (Mon < Screen.MonitorCount) then
                          FOwnerForm.MakeFullyVisible(Screen.Monitors[Mon]);
    //Now maximize the window
                        FOwnerForm.WindowState := wsMaximized;
                        FOwnerForm.Update;
                      end;
                  end;
              end;
          end;
      end;
    if FControlList.Count > 0 then
        for i := 0 to FControlList.Count - 1 do
          begin
            if SaveFormPos then
              begin
                if not (TControl(FControlList[i]) is TTntToolbar) then
                  with TControl(FControlList[i]) do
                    begin
        //We only care (at the moment, at any rate) about controls which have an Align
        //setting which indicates that they're resizable.
                      Case Align of
                        alLeft:
                          begin
                            if DomDoc.GetElementsByTagName(Name).Length > 0 then
                              Width := StrToIntDef(DomDoc.GetElementsByTagName(Name).Item(0).textContent, Width);
                          end;

                        alTop:
                          begin
                            if DomDoc.GetElementsByTagName(Name).Length > 0 then
                              Height := StrToIntDef(DomDoc.GetElementsByTagName(Name).Item(0).textContent, Height);
                          end;
    //Addition to handle docking panels 18/04/06
                        alRight:
                          begin
                            if DomDoc.GetElementsByTagName(Name).Length > 0 then
                              Width := StrToIntDef(DomDoc.GetElementsByTagName(Name).Item(0).textContent, Width);
                          end;
                        alBottom:
                          begin
                            if DomDoc.GetElementsByTagName(Name).Length > 0 then
                              Height := StrToIntDef(DomDoc.GetElementsByTagName(Name).Item(0).textContent, Height);
                          end;
                      end;
                    end;
    //List view control needs to reload its column widths. Don't bother with the last one;
    //that will default to what remains
                  if TControl(FControlList[i]) is TTntListView then
                    with TControl(FControlList[i]) as TTntListView do
                      begin
                        if Columns.Count > 1 then
                          for j := 0 to Columns.Count - 2 do
                            begin
                              wsTemp := Name + '_Col_' + IntToStr(j);
                              if DomDoc.GetElementsByTagName(wsTemp).Length > 0 then
                                begin
                                  Columns[j].Width := StrToIntDef(DomDoc.GetElementsByTagName(wsTemp).Item(0).textContent, Columns[j].Width);
    //Trigger a resize event -- usually needed to shuffle the column widths to avoid scrollbars
                                  if Assigned(onResize) then
                                    begin
                                      onResize(nil);
                                      Application.ProcessMessages;
                                    end;
                                end;
                            end;
                      end;
              end;
//If the property is set, read the state of checkboxes on the form
              if (TControl(FControlList[i]) is TTntCheckbox) and SaveCheckboxStates then
                with TTntCheckBox(FControlList[i]) do
                  if DomDoc.GetElementsByTagName(Name).Length > 0 then
                    begin
                      wsBool := DomDoc.GetElementsByTagName(Name).Item(0).textContent;
                      Checked := StrToBoolDef(string(wsBool), Checked);
                    end;

//If the same property is set, reload the state of checklistbox items
              if (TControl(FControlList[i]) is TTntCheckListbox) and SaveCheckboxStates then
                with TTntCheckListBox(FControlList[i]) do
                  if Items.Count > 0 then
                    for j := 0 to Items.Count-1 do
                      begin
                        wsTemp := Name + '_Item_' + IntToStr(j);
                        if DomDoc.GetElementsByTagName(wsTemp).Length > 0 then
                          Checked[j] := StrToBoolDef(DomDoc.GetElementsByTagName(wsTemp).Item(0).textContent, Checked[j]);
                      end;

//If the property is set, read the values of spinedits on the form
              if (TControl(FControlList[i]) is TSpinEdit) and SaveSpinEditValues then
                with TSpinEdit(FControlList[i]) do
                  if DomDoc.GetElementsByTagName(Name).Length > 0 then
                    begin
                      wsInt := DomDoc.GetElementsByTagName(Name).Item(0).textContent;
                      Value := StrToIntDef(string(wsInt), Value);
                    end;

//Also handle my own MdhSpinEdit control
              if (TControl(FControlList[i]) is TMdhSpinEdit) and SaveSpinEditValues then
                with TMdhSpinEdit(FControlList[i]) do
                  if DomDoc.GetElementsByTagName(Name).Length > 0 then
                    begin
                      wsInt := DomDoc.GetElementsByTagName(Name).Item(0).textContent;
                      Value := StrToIntDef(string(wsInt), Value);
                    end;

//If the property is set, read the individual items of combo boxes on the form
              if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxItems then
                with TTntComboBox(FControlList[i]) do
                  if DomDoc.GetElementsByTagName(Name + '_Item').Length > 0 then
                    begin
                      Items.Clear;
                      for j := 0 to DomDoc.GetElementsByTagName(Name + '_Item').Length-1 do
                        Items.Add(DomDoc.GetElementsByTagName(Name + '_Item').Item(j).textContent);
                    end;
//If the property is set, read the itemindex of combo boxes on the form
              if (TControl(FControlList[i]) is TTntComboBox) and SaveComboBoxStates then
                with TTntComboBox(FControlList[i]) do
                  if DomDoc.GetElementsByTagName(Name).Length > 0 then
                    begin
                      wsInt := DomDoc.GetElementsByTagName(Name).Item(0).textContent;
                      ItemIndex := StrToIntDef(string(wsInt), ItemIndex);
                    end;
//If the property is set, read the itemindex of radiogroups on the form
              if (TControl(FControlList[i]) is TTntRadioGroup) and SaveRadioGroupStates then
                with TTntRadioGroup(FControlList[i]) do
                  if DomDoc.GetElementsByTagName(Name).Length > 0 then
                    begin
                      wsInt := DomDoc.GetElementsByTagName(Name).Item(0).textContent;
                      ItemIndex := StrToIntDef(string(wsInt), ItemIndex);
                    end;
//If the property is set, read the text of edit controls on the form
              if (TControl(FControlList[i]) is TTntEdit) and SaveEditContents then
                with TTntEdit(FControlList[i]) do
                  if DomDoc.GetElementsByTagName(Name).Length > 0 then
                    Text := DomDoc.GetElementsByTagName(Name).Item(0).textContent;
          end;
    //Now get any dialog box directories, if appropriate.
    if SaveDialogDirs then
      if FDialogList.Count > 0 then
        for i := 0 to FDialogList.Count - 1 do
          if DomDoc.GetElementsByTagName(TOpenDialog(FDialogList[i]).Name).Length > 0 then
            begin
              wsTemp := DomDoc.GetElementsByTagName(TOpenDialog(FDialogList[i]).Name).Item(0).textContent;
              SetDialogDir(TOpenDialog(FDialogList[i]), wsTemp);
            end;

//Always try to read last update prompt date.
        if DomDoc.GetElementsByTagName('lastUpdatePrompt').Length > 0 then
          begin
            wsTemp := DomDoc.GetElementsByTagName('lastUpdatePrompt').Item(0).textContent;
            FdtLastUpdatePrompt := StrToInt(wsTemp);
          end;
          
  finally
    DomImpl.Free;
  end;
end;

procedure TFormStateSaver.EnumerateControls(Parent: TComponent);
var
i: integer;

begin
//Get a list of components owned by the component (recursive).
  if Parent.ComponentCount > 0 then
    for i := 0 to Parent.ComponentCount-1 do
      begin
        if Parent.Components[i] is TControl then
          FControlList.Add(Parent.Components[i]);
        if Parent.Components[i] is TOpenDialog then
          FDialogList.Add(Parent.Components[i]);
        EnumerateControls(Parent.Components[i]);
      end;
end;



function TFormStateSaver.FirstRunOfApp: Boolean;
begin
//If the form state has been saved before, then this is not the first
//run of the application.
  Result := True;
  if FileExists(FXMLFilePath) then
    Result := False
  else
    if FileExists(FFilePath) then
      Result := False;
end;

function TFormStateSaver.GetDialogDir(dlgBox: TOpenDialog): WideString;
begin
  Result := '';
  if dlgBox is TTntOpenDialog then
    begin
      with TTntOpenDialog(dlgBox) do
        begin
          if WideDirectoryExists(WideExtractFilePath(FileName)) then
            Result := WideExtractFilePath(FileName)
          else
            if WideDirectoryExists(InitialDir) then
              Result := InitialDir;
        end;
    end
  else
    begin
      with TOpenDialog(dlgBox) do
        begin
          if DirectoryExists(ExtractFilePath(FileName)) then
            Result := ExtractFilePath(FileName)
          else
            if DirectoryExists(InitialDir) then
              Result := InitialDir;
        end;
    end;
end;

procedure TFormStateSaver.SetDialogDir(dlgBox: TOpenDialog; wsDir: WideString);
begin
  if Length(wsDir) < 3 then
    Exit;
  if wsDir[Length(wsDir)] = WideChar('/') then
    wsDir := Copy(wsDir, 1, Length(wsDir)-1);
  if dlgBox is TTntOpenDialog then
    begin
      if WideDirectoryExists(wsDir) then
        TTntOpenDialog(dlgBox).InitialDir := wsDir;
    end
  else
    begin
      if DirectoryExists(wsDir) then
        TOpenDialog(dlgBox).InitialDir := string(wsDir);
    end;
end;

procedure TFormStateSaver.SetUpdateURL(inURL: WideString);
begin
{This is a method rather than a direct assignment so that
 we can add some kind of verification code if we wish.}
  if (Length(inURL) > 0) then
    begin
      FUpdateURL := inURL + '?version=' + FVersionInfo.DottedVersion;
    end;
end;

function TFormStateSaver.UpdateCheckDue: Boolean;
begin
  Result := False; //Default
  if (Length(FUpdateURL) > 0) then
    if ((Date - 30) > FdtLastUpdatePrompt) then
      Result := True;
end;

procedure TFormStateSaver.SetUpdatePrompted(const Value: Boolean);
begin
  if Value = True then
    FdtLastUpdatePrompt := Integer(Trunc(Date));
end;

end.
