{
  Copyright 2004-2008 Richard B. Winston, U.S. Geological Survey (USGS)
  Copyright 2005-2026 Michalis Kamburelis

  This file is part of pasdoc_gui.

  pasdoc_gui is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  pasdoc_gui is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with pasdoc_gui; if not, write to the Free Software
  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
}

{
  @abstract(Main form of PasDoc GUI.)
  @author(Richard B. Winston <rbwinst@usgs.gov>)
  @author(Michalis Kamburelis)
  @created(2004-11-28)
}

unit frmHelpGeneratorUnit;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, LResources, Graphics, Controls, Forms,
  Dialogs, PasDoc_Gen, PasDoc_GenHtml, PasDoc_Base, StdCtrls, PasDoc_Types,
  ComCtrls, ExtCtrls, CheckLst, PasDoc_Languages, Menus,
  Buttons, Spin, PasDoc_GenLatex, PasDoc_Serialize,
  IniFiles, PasDoc_GenHtmlHelp, EditBtn, PasDoc_Utils, LCLType, SynEdit,
  PasDoc_Items, PasDoc_GenSimpleXML, PasDoc_GenPHP;

type
  EInvalidSpellingLanguage = class(Exception);

  { Main form. Its published fields are mainly components that are used to
    save the project settings. }
  TfrmHelpGenerator = class(TForm)
    BtnBrowseAdditionalFiles: TButton;
    btnBrowseIncludeDirectory: TButton;
    btnBrowseSourceFiles: TButton;
    // Click @name to generate output
    ButtonGenerateDocs: TButton;
    ButtonAspellURL: TButton;
    ButtonGraphVizURL: TButton;
    cbCheckSpelling: TCheckBox;
    cbVizGraphClasses: TCheckBox;
    cbVizGraphUses: TCheckBox;
    CheckAutoAbstract: TCheckBox;
    CheckAutoLink: TCheckBox;
    CheckBoxBootstrap: TCheckBox;
    CheckOpenHTML: TCheckBox;
    CheckStoreRelativePaths: TCheckBox;
    CheckHandleMacros: TCheckBox;
    CheckUseTipueSearch: TCheckBox;
    // Click @name to support Markdown in comments.
    CheckMarkdown: TCheckBox;
    // Click @name to automatically assign a // comment to the
    // preceding identifier on the same line.
    CheckAutoBackComments: TCheckBox;
    // @name controls what members (based on visibility)
    // will be included in generated output.
    CheckListVisibleMembers: TCheckListBox;
    CheckWriteUsesList: TCheckBox;
    ComboInheritedMembers: TComboBox;
    // @name determines what sort of files will be created
    comboGenerateFormat: TComboBox;
    ComboImplementationComments: TComboBox;
    // comboLanguages is used to set the language in which the web page will
    // be written.  Of course, this only affects tha language for the text
    // generated by the program, not the comments about the program.
    comboLanguages: TComboBox;
    comboLatexGraphicsPackage: TComboBox;
    EditConclusionFileName: TFileNameEdit;
    EditCssFileName: TFileNameEdit;
    EditExternalDescriptions: TFileNameEdit;
    EditIntroductionFileName: TFileNameEdit;
    EditHtmlHead: TFileNameEdit;
    EditHtmlBodyBegin: TFileNameEdit;
    EditHtmlBodyEnd: TFileNameEdit;
    // @name is used to set the name of the project.
    edProjectName: TEdit;
    CssFileNameFileNameEdit1: TFileNameEdit;
    edTitle: TEdit;
    HtmlHelpDocGenerator: THTMLHelpDocGenerator;
    LabelInheritedMembers: TLabel;
    Label8: TLabel;
    Label9: TLabel;
    LabelAdditionalFiles: TLabel;
    LabelAutoLinkExclude: TLabel;
    LabelExternalDescriptions: TLabel;
    LabelHtmlHead: TLabel;
    LabelHtmlBodyBegin: TLabel;
    LabelHtmlBodyEnd: TLabel;
    LabelImplementationComments: TLabel;
    LabelProjectName: TLabel;
    LabelHeader: TLabel;
    LabelFooter: TLabel;
    LabelConclusionFile: TLabel;
    LabelIntroductionFile: TLabel;
    LabelCssFileName: TLabel;
    LabelLanguages: TLabel;
    LabelTitle: TLabel;
    Label20: TLabel;
    LabelVisibleMembers: TLabel;
    Label22: TLabel;
    Label23: TLabel;
    LabelLatexGraphicsPackage: TLabel;
    LabelOutputType: TLabel;
    Label7: TLabel;
    memoCommentMarkers: TMemo;
    memoDefines: TMemo;
    // @name holds the complete paths of all the source files
    // in the project.
    memoFiles: TMemo;
    MemoAutoLinkExclude: TMemo;
    MemoAdditionalFiles: TMemo;
    memoFooter: TMemo;
    memoHeader: TMemo;
    // The lines in @name are the paths of the files that
    // may have include files that are part of the project.
    memoIncludeDirectories: TMemo;
    // memoMessages displays compiler warnings.  See also @link(seVerbosity);
    memoMessages: TMemo;
    memoSpellCheckingIgnore: TMemo;
    MenuAbout: TMenuItem;
    MenuContextHelp: TMenuItem;
    MenuEdit: TMenuItem;
    MenuGenerate: TMenuItem;
    MenuGenerateRun: TMenuItem;
    MenuSave: TMenuItem;
    MenuPreferences: TMenuItem;
    NotebookMain: TNotebook;
    pageAutoLink: TPage;
    PageVisibleMembers: TPage;
    pageDisplayComments: TPage;
    pageDefines: TPage;
    pageGenerate: TPage;
    pageGraphs: TPage;
    pageHeadFoot: TPage;
    pageIncludeDirectories: TPage;
    pageLatexOptions: TPage;
    pageCustomFiles: TPage;
    pageMarkers: TPage;
    pageOptions: TPage;
    pageSourceFiles: TPage;
    pageSpellChecking: TPage;
    PanelAutoLinkTop: TPanel;
    PanelIncludeDirectoriesTop: TPanel;
    PanelSourceFilesTop: TPanel;
    PanelFooterHidden: TPanel;
    PanelHeaderHidden: TPanel;
    PHPDocGenerator: TPHPDocGenerator;
    pnlEditCommentInstructions: TPanel;
    PanelGenerateTop: TPanel;
    PanelSpellCheckingTop1: TPanel;
    // @name is the main workhorse of @classname.  It analyzes the source
    // code and cooperates with @link(HtmlDocGenerator)
    // and @link(TexDocGenerator) to create the output.
    PasDoc1: TPasDoc;
    // @name generates HTML output.
    HtmlDocGenerator: THTMLDocGenerator;
    OpenDialog1: TOpenDialog;
    RadioImplicitVisibility: TRadioGroup;
    rgLineBreakQuality: TRadioGroup;
    SaveDialog1: TSaveDialog;
    OpenDialog2: TOpenDialog;
    OpenDialog3: TOpenDialog;
    MainMenu1: TMainMenu;
    MenuFile: TMenuItem;
    MenuOpen: TMenuItem;
    MenuSaveAs: TMenuItem;
    MenuExit: TMenuItem;
    MenuNew: TMenuItem;
    seVerbosity: TSpinEdit;
    SimpleXMLDocGenerator: TSimpleXMLDocGenerator;
    Splitter1: TSplitter;
    seComment: TSynEdit;
    Splitter2: TSplitter;
    // @name generates Latex output.
    TexDocGenerator: TTexDocGenerator;
    MenuHelp: TMenuItem;
    tvUnits: TTreeView;
    Label12: TLabel;
    PageSort: TPage;
    LabelItemsToSort: TLabel;
    clbSorting: TCheckListBox;
    PanelLeft: TPanel;
    lbNavigation: TListBox;
    ButtonGenerate: TBitBtn;
    LabelOutputDirectory: TLabel;
    EditOutputDirectory: TDirectoryEdit;
    LabelHyphenatedWords: TLabel;
    memoHyphenatedWords: TMemo;
    LabelCommentMarkers: TLabel;
    rgCommentMarkers: TRadioGroup;
    procedure ButtonURLClick(Sender: TObject);
    procedure ComboImplementationCommentsChange(Sender: TObject);
    procedure ComboInheritedMembersChange(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure FormKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure MenuContextHelpClick(Sender: TObject);
    procedure MenuGenerateRunClick(Sender: TObject);
    procedure MenuPreferencesClick(Sender: TObject);
    procedure MenuSaveClick(Sender: TObject);
    procedure SomethingChanged(Sender: TObject);
    procedure MenuAboutClick(Sender: TObject);
    procedure PasDoc1Warning(const MessageType: TPasDocMessageType;
      const AMessage: string; const AVerbosity: Cardinal);
    procedure btnBrowseSourceFilesClick(Sender: TObject);
    procedure BtnBrowseAdditionalFilesClick(Sender: TObject);
    procedure cbCheckSpellingChange(Sender: TObject);
    procedure CheckListVisibleMembersClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure ButtonGenerateDocsClick(Sender: TObject);
    procedure comboLanguagesChange(Sender: TObject);
    procedure btnBrowseIncludeDirectoryClick(Sender: TObject);
    procedure btnOpenClick(Sender: TObject);
    procedure MenuSaveAsClick(Sender: TObject);
    procedure Exit1Click(Sender: TObject);
    procedure FormClose(Sender: TObject; var AnAction: TCloseAction);
    procedure MenuNewClick(Sender: TObject);
    procedure comboGenerateFormatChange(Sender: TObject);
    procedure lbNavigationClick(Sender: TObject);
    procedure rgCommentMarkersClick(Sender: TObject);
    // @name displays the comment associated with the selected node of
    // @link(tvUnits) in @link(seComment).
    procedure tvUnitsClick(Sender: TObject);
  private
    function GetCheckListVisibleMembersValue: TVisibilities;
    procedure ReadSettingsFromFile(const FileName: string);
    procedure SetCheckListVisibleMembersValue(const AValue: TVisibilities);
  private
    FChanged: boolean;
    FSettingsFileName: string;
    MisspelledWords: TStringList;
    InsideCreateWnd: boolean;

    { If Changed then this offers user the chance to save the project.
      Returns @false when user chose to Cancel the whole operation
      (not only file saving, but also the parent operation -- you
      should always check the result of this function and cancel
      anything further if result is false). }
    function SaveChanges: boolean;

    procedure SetChanged(const AValue: boolean);
    procedure SetDefaults;
    procedure SetSettingsFileName(const AValue: string);
    procedure UpdateCaption;
    procedure FillNavigationListBox;
    procedure SetOutputDirectory(const FileName: string);
    // @name fills @link(tvUnits) with a heirarchical representation of the
    // TPasItems in PasDoc1.
    procedure FillTreeView;

    { This property allows to get and set all
      CheckListVisibleMembers.Checked[] values as a simple
      TVisibilities type. }
    property CheckListVisibleMembersValue: TVisibilities
      read GetCheckListVisibleMembersValue
      write SetCheckListVisibleMembersValue;

    { Saves current settings to FileName. Additionally may
      also do some other things commonly done at saving time:

      if UpdateSettingsFileName then sets SettingsFileName property
      to FileName.

      if ClearChanged then sets Changed to false. }
    procedure SaveSettingsToFile(const FileName: string;
      UpdateSettingsFileName, ClearChanged: boolean);
  protected
    procedure CreateWnd; override;
  public
    DefaultDirectives: TStringList;

    // @name is @true when the user has changed the project settings.
    // Otherwise it is @false.
    property Changed: boolean read FChanged write SetChanged;

    { This is the settings filename (.pds file) that is currently
      opened. You can look at pasdoc_gui as a "program to edit pds files".
      It is '' if current settings are not associated with any filename
      (because user did not open any pds file, or he chose "New" menu item). }
    property SettingsFileName: string read FSettingsFileName
      write SetSettingsFileName;

    { If SettingsFileName <> '',
      this returns ExtractFileName(SettingsFileName),
      else it returns 'Unsaved PasDoc settings'. This is good
      when you want to nicely present the value of SettingsFileName
      to the user.

      This follows GNOME HIG standard for window caption. }
    function SettingsFileNameNice: string;
  end;

var
  // @name is the main form of Help Generator
  frmHelpGenerator: TfrmHelpGenerator;

implementation

uses LCLIntf, PasDoc_SortSettings, frmAboutUnit, HelpProcessor,
  PreferencesFrm, PasDocGuiSettings;

procedure TfrmHelpGenerator.PasDoc1Warning(const MessageType: TPasDocMessageType;
  const AMessage: string; const AVerbosity: Cardinal);
const
  MisText = 'Word misspelled "';
var
  MisspelledWord: string;
begin
  memoMessages.Lines.Add(AMessage);
  if Pos(MisText, AMessage) =1 then begin
    MisspelledWord := Copy(AMessage, Length(MisText)+1, MAXINT);
    SetLength(MisspelledWord, Length(MisspelledWord) -1);
    MisspelledWords.Add(MisspelledWord)
  end;
end;

procedure TfrmHelpGenerator.MenuAboutClick(Sender: TObject);
begin
  frmAbout.ShowModal;
end;

procedure TfrmHelpGenerator.SetOutputDirectory(const FileName: string);
begin
  EditOutputDirectory.Directory := ExtractFileDir(FileName)
    + PathDelim + 'PasDoc';
end;

procedure TfrmHelpGenerator.SomethingChanged(Sender: TObject);
begin
  { Some components (in Lazarus 0.9.10, this concerns at
    least TMemo with GTK 1 interface) generate some OnChange
    event when creating their widget (yes, I made sure: it doesn't
    happen when reading their properties.)

    This is not good, because when we open pasdoc_gui,
    the default project should be left with Changed = false.

    Checking ComponentState and ControlState to safeguard
    against this is not possible. I'm using InsideCreateWnd to
    safeguard against this. }
  if InsideCreateWnd then Exit;

  Changed := true;
  if (memoFiles.Lines.Count > 0) and
     (EditOutputDirectory.Directory = '') then
  begin
    SetOutputDirectory(memoFiles.Lines[0]);
  end;

  MemoAutoLinkExclude.Enabled := CheckAutoLink.Checked;
end;

procedure TfrmHelpGenerator.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key = VK_F1 then
  begin
    MenuContextHelpClick(ActiveControl);
  end;
end;

procedure TfrmHelpGenerator.ButtonURLClick(Sender: TObject);
begin
  OpenURL((Sender as TButton).Caption);
end;

procedure TfrmHelpGenerator.ComboImplementationCommentsChange(Sender: TObject);
begin
  Changed := true;
end;

procedure TfrmHelpGenerator.ComboInheritedMembersChange(Sender: TObject);
begin
  Changed := true;
end;

procedure TfrmHelpGenerator.FormDestroy(Sender: TObject);
begin
  DefaultDirectives.Free;
  MisspelledWords.Free;
end;

procedure TfrmHelpGenerator.btnBrowseSourceFilesClick(Sender: TObject);
var
  Directory: string;
  FileIndex: integer;
  Files: TStringList;
begin
  if OpenDialog1.Execute then
  begin
    Files := TStringList.Create;
    try

      if EditOutputDirectory.Directory = '' then
      begin
        SetOutputDirectory(OpenDialog1.FileName);
      end;

      Files.Sorted := True;
      Files.Duplicates := dupIgnore;

      Files.AddStrings(memoFiles.Lines);
      Files.AddStrings(OpenDialog1.Files);

      memoFiles.Lines := Files;

      for FileIndex := 0 to OpenDialog1.Files.Count - 1 do
      begin
        Directory := ExtractFileDir(OpenDialog1.Files[FileIndex]);
        if memoIncludeDirectories.Lines.IndexOf(Directory) < 0 then
        begin
          memoIncludeDirectories.Lines.Add(Directory);
        end;
      end;
    finally
      Files.Free;
    end;
  end;
end;

procedure TfrmHelpGenerator.BtnBrowseAdditionalFilesClick(Sender: TObject);
var
  Directory: string;
  FileIndex: integer;
  Files: TStringList;
begin
  if OpenDialog3.Execute then
  begin
    Files := TStringList.Create;
    try

      if EditOutputDirectory.Directory = '' then
      begin
        SetOutputDirectory(OpenDialog3.FileName);
      end;

      Files.Sorted := True;
      Files.Duplicates := dupIgnore;

      Files.AddStrings(MemoAdditionalFiles.Lines);
      Files.AddStrings(OpenDialog3.Files);

      MemoAdditionalFiles.Lines := Files;

      for FileIndex := 0 to OpenDialog3.Files.Count - 1 do
      begin
        Directory := ExtractFileDir(OpenDialog3.Files[FileIndex]);
        if memoIncludeDirectories.Lines.IndexOf(Directory) < 0 then
        begin
          memoIncludeDirectories.Lines.Add(Directory);
        end;
      end;
    finally
      Files.Free;
    end;
  end;
end;

procedure TfrmHelpGenerator.FillNavigationListBox;
var
  Index: integer;
  page: TPage;
begin
  { Under GTK interface, lbNavigation.OnClick event may occur
    when we change lbNavigation.Items. Our lbNavigationClick
    is not ready to handle this, so we turn him off. }
  lbNavigation.OnClick := nil;
  try
    lbNavigation.Items.Clear;
    for Index := 0 to NotebookMain.PageCount -1 do
    begin
      page := NotebookMain.Page[Index];
      lbNavigation.Items.AddObject(page.Hint, page);
    end;
    lbNavigation.ItemIndex := 0; { otherwise it may stay -1 after program loads }
  finally
    lbNavigation.OnClick := @lbNavigationClick;
  end;
end;

procedure TfrmHelpGenerator.cbCheckSpellingChange(Sender: TObject);
begin
  Changed := True;
end;

procedure TfrmHelpGenerator.CheckListVisibleMembersClick(Sender: TObject);
var
  NewValue: TVisibilities;
begin
  NewValue := CheckListVisibleMembersValue;

  if PasDoc1.ShowVisibilities <> NewValue then
  begin
    Changed := True;
    PasDoc1.ShowVisibilities := NewValue;
  end;
end;

procedure TfrmHelpGenerator.SetDefaults;
var
  SortIndex: TSortSetting;
begin
  CheckListVisibleMembersValue := DefaultVisibilities;
  RadioImplicitVisibility.ItemIndex := 0;

  comboLanguages.ItemIndex := Ord(lgEnglish);
  comboLanguagesChange(nil);

  comboGenerateFormat.ItemIndex := 0;
  comboGenerateFormatChange(nil);

  ComboImplementationComments.ItemIndex := Ord(imtNone);
  ComboImplementationCommentsChange(nil);

  ComboInheritedMembers.ItemIndex := Ord(imNever);
  ComboInheritedMembersChange(nil);

  edTitle.Text := '';
  edProjectName.Text := '';
  EditOutputDirectory.Directory := GetTempDir(true);
  seVerbosity.Value := 2;
  comboGenerateFormat.ItemIndex := 0;
  memoFiles.Clear;
  memoIncludeDirectories.Clear;
  memoMessages.Clear;
  MemoAutoLinkExclude.Clear;

  memoDefines.Lines.Assign(DefaultDirectives);

  EditCssFileName.FileName := '';
  CheckBoxBootstrap.Checked := true;
  EditIntroductionFileName.FileName := '';
  EditConclusionFileName.FileName := '';
  EditHtmlHead.FileName := '';
  EditHtmlBodyBegin.FileName := '';
  EditHtmlBodyEnd.FileName := '';
  EditExternalDescriptions.FileName := '';

  CheckWriteUsesList.Checked := false;
  CheckAutoAbstract.Checked := TDocGenerator.DefaultAutoAbstract;
  CheckAutoLink.Checked := false;
  CheckHandleMacros.Checked := true;
  CheckUseTipueSearch.Checked := false;
  CheckMarkdown.Checked := false;
  CheckAutoBackComments.Checked := false;
  CheckOpenHTML.Checked := true;

  for SortIndex := Low(TSortSetting) to High(TSortSetting) do
    clbSorting.Checked[Ord(SortIndex)] := false;

  CheckStoreRelativePaths.Checked := true;

  Changed := False;
end;

procedure TfrmHelpGenerator.UpdateCaption;
var
  NewCaption: string;
begin
  { Caption value follows GNOME HIG 2.0 standard }
  NewCaption := '';
  if Changed then NewCaption += '*';
  NewCaption += SettingsFileNameNice;
  NewCaption += ' - PasDoc GUI';
  Caption := NewCaption;
end;

procedure TfrmHelpGenerator.SetChanged(const AValue: boolean);
begin
  if FChanged = AValue then Exit;
  FChanged := AValue;
  UpdateCaption;
end;

procedure TfrmHelpGenerator.SetSettingsFileName(const AValue: string);
begin
  FSettingsFileName := AValue;
  UpdateCaption;
end;

procedure TfrmHelpGenerator.FormCreate(Sender: TObject);
var
  LanguageIndex: TLanguageID;
  Index: integer;
  Vis: TVisibility;
begin
  MisspelledWords:= TStringList.Create;
  MisspelledWords.Sorted := True;
  MisspelledWords.Duplicates := dupIgnore;

  comboLanguages.Items.Capacity :=
    Ord(High(TLanguageID)) - Ord(Low(TLanguageID)) + 1;
  for LanguageIndex := Low(TLanguageID) to High(TLanguageID) do
  begin
    comboLanguages.Items.Add(LanguageInformation(LanguageIndex)^.Name);
  end;

  Constraints.MinWidth := Width;
  Constraints.MinHeight := Height;

  DefaultDirectives := TStringList.Create;

  { Original HelpGenerator did here
    DefaultDirectives.Assign(memoDefines.Lines)
    I like this solution, but unfortunately current Lazarus seems
    to sometimes "lose" value of TMemo.Lines...
    So I'm setting these values at runtime. }

  {$IFDEF FPC}
  DefaultDirectives.Append('FPC');
  {$ENDIF}
  {$IFDEF UNIX}
  DefaultDirectives.Append('UNIX');
  {$ENDIF}
  {$IFDEF LINUX}
  DefaultDirectives.Append('LINUX');
  {$ENDIF}
  {$IFDEF DEBUG}
  DefaultDirectives.Append('DEBUG');
  {$ENDIF}

  {$IFDEF VER130}
  DefaultDirectives.Append('VER130');
  {$ENDIF}
  {$IFDEF VER140}
  DefaultDirectives.Append('VER140');
  {$ENDIF}
  {$IFDEF VER150}
  DefaultDirectives.Append('VER150');
  {$ENDIF}
  {$IFDEF VER160}
  DefaultDirectives.Append('VER160');
  {$ENDIF}
  {$IFDEF VER170}
  DefaultDirectives.Append('VER170');
  {$ENDIF}
  {$IFDEF MSWINDOWS}
  DefaultDirectives.Append('MSWINDOWS');
  {$ENDIF}
  {$IFDEF WIN32}
  DefaultDirectives.Append('WIN32');
  {$ENDIF}
  {$IFDEF CPU386}
  DefaultDirectives.Append('CPU386');
  {$ENDIF}
  {$IFDEF CONDITIONALEXPRESSIONS}
  DefaultDirectives.Append('CONDITIONALEXPRESSIONS');
  {$ENDIF}

  CheckListVisibleMembers.Items.Clear;
  for Vis := Low(TVisibility) to High(TVisibility) do
  begin
    CheckListVisibleMembers.Items.Add(VisibilityStr[Vis]);
  end;

  // Have command line argument? It must be path to settings file, load it.
  if ParamCount > 0 then
    ReadSettingsFromFile(ExpandFileName(ParamStr(1)))
  else
    SetDefaults;

  { It's too easy to change it at design-time, so we set it at runtime. }
  NotebookMain.PageIndex := 0;
  Application.ProcessMessages;

  {$IFDEF WIN32}
  // Deal with bug in display of TSpinEdit in Win32.
  seVerbosity.Constraints.MinWidth := 60;
  seVerbosity.Width := seVerbosity.Constraints.MinWidth;
  {$ENDIF}

  { Workaround for Lazarus bug 0000713,
    [http://www.lazarus.freepascal.org/mantis/view.php?id=713]:
    we set menu shortcuts at runtime.
    (the bug is only for Win32, but we must do this workaround for every
    target). }
  MenuOpen.ShortCut := ShortCut(VK_O, [ssCtrl]);
  MenuSave.ShortCut := ShortCut(VK_S, [ssCtrl]);
  MenuGenerateRun.ShortCut := ShortCut(VK_F9, []);

  comboGenerateFormatChange(nil);

  FillNavigationListBox;

  Changed := False;
end;

procedure TfrmHelpGenerator.FillTreeView;
var
   Lang: TPasDocLanguages;

  procedure TreeAddCio(const ALLCiosNode: TTreeNode);
  var
    LCio: TPasCio;
    LCios: TPasNestedCios;
    I, J: Integer;
    ClassNode: TTreeNode;
    FieldsNode: TTreeNode;
    MethodNode: TTreeNode;
    PropertiesNode: TTreeNode;
    TypesNode: TTreeNode;
    PasItem: TPasItem;
  begin
    LCios := TPasNestedCios(ALLCiosNode.Data);
    for J := 0 to LCios.Count - 1 do
    begin
      LCio := TPasCio(LCios.PasItemAt[J]);

      ClassNode := tvUnits.Items.AddChildObject(ALLCiosNode,
        LCio.Name, LCio);

      if LCio.Fields.Count > 0 then
      begin
        FieldsNode := tvUnits.Items.AddChildObject(ClassNode,
          Lang.Translation[trFields], LCio.Fields);
        for I := 0 to LCio.Fields.Count -1 do
        begin
          PasItem := LCio.Fields.PasItemAt[I];
          tvUnits.Items.AddChildObject(FieldsNode, PasItem.Name, PasItem);
        end;
      end;
      if LCio.Methods.Count > 0 then
      begin
        MethodNode := tvUnits.Items.AddChildObject(ClassNode,
          Lang.Translation[trMethods], LCio.Methods);
        for I := 0 to LCio.Methods.Count -1 do
        begin
          PasItem := LCio.Methods.PasItemAt[I];
          tvUnits.Items.AddChildObject(MethodNode, PasItem.Name, PasItem);
        end;
      end;
      if LCio.Properties.Count > 0 then
      begin
        PropertiesNode := tvUnits.Items.AddChildObject(ClassNode,
          Lang.Translation[trProperties], LCio.Properties);
        for I := 0 to LCio.Properties.Count -1 do
        begin
          PasItem := LCio.Properties.PasItemAt[I];
          tvUnits.Items.AddChildObject(PropertiesNode, PasItem.Name, PasItem);
        end;
      end;
      if LCio.Types.Count > 0 then
      begin
        TypesNode := tvUnits.Items.AddChildObject(ClassNode,
          Lang.Translation[trNestedTypes], LCio.Types);
        for I := 0 to LCio.Types.Count -1 do
        begin
          PasItem := LCio.Types.PasItemAt[I];
          tvUnits.Items.AddChildObject(TypesNode, PasItem.Name, PasItem);
        end;
      end;
      if LCio.Cios.Count > 0 then
      begin
        ClassNode := tvUnits.Items.AddChildObject(ClassNode,
          Lang.Translation[trNestedCR], LCio.CIOs);
        TreeAddCio(ClassNode);
      end;
    end;
  end;

var
  UnitItem: TPasUnit;
  AllUnitsNode: TTreeNode;
  UnitIndex: integer;
  UnitNode: TTreeNode;
  AllTypesNode: TTreeNode;
  AllVariablesNode: TTreeNode;
  AllCIOs_Node: TTreeNode;
  AllConstantsNode: TTreeNode;
  AllProceduresNode: TTreeNode;
  UsesNode: TTreeNode;
  PasItemIndex: integer;
  PasItem: TPasItem;
  UsesIndex: integer;
begin
  tvUnits.Items.Clear;
  Lang := TPasDocLanguages.Create;
  try
    Lang.Language := TLanguageID(comboLanguages.ItemIndex);
    if PasDoc1.IntroductionFileName <> '' then
    begin
      tvUnits.Items.AddObject(nil, PasDoc1.IntroductionFileName, PasDoc1.Introduction);
    end;
    AllUnitsNode := tvUnits.Items.AddObject(nil,
      Lang.Translation[trUnits], PasDoc1.Units);
    for UnitIndex := 0 to PasDoc1.Units.Count -1 do
    begin
      UnitItem := PasDoc1.Units.UnitAt[UnitIndex];
      UnitNode := tvUnits.Items.AddChildObject(AllUnitsNode,
        UnitItem.SourceFileName, UnitItem);
      if UnitItem.Types.Count > 0 then
      begin
        AllTypesNode := tvUnits.Items.AddChildObject(UnitNode,
          Lang.Translation[trTypes], UnitItem.Types);
        for PasItemIndex := 0 to UnitItem.Types.Count -1 do
        begin
          PasItem := UnitItem.Types.PasItemAt[PasItemIndex];
          tvUnits.Items.AddChildObject(AllTypesNode, PasItem.Name, PasItem);
        end;
      end;
      if UnitItem.Variables.Count > 0 then
      begin
        AllVariablesNode := tvUnits.Items.AddChildObject(UnitNode,
          Lang.Translation[trVariables], UnitItem.Variables);
        for PasItemIndex := 0 to UnitItem.Variables.Count -1 do
        begin
          PasItem := UnitItem.Variables.PasItemAt[PasItemIndex];
          tvUnits.Items.AddChildObject(AllVariablesNode, PasItem.Name, PasItem);
        end;
      end;
      if UnitItem.CIOs.Count > 0 then
      begin
        AllCIOs_Node := tvUnits.Items.AddChildObject(UnitNode,
          Lang.Translation[trCio], UnitItem.CIOs);
        TreeAddCio(AllCIOs_Node);
      end;
      if UnitItem.Constants.Count > 0 then
      begin
        AllConstantsNode := tvUnits.Items.AddChildObject(UnitNode,
          Lang.Translation[trConstants], UnitItem.Constants);
        for PasItemIndex := 0 to UnitItem.Constants.Count -1 do
        begin
          PasItem := UnitItem.Constants.PasItemAt[PasItemIndex];
          tvUnits.Items.AddChildObject(AllConstantsNode, PasItem.Name, PasItem);
        end;
      end;
      if UnitItem.FuncsProcs.Count > 0 then
      begin
        AllProceduresNode := tvUnits.Items.AddChildObject(UnitNode,
          Lang.Translation[trFunctionsAndProcedures], UnitItem.FuncsProcs);
        for PasItemIndex := 0 to UnitItem.FuncsProcs.Count -1 do
        begin
          PasItem := UnitItem.FuncsProcs.PasItemAt[PasItemIndex];
          tvUnits.Items.AddChildObject(AllProceduresNode, PasItem.Name, PasItem);
        end;
      end;
      if UnitItem.UsesUnits.Count > 0 then
      begin
        UsesNode := tvUnits.Items.AddChildObject(UnitNode,
          Lang.Translation[trUses], UnitItem.UsesUnits);
        for UsesIndex := 0 to UnitItem.UsesUnits.Count -1 do
        begin
          tvUnits.Items.AddChild(UsesNode, UnitItem.UsesUnits[UsesIndex]);
        end;
      end;
    end;
    if PasDoc1.ConclusionFileName <> '' then
    begin
      tvUnits.Items.AddObject(nil, PasDoc1.ConclusionFileName,
        PasDoc1.Conclusion);
    end;
  finally
    Lang.Free;
  end;
end;

procedure TfrmHelpGenerator.ButtonGenerateDocsClick(Sender: TObject);

  { Load the contents of file referenced by Edit,
    or return DefaultResult if no file chosen. }
  function LoadFileNameEdit(const Edit: TFileNameEdit; const DefaultResult: string): string;
  begin
    if Edit.FileName <> '' then
      Result := FileToString(Edit.FileName)
    else
      Result := DefaultResult;
  end;

var
  Files: TStringList;
  index: integer;
  SortIndex: TSortSetting;
const
  VizGraphImageExtension = 'png';
begin
  if EditOutputDirectory.Directory = '' then
  begin
    Beep;
    MessageDlg('You need to specify the output directory on the "Options" tab.',
      Dialogs.mtWarning, [mbOK], 0);
    Exit;
  end;
  Screen.Cursor := crHourGlass;
  try
    memoMessages.Clear;

    case comboGenerateFormat.ItemIndex of
      0: PasDoc1.Generator := HtmlDocGenerator;
      1: PasDoc1.Generator := HtmlHelpDocGenerator;
      2, 3:
         begin
           PasDoc1.Generator := TexDocGenerator;
           TexDocGenerator.Latex2rtf := (comboGenerateFormat.ItemIndex = 3);

           TexDocGenerator.LatexHead.Clear;
           if rgLineBreakQuality.ItemIndex = 1 then
           begin
             TexDocGenerator.LatexHead.Add('\sloppy');
           end;
           if memoHyphenatedWords.Lines.Count > 0 then
           begin
             TexDocGenerator.LatexHead.Add('\hyphenation{');
             for Index := 0 to memoHyphenatedWords.Lines.Count -1 do
             begin
               TexDocGenerator.LatexHead.Add(memoHyphenatedWords.Lines[Index]);
             end;
             TexDocGenerator.LatexHead.Add('}');
           end;

           case comboLatexGraphicsPackage.ItemIndex of
             0: // none
               begin
                 // do nothing
               end;
             1: // PDF
               begin
                 TexDocGenerator.LatexHead.Add('\usepackage[pdftex]{graphicx}');
               end;
             2: // DVI
               begin
                 TexDocGenerator.LatexHead.Add('\usepackage[dvips]{graphicx}');
               end;
             else Assert(False);
           end;
         end;
      4: PasDoc1.Generator := SimpleXMLDocGenerator;
      5: PasDoc1.Generator := PHPDocGenerator;
      else Assert(False);
    end;

    PasDoc1.Generator.Language := TLanguageID(comboLanguages.ItemIndex);
    PasDoc1.InfoMergeType := TInfoMergeType(ComboImplementationComments.ItemIndex);

    if PasDoc1.Generator is TGenericHTMLDocGenerator then
    begin
      TGenericHTMLDocGenerator(PasDoc1.Generator).Header := memoHeader.Lines.Text;
      TGenericHTMLDocGenerator(PasDoc1.Generator).Footer := memoFooter.Lines.Text;
      TGenericHTMLDocGenerator(PasDoc1.Generator).CSS :=
        LoadFileNameEdit(EditCssFileName, DefaultPasDocCss);
      TGenericHTMLDocGenerator(PasDoc1.Generator).Bootstrap := CheckBoxBootstrap.Checked;
      TGenericHTMLDocGenerator(PasDoc1.Generator).HtmlHead :=
        LoadFileNameEdit(EditHtmlHead, '');
      TGenericHTMLDocGenerator(PasDoc1.Generator).HtmlBodyBegin :=
        LoadFileNameEdit(EditHtmlBodyBegin, '');
      TGenericHTMLDocGenerator(PasDoc1.Generator).HtmlBodyEnd :=
        LoadFileNameEdit(EditHtmlBodyEnd, '');
      TGenericHTMLDocGenerator(PasDoc1.Generator).UseTipueSearch :=
        CheckUseTipueSearch.Checked;
      TGenericHTMLDocGenerator(PasDoc1.Generator).Markdown :=
        CheckMarkdown.Checked;
      TGenericHTMLDocGenerator(PasDoc1.Generator).AspellLanguage := LanguageCode(TLanguageID(comboLanguages.ItemIndex));
      TGenericHTMLDocGenerator(PasDoc1.Generator).CheckSpelling := cbCheckSpelling.Checked;
      if cbCheckSpelling.Checked then
      begin
        TGenericHTMLDocGenerator(PasDoc1.Generator).SpellCheckIgnoreWords.Assign(memoSpellCheckingIgnore.Lines);
      end;
      TGenericHTMLDocGenerator(PasDoc1.Generator).InheritedMembers :=
        TInheritedMembers(ComboInheritedMembers.ItemIndex);
    end;

    // Create the output directory if it does not exist.
    if not DirectoryExists(EditOutputDirectory.Directory) then
    begin
      CreateDir(EditOutputDirectory.Directory)
    end;
    PasDoc1.Generator.DestinationDirectory := EditOutputDirectory.Directory;

    PasDoc1.Generator.WriteUsesClause := CheckWriteUsesList.Checked;
    PasDoc1.Generator.AutoAbstract := CheckAutoAbstract.Checked;
    PasDoc1.AutoLink := CheckAutoLink.Checked;
    PasDoc1.Generator.AutoLinkExclude.Assign(MemoAutoLinkExclude.Lines);
    PasDoc1.HandleMacros := CheckHandleMacros.Checked;

    PasDoc1.AutoBackComments := CheckAutoBackComments.Checked;

    PasDoc1.ProjectName := edProjectName.Text;
    PasDoc1.IntroductionFileName := EditIntroductionFileName.Text;
    PasDoc1.ConclusionFileName := EditConclusionFileName.Text;
    PasDoc1.DescriptionFileNames.Text := EditExternalDescriptions.FileName;

    { CheckListVisibleMembersClick event *should* already
      take care of setting PasDoc1.ShowVisibilities.
      Unfortunately CheckListVisibleMembersClick is not guarenteed
      to be fired on every change of state of
      CheckListVisibleMembersValue. See Lazarus bug
      [http://www.lazarus.freepascal.org/mantis/view.php?id=905].
      So sometimes user will click on CheckListVisibleMembers
      and Changed will not be updated as it should.
      Below we at least make sure that PasDoc1.ShowVisibilities
      is always updated. }
    PasDoc1.ShowVisibilities := CheckListVisibleMembersValue;

    PasDoc1.ImplicitVisibility :=
      TImplicitVisibility(RadioImplicitVisibility.ItemIndex);

    Files := TStringList.Create;
    try
      Files.AddStrings(memoFiles.Lines);
      PasDoc1.SourceFileNames.Clear;
      PasDoc1.AddSourceFileNames(Files);

      Files.Clear;
      Files.AddStrings(memoIncludeDirectories.Lines);
      PasDoc1.IncludeDirectories.Assign(Files);

      Files.Clear;
      Files.AddStrings(memoDefines.Lines);
      PasDoc1.Directives.Assign(Files);

      Files.Clear;
      Files.AddStrings(MemoAdditionalFiles.Lines);
      PasDoc1.AdditionalFilesNames.Assign(Files);
    finally
      Files.Free;
    end;
    PasDoc1.Verbosity := seVerbosity.Value;

    case rgCommentMarkers.ItemIndex of
      0:
        begin
          PasDoc1.CommentMarkers.Clear;
          PasDoc1.MarkerOptional := True;
        end;
      1:
        begin
          PasDoc1.MarkerOptional := True;
          PasDoc1.CommentMarkers.Assign(memoCommentMarkers.Lines);
        end;
      2:
        begin
          PasDoc1.MarkerOptional := False;
          PasDoc1.CommentMarkers.Assign(memoCommentMarkers.Lines);
        end;
    else
      Assert(False);
    end;

    if edTitle.Text = '' then begin
      PasDoc1.Title := edProjectName.Text;
    end
    else begin
      PasDoc1.Title := edTitle.Text;
    end;

    if cbVizGraphClasses.Checked then begin
      PasDoc1.Generator.OutputGraphVizClassHierarchy := True;
      PasDoc1.Generator.LinkGraphVizClasses := VizGraphImageExtension;
    end
    else begin
      PasDoc1.Generator.OutputGraphVizClassHierarchy := False;
      PasDoc1.Generator.LinkGraphVizClasses := '';
    end;

    if cbVizGraphUses.Checked then begin
      PasDoc1.Generator.OutputGraphVizUses := True;
      PasDoc1.Generator.LinkGraphVizUses := VizGraphImageExtension;
    end
    else begin
      PasDoc1.Generator.OutputGraphVizUses := False;
      PasDoc1.Generator.LinkGraphVizUses := '';
    end;

    Assert(Ord(High(TSortSetting)) = clbSorting.Items.Count -1);
    PasDoc1.SortSettings := [];
    for SortIndex := Low(TSortSetting) to High(TSortSetting) do
    begin
      if clbSorting.Checked[Ord(SortIndex)] then begin
        PasDoc1.SortSettings := PasDoc1.SortSettings + [SortIndex];
      end;
    end;

    MisspelledWords.Clear;
    PasDoc1.Execute;

    if MisspelledWords.Count > 0 then
    begin
      memoMessages.Lines.Add('');
      memoMessages.Lines.Add('Misspelled Words');
      memoMessages.Lines.AddStrings(MisspelledWords)
    end;

    FillTreeView;

    if cbVizGraphUses.Checked or cbVizGraphClasses.Checked then begin
      // To do: actually start dot here.
      MessageDlg('You will have to run the GraphViz "dot" program to generate '
        + 'the images used in your documentation.', Dialogs.mtInformation,
        [mbOK], 0);
    end;

    if PasDoc1.Generator is TGenericHTMLDocGenerator then
      if CheckOpenHTML.Checked then
        OpenURL(HtmlDocGenerator.DestinationDirectory + 'index.html');
  finally
    Screen.Cursor := crDefault;
  end;
end;

procedure TfrmHelpGenerator.comboLanguagesChange(Sender: TObject);
begin
  Changed := True;
end;

procedure TfrmHelpGenerator.btnBrowseIncludeDirectoryClick(Sender: TObject);
var
  directory: string;
begin
  if memoIncludeDirectories.Lines.Count > 0 then
  begin
    directory := memoIncludeDirectories.Lines[
      memoIncludeDirectories.Lines.Count - 1];
  end
  else
  begin
    directory := '';
  end;

  if SelectDirectory('Select directory to include', '', directory)
    then
  begin
    if memoIncludeDirectories.Lines.IndexOf(directory) < 0 then
    begin
      memoIncludeDirectories.Lines.Add(directory);
    end
    else
    begin
      MessageDlg('The directory you selected, (' + directory
        + ') is already included.', Dialogs.mtInformation, [mbOK], 0);
    end;
  end;
end;

procedure TfrmHelpGenerator.btnOpenClick(Sender: TObject);
begin
  if not SaveChanges then Exit;

  if OpenDialog2.Execute then
    ReadSettingsFromFile(OpenDialog2.FileName);
end;

procedure TfrmHelpGenerator.ReadSettingsFromFile(const FileName: string);
var
  Ini: TIniFile;

  procedure ReadStrings(const Section: string; S: TStrings);
  var i: Integer;
  begin
    S.Clear;
    for i := 0 to Ini.ReadInteger(Section, 'Count', 0) - 1 do
      S.Append(Ini.ReadString(Section, 'Item_' + IntToStr(i), ''));
  end;

  { When reading any filename from Ini file, we make sure
    that it's an absolute filename. This is needed to
    properly handle the case when user choses "Save As"
    and stores the same project within a different directory.
    So it's safest to always keep absolute filenames
    when project is loaded in pasdoc_gui.

    Below are some helper wrappers around ExpandFileName
    that help us with this. }

  { This returns '' if FileName is '', else returns
    ExpandFileName(FileName). It's useful because often
    FileName = '' has special meaning:
    it means that "given filename was not chosen by user",
    so calling ExpandFileName is not wanted in this case. }
  function ExpandNotEmptyFileName(const FileName: string): string;
  begin
    if FileName = '' then
      Result := '' else
      Result := ExpandFileName(FileName);
  end;

  { Call ExpandNotEmptyFileName on each item. }
  procedure ExpandFileNames(List: TStrings);
  var
    I: Integer;
  begin
    for I := 0 to List.Count - 1 do
      List[I] := ExpandNotEmptyFileName(List[I]);
  end;

  procedure ReadFileNames(const Section: string; S: TStrings);
  begin
    ReadStrings(Section, S);
    ExpandFileNames(S);
  end;

var
  i: Integer;
  SettingsFileNamePath: string;
  LanguageSyntax: string;
  LanguageId: TLanguageID;
begin
  SettingsFileName := FileName;
  SaveDialog1.FileName := SettingsFileName;

  { Change current directory now to SettingsFileNamePath,
    this is needed to make all subsequent ExpandFileName
    operations work with respect to SettingsFileNamePath. }
  SettingsFileNamePath := ExtractFilePath(SettingsFileName);
  if not SetCurrentDir(SettingsFileNamePath) then
    raise Exception.CreateFmt('Cannot change current directory to "%s"',
      [SettingsFileNamePath]);

  Ini := TIniFile.Create(SettingsFileName);
  try
    { Default values for ReadXxx() methods here are not so important,
      don't even try to set them right.
      *Good* default values are set in SetDefaults method of this class.
      Here we can assume that values are always present in ini file.

      Well, OK, in case user will modify settings file by hand we should
      set here some sensible default values... also in case we will add
      in the future some new values to this file...
      so actually we should set here sensible "default values".
      We can think of them as "good default values for user opening a settings
      file written by older version of pasdoc_gui program".
      They need not necessarily be equal to default values set by
      SetDefaults method, and this is very good, as it may give us
      additional possibilities. }

    CheckStoreRelativePaths.Checked :=
      Ini.ReadBool('Main', 'StoreRelativePaths', true);

    { Compatibility: in version < 0.11.0, we stored only the "id" (just an
      index to LANGUAGE_ARRAY) of the language. This was very wrong, as the
      id can change between pasdoc releases (items can get shifted and moved
      in the LANGUAGE_ARRAY). So now we store language "syntax" code
      (the same thing as is used for --language command-line option),
      as this is guaranteed to stay "stable".

      To do something mildly sensible when opening pds files from older
      versions, we set language to default (English) when language string
      is not recognized. }

    LanguageSyntax := Ini.ReadString('Main', 'Language',
      LanguageInformation(DEFAULT_LANGUAGE)^.Syntax);
    if not LanguageFromStr(LanguageSyntax, LanguageId) then
      LanguageId := DEFAULT_LANGUAGE;
    comboLanguages.ItemIndex := Ord(LanguageId);
    comboLanguagesChange(nil);

    EditOutputDirectory.Directory := ExpandNotEmptyFileName(
      Ini.ReadString('Main', 'OutputDir', ''));

    comboGenerateFormat.ItemIndex := Ini.ReadInteger('Main', 'GenerateFormat', 0);
    comboGenerateFormatChange(nil);

    ComboImplementationComments.ItemIndex := Ini.ReadInteger('Main', 'ImplementationComments', Ord(imtNone));
    ComboImplementationCommentsChange(nil);

    ComboInheritedMembers.ItemIndex := Ini.ReadInteger('Main', 'InheritedMembers', Ord(imNever));
    ComboInheritedMembersChange(nil);

    edProjectName.Text := Ini.ReadString('Main', 'ProjectName', '');
    seVerbosity.Value := Ini.ReadInteger('Main', 'Verbosity', 0);

    Assert(Ord(High(TVisibility)) = CheckListVisibleMembers.Items.Count -1);
    for i := Ord(Low(TVisibility)) to Ord(High(TVisibility)) do
      CheckListVisibleMembers.Checked[i] := Ini.ReadBool(
        'Main', 'ClassMembers_' + IntToStr(i), true);
    CheckListVisibleMembersClick(nil);

    RadioImplicitVisibility.ItemIndex :=
      Ini.ReadInteger('Main', 'ImplicitVisibility', 0);

    Assert(Ord(High(TSortSetting)) = clbSorting.Items.Count -1);
    for i := Ord(Low(TSortSetting)) to Ord(High(TSortSetting)) do
    begin
      clbSorting.Checked[i] := Ini.ReadBool(
        'Main', 'Sorting_' + IntToStr(i), True);
    end;

    ReadStrings('Defines', memoDefines.Lines);
    ReadStrings('Header', memoHeader.Lines);
    ReadStrings('Footer', memoFooter.Lines);
    ReadStrings('AutoLinkExclude', MemoAutoLinkExclude.Lines);
    ReadFileNames('IncludeDirectories', memoIncludeDirectories.Lines);
    ReadFileNames('Files', memoFiles.Lines);

    EditCssFileName.FileName := ExpandNotEmptyFileName(
      Ini.ReadString('Main', 'CssFileName', ''));
    CheckBoxBootstrap.Checked := Ini.ReadBool('Main', 'Bootstrap', true);
    EditIntroductionFileName.FileName := ExpandNotEmptyFileName(
      Ini.ReadString('Main', 'IntroductionFileName', ''));
    EditConclusionFileName.FileName := ExpandNotEmptyFileName(
      Ini.ReadString('Main', 'ConclusionFileName', ''));
    EditHtmlHead.FileName := ExpandNotEmptyFileName(
      Ini.ReadString('Main', 'HtmlHead', ''));
    EditHtmlBodyBegin.FileName := ExpandNotEmptyFileName(
      Ini.ReadString('Main', 'HtmlBodyBegin', ''));
    EditHtmlBodyEnd.FileName := ExpandNotEmptyFileName(
      Ini.ReadString('Main', 'HtmlBodyEnd', ''));
    EditExternalDescriptions.FileName := ExpandNotEmptyFileName(
      Ini.ReadString('Main', 'ExternalDescriptions', ''));
    ReadFileNames('AdditionalFiles', MemoAdditionalFiles.Lines);

    CheckWriteUsesList.Checked := Ini.ReadBool('Main', 'WriteUsesList', false);
    CheckAutoAbstract.Checked := Ini.ReadBool('Main', 'AutoAbstract', TDocGenerator.DefaultAutoAbstract);
    CheckAutoLink.Checked := Ini.ReadBool('Main', 'AutoLink', false);
    CheckHandleMacros.Checked := Ini.ReadBool('Main', 'HandleMacros', true);
    CheckUseTipueSearch.Checked := Ini.ReadBool('Main', 'UseTipueSearch', false);
    CheckMarkdown.Checked := Ini.ReadBool('Main', 'Markdown', false);
    CheckAutoBackComments.Checked := Ini.ReadBool('Main', 'AutoBackComments', false);
    CheckOpenHTML.Checked := Ini.ReadBool('Main', 'OpenHTML', true);
    rgLineBreakQuality.ItemIndex := Ini.ReadInteger('Main', 'LineBreakQuality', 0);
    ReadStrings('HyphenatedWords', memoHyphenatedWords.Lines);
    rgCommentMarkers.ItemIndex := Ini.ReadInteger('Main', 'SpecialMarkerTreatment', 1);
    ReadStrings('SpecialMarkers', memoCommentMarkers.Lines);
    edTitle.Text := Ini.ReadString('Main', 'Title', '');
    cbVizGraphClasses.Checked := Ini.ReadBool('Main', 'VizGraphClasses', false);
    cbVizGraphUses.Checked := Ini.ReadBool('Main', 'VizGraphUses', false);

    cbCheckSpelling.Checked :=
      Ini.ReadBool('Main', 'CheckSpelling', false);
    comboLatexGraphicsPackage.ItemIndex :=
      Ini.ReadInteger('Main', 'LatexGraphicsPackage', 0);

    ReadStrings('IgnoreWords', memoSpellCheckingIgnore.Lines);
  finally Ini.Free end;

  Changed := False;
end;

procedure TfrmHelpGenerator.SaveSettingsToFile(const FileName: string;
  UpdateSettingsFileName, ClearChanged: boolean);
var
  Ini: TIniFile;

  procedure WriteStrings(const Section: string; S: TStrings);
  var
    i: Integer;
  begin
    { It's not really necessary for correctness but it's nice to protect
      user privacy by removing trash data from file (in case previous
      value of S had larger Count). }
    Ini.EraseSection(Section);

    Ini.WriteInteger(Section, 'Count', S.Count);
    for i := 0 to S.Count - 1 do
      Ini.WriteString(Section, 'Item_' + IntToStr(i), S[i]);
  end;

  { If CheckStoreRelativePaths.Checked and FileNameToCorrect <> '',
    this returns relative filename (with respect to
    directory where FileName is stored), else returns just
    FileNameToCorrect. }
  function CorrectFileName(const FileNameToCorrect: string): string;
  begin
    if CheckStoreRelativePaths.Checked and (FileNameToCorrect <> '') then
      Result := ExtractRelativepath(FileName, FileNameToCorrect) else
      Result := FileNameToCorrect;
  end;

  { Modified version of WriteStrings that always write
    CorrectFileName(S[I]) instead of just S[I]. }
  procedure WriteFileNames(const Section: string; S: TStrings);
  var
    i: Integer;
  begin
    { It's not really necessary for correctness but it's nice to protect
      user privacy by removing trash data from file (in case previous
      value of S had larger Count). }
    Ini.EraseSection(Section);

    Ini.WriteInteger(Section, 'Count', S.Count);
    for i := 0 to S.Count - 1 do
      Ini.WriteString(Section, 'Item_' + IntToStr(i),
        CorrectFileName(S[i]));
  end;

var
  i: Integer;
begin
  Ini := TIniFile.Create(FileName);
  try
    Ini.WriteBool('Main', 'StoreRelativePaths', CheckStoreRelativePaths.Checked);

    Ini.WriteString('Main', 'Language',
      LanguageInformation(TLanguageID(comboLanguages.ItemIndex))^.Syntax);
    Ini.WriteString('Main', 'OutputDir', CorrectFileName(EditOutputDirectory.Directory));
    Ini.WriteInteger('Main', 'GenerateFormat', comboGenerateFormat.ItemIndex);
    Ini.WriteInteger('Main', 'ImplementationComments', ComboImplementationComments.ItemIndex);
    Ini.WriteInteger('Main', 'InheritedMembers', ComboInheritedMembers.ItemIndex);
    Ini.WriteString('Main', 'ProjectName', edProjectName.Text);
    Ini.WriteInteger('Main', 'Verbosity', seVerbosity.Value);

    for i := Ord(Low(TVisibility)) to Ord(High(TVisibility)) do
      Ini.WriteBool('Main', 'ClassMembers_' + IntToStr(i),
        CheckListVisibleMembers.Checked[i]);

    Ini.WriteInteger('Main', 'ImplicitVisibility',
      RadioImplicitVisibility.ItemIndex);

    for i := Ord(Low(TSortSetting)) to Ord(High(TSortSetting)) do
    begin
      Ini.WriteBool('Main', 'Sorting_' + IntToStr(i),
        clbSorting.Checked[i]);
    end;

    WriteStrings('Defines', memoDefines.Lines);
    WriteStrings('Header', memoHeader.Lines);
    WriteStrings('Footer', memoFooter.Lines);
    WriteStrings('AutoLinkExclude', MemoAutoLinkExclude.Lines);
    WriteFileNames('IncludeDirectories', memoIncludeDirectories.Lines);
    WriteFileNames('Files', memoFiles.Lines);

    Ini.WriteString('Main', 'CssFileName', CorrectFileName(
      EditCssFileName.FileName));
    Ini.WriteBool('Main', 'Bootstrap', CheckBoxBootstrap.Checked);
    Ini.WriteString('Main', 'IntroductionFileName', CorrectFileName(
      EditIntroductionFileName.FileName));
    Ini.WriteString('Main', 'ConclusionFileName', CorrectFileName(
      EditConclusionFileName.FileName));
    Ini.WriteString('Main', 'HtmlHead', CorrectFileName(
      EditHtmlHead.FileName));
    Ini.WriteString('Main', 'HtmlBodyBegin', CorrectFileName(
      EditHtmlBodyBegin.FileName));
    Ini.WriteString('Main', 'HtmlBodyEnd', CorrectFileName(
      EditHtmlBodyEnd.FileName));
    Ini.WriteString('Main', 'ExternalDescriptions', CorrectFileName(
      EditExternalDescriptions.FileName));
    WriteFileNames('AdditionalFiles', MemoAdditionalFiles.Lines);

    Ini.WriteBool('Main', 'WriteUsesList', CheckWriteUsesList.Checked);
    Ini.WriteBool('Main', 'AutoAbstract', CheckAutoAbstract.Checked);
    Ini.WriteBool('Main', 'AutoLink', CheckAutoLink.Checked);
    Ini.WriteBool('Main', 'HandleMacros', CheckHandleMacros.Checked);
    Ini.WriteBool('Main', 'UseTipueSearch', CheckUseTipueSearch.Checked);
    Ini.WriteBool('Main', 'Markdown', CheckMarkdown.Checked);
    Ini.WriteBool('Main', 'AutoBackComments', CheckAutoBackComments.Checked);
    Ini.WriteBool('Main', 'OpenHTML', CheckOpenHTML.Checked);

    Ini.WriteInteger('Main', 'LineBreakQuality', rgLineBreakQuality.ItemIndex);
    WriteStrings('HyphenatedWords', memoHyphenatedWords.Lines);
    Ini.WriteInteger('Main', 'SpecialMarkerTreatment', rgCommentMarkers.ItemIndex);
    WriteStrings('SpecialMarkers', memoCommentMarkers.Lines);
    Ini.WriteString('Main', 'Title', edTitle.Text);

    Ini.WriteBool('Main', 'VizGraphClasses', cbVizGraphClasses.Checked);
    Ini.WriteBool('Main', 'VizGraphUses', cbVizGraphUses.Checked);

    Ini.WriteBool('Main', 'CheckSpelling', cbCheckSpelling.Checked);
    Ini.WriteInteger('Main', 'LatexGraphicsPackage', comboLatexGraphicsPackage.ItemIndex);
    WriteStrings('IgnoreWords', memoSpellCheckingIgnore.Lines);

    Ini.UpdateFile;
  finally Ini.Free end;

  if UpdateSettingsFileName then
    SettingsFileName := FileName;

  if ClearChanged then
    Changed := false;
end;

procedure TfrmHelpGenerator.MenuSaveAsClick(Sender: TObject);
begin
  if SaveDialog1.Execute then
    SaveSettingsToFile(SaveDialog1.FileName, true, true);
end;

procedure TfrmHelpGenerator.Exit1Click(Sender: TObject);
begin
  Close;
end;

function TfrmHelpGenerator.SaveChanges: boolean;
var
  MessageResult: integer;
begin
  Result := true;
  if Changed then
  begin
    MessageResult := MessageDlg(
      Format('Project "%s" was modified. ' +
        'Do you want to save it now ?', [SettingsFileNameNice]),
      Dialogs.mtInformation, [mbYes, mbNo, mbCancel], 0);
    case MessageResult of
      mrYes:
        begin
          MenuSaveClick(MenuSave);
        end;
      mrNo:
        begin
          // do nothing.
        end;
    else
      Result := false;
    end;
  end;
end;

procedure TfrmHelpGenerator.FormClose(Sender: TObject;
  var AnAction: TCloseAction);
begin
  if not SaveChanges then
    AnAction := caNone;
end;

procedure TfrmHelpGenerator.MenuNewClick(Sender: TObject);
begin
  if not SaveChanges then Exit;

  SetDefaults;

  SettingsFileName := '';

  Changed := False;
end;

procedure TfrmHelpGenerator.comboGenerateFormatChange(Sender: TObject);

  { With WinAPI interface, this is useful to give user indication of
    Edit.Enabled state. Other WinAPI programs also do this.
    With other widgetsets, like GTK, this is not needed, Lazarus + GTK
    already handle such things (e.g. edit boxes have automatically
    slightly dimmed background when they are disabled). }
  procedure SetColorFromEnabled(Edit: TFileNameEdit); overload;
  begin
    {$ifdef WIN32}
    if Edit.Enabled then
      Edit.Color := clWindow else
      Edit.Color := clBtnFace;
    {$endif}
  end;

  procedure SetColorFromEnabled(Edit: TEdit); overload;
  begin
    {$ifdef WIN32}
    if Edit.Enabled then
      Edit.Color := clWindow else
      Edit.Color := clBtnFace;
    {$endif}
  end;

begin
  CheckUseTipueSearch.Enabled := comboGenerateFormat.ItemIndex = 0;

  edProjectName.Enabled := comboGenerateFormat.ItemIndex <> 0;
  SetColorFromEnabled(edProjectName);

  EditCssFileName.Enabled := comboGenerateFormat.ItemIndex in [0,1];
  SetColorFromEnabled(EditCssFileName);
  CheckAutoLink.Enabled := comboGenerateFormat.ItemIndex in [0,1];

  comboLatexGraphicsPackage.Enabled := comboGenerateFormat.ItemIndex in [2,3];
  FillNavigationListBox;
  Changed := true;
end;

procedure TfrmHelpGenerator.lbNavigationClick(Sender: TObject);
var
  Page: TPage;
  Index: Integer;
begin
  if lbNavigation.ItemIndex = -1 then Exit;

  Page := lbNavigation.Items.Objects[lbNavigation.ItemIndex] as TPage;

  { We want to set NotebookMain.ActivePageComponent := Page.
    There doesn't seem to exist a comfortable way to do this (Page.PageIndex
    was removed, ActivePageComponent is not settable, our
    lbNavigation.ItemIndex is not necessarily what we need...),
    so just search NotebookMain.Page[] for the right page. }
  for Index := 0 to NotebookMain.PageCount -1 do
    if Page = NotebookMain.Page[Index] then
    begin
      NotebookMain.PageIndex := Index;
      Break;
    end;
end;

procedure TfrmHelpGenerator.MenuContextHelpClick(Sender: TObject);
var
  HelpControl: TControl;
begin
  HelpControl := nil;
  if (Sender is TMenuItem) or (Sender = lbNavigation) then
  begin
    HelpControl := NotebookMain.ActivePageComponent;
    GetHelpControl(HelpControl, HelpControl);
  end
  else if (Sender is TControl) then
  begin
    GetHelpControl(TControl(Sender), HelpControl);
  end;

  if HelpControl <> nil then
  begin
    Assert(HelpControl.HelpType = htKeyword);
    OpenURL(WWWHelpServer + HelpControl.HelpKeyword);
  end;
end;

procedure TfrmHelpGenerator.MenuGenerateRunClick(Sender: TObject);
begin
  { Switch to "Generate" page }
  lbNavigation.ItemIndex := lbNavigation.Items.IndexOfObject(pageGenerate);
  lbNavigationClick(nil);

  ButtonGenerateDocsClick(nil);
end;

procedure TfrmHelpGenerator.MenuPreferencesClick(Sender: TObject);
begin
  TPreferences.Execute;
end;

procedure TfrmHelpGenerator.MenuSaveClick(Sender: TObject);
begin
  if SettingsFileName = '' then
    MenuSaveAsClick(nil) else
    SaveSettingsToFile(SettingsFileName, true, true);
end;

procedure TfrmHelpGenerator.rgCommentMarkersClick(Sender: TObject);
begin
  Changed := True;
  memoCommentMarkers.Enabled := (rgCommentMarkers.ItemIndex >= 1);
  if memoCommentMarkers.Enabled then begin
    memoCommentMarkers.Color := clWindow;
  end
  else begin
    memoCommentMarkers.Color := clBtnFace;
  end;
end;

procedure TfrmHelpGenerator.tvUnitsClick(Sender: TObject);
var
  Item: TBaseItem;
begin
  seComment.Lines.Clear;
  seComment.Hint := '';
  if (tvUnits.Selected <> nil) and (tvUnits.Selected.Data <> nil) then
  begin
    if TObject(tvUnits.Selected.Data) is TBaseItem then
    begin
      Item := TBaseItem(tvUnits.Selected.Data);
      seComment.Lines.Text := Item.RawDescription;
      seComment.Hint := Format(
        'Comment in stream "%s", on position %d - %d',
        [ Item.RawDescriptionInfo^.StreamName,
          Item.RawDescriptionInfo^.BeginPosition,
          Item.RawDescriptionInfo^.EndPosition ]);
    end;
  end;
end;

function TfrmHelpGenerator.GetCheckListVisibleMembersValue: TVisibilities;
var
  V: TVisibility;
begin
  Result := [];
  for V := Low(V) to High(V) do
  begin
    if CheckListVisibleMembers.Checked[Ord(V)] then
      Include(Result, V);
  end;
end;

procedure TfrmHelpGenerator.SetCheckListVisibleMembersValue(
  const AValue: TVisibilities);
var
  V: TVisibility;
begin
  for V := Low(V) to High(V) do
    CheckListVisibleMembers.Checked[Ord(V)] := V in AValue;
end;

procedure TfrmHelpGenerator.CreateWnd;
begin
  InsideCreateWnd := true;
  try
    inherited;
  finally
    InsideCreateWnd := false;
  end;
end;

function TfrmHelpGenerator.SettingsFileNameNice: string;
begin
  if SettingsFileName = '' then
    Result := 'Unsaved PasDoc settings' else
    Result := ExtractFileName(SettingsFileName);
end;

initialization
  {$I frmhelpgeneratorunit.lrs}
end.
