{
 ***************************************************************************
 *                                                                         *
 *   This source 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.                                   *
 *                                                                         *
 *   This code 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.                              *
 *                                                                         *
 *   A copy of the GNU General Public License is available on the World    *
 *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
 *   obtain it by writing to the Free Software Foundation,                 *
 *   Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA.   *
 *                                                                         *
 ***************************************************************************

  Author: Michael Van Canneyt

  Page editor. Edits 1 file. A page editor exists of a package editor
  and an element editor, joined on a panel with a splitter between them.
  
  It is built modular, in 3 pieces.
  - Package editor.  Edits structure of the documentation file.
  - Element editor.  Edits 1 element node in the documentation file.
  - EditorPage.      Combines the above two to one visual element.
  
  The package editor is mainly concerned with structure of the file in
  terms of packages/topic/module/element. All things which are concerned
  with structure are shunted to the package editor. In turn, the package
  editor has a series of event handlers in order to react on changes.
  
  The element editor edits 1 element. It can handle the following tags:
  short/descr/errors/seealso/example

  There is no direct interaction between the package editor and the element
  editor. This allows changing either of them without needing to change the other.
  The page editor handles all communication between the two.
  
  This allows one to implement several editors. One could also implement the
  editor page so it is split horizontal, whatever.

}
unit pgEditor;

{$mode objfpc}
{$h+}

interface

uses SysUtils, Classes, dom, xmlread, xmlwrite, Forms, Controls, ExtCtrls,
     ComCtrls, Dialogs, freditor, frpeditor, fpdeutil, LazFileUtils, LazUTF8;

type
  TEditorPageNew = class(TFrame)

  end;

Type

  { TEditorPage }
  TEditorPage = Class(TTabSheet)
  Private
    FDocument : TXMLDocument;
    FPackages : TPackageEditor;
    FElement : TElementEditor;
    FSplitter : TSplitter;
    FFileNAme : String;
    Procedure ElementSelected(Node : TDomElement) ;
    function FindElement(const ElementName: String; Out PE,ME : TDomElement): TDomElement;
    function FindElementNode(AParent: TDomElement; const ANodeName: String;
      const AElementName: string): TDomElement;
    Procedure TopicSelected(Node : TDomElement) ;
    Procedure ModuleSelected(Node : TDomElement) ;
    Procedure PackageSelected(Node : TDomElement) ;
//    Procedure SelectionChanged(Sender : TObject; Node : TDomElement) ;
//    Procedure ElementDeSelected(Sender : TObject; Node : TDomElement) ;
    Function GetCurrentSelection : String;
    Function GetCurrentPackage : TDomElement;
    Function GetCurrentModule : TDomElement;
    Function GetCurrentTopic : TDomElement;
    Function GetCurrentElement : TDomElement;
    Procedure SetCurrentModule(Value : TDomElement);
    Procedure SetCurrentTopic(Value : TDomElement);
    Procedure SetCurrentPackage(Value : TDomElement);
    Procedure SetCurrentElement(Value : TDomElement);
    Procedure SetModified(Value : Boolean);
    Function  GetModified : Boolean;
    Function MakeBackup(FN : String) : Boolean;
    Procedure DisplayDocument(Const AStartNode : String = '');
    Procedure ElementChanged(Sender: TObject);
  protected
    procedure SetParent(NewParent: TWinControl); override;
  Public
    constructor Create(AOwner : TComponent); override;
    Function  FirstPackage : TDomElement;
    Function  FirstModule(APackage : TDomElement) : TDomElement;
    Procedure LoadFromFile(FN : String; Const AStartNode : String = '');
    Procedure LoadFromStream(S : TStream);
    Procedure SaveToFile(FN : String);
    Procedure SetFileName(FN : String);
    Procedure InsertTag(TagType : TTagType);
    Procedure InsertLink(LinkTarget,LinkText : String);
    Procedure InsertTable(Cols,Rows : Integer; UseHeader : Boolean);
    Procedure InsertShortPrintLink(pLinkTarget: string);
    Procedure InsertItemizeList(ItemsCount: Integer);
    Procedure InsertEnumerateList(ItemsCount: Integer);
    Procedure NewPackage(APackageName : String);
    Procedure NewModule(AModuleName : String);
    Procedure NewTopic(ATopicName : String);
    procedure NewElement(AElementName: String);
    Procedure GetElementList(List : TStrings);
    function  GetInitialDir: String;
    Procedure ClearDocument;
    procedure UpdateTree;
    Function CanInsertTag(TagType : TTagType) : Boolean;
    Property FileName : String Read FFileName;
    Property CurrentSelection : String Read GetCurrentSelection;
    Property CurrentPackage : TDomElement Read GetCurrentPackage Write SetCurrentPackage;
    Property CurrentModule : TDomElement Read GetCurrentModule Write SetCurrentModule;
    Property CurrentTopic : TDomElement Read GetCurrentTopic Write SetCurrentTopic;
    Property CurrentElement : TDomElement Read GetCurrentElement Write SetCurrentElement;
    Property Modified : Boolean Read GetModified Write SetModified;
  end;


implementation

uses lazdeopts,lazdemsg;

{$R *.lfm}

{ ---------------------------------------------------------------------
  TPageEditor
  ---------------------------------------------------------------------}

constructor TEditorPage.Create(AOwner : TComponent);
begin
  inherited;
  FPackages:=TPackageEditor.Create(Self);
  FPackages.Parent:=Self;
  FPackages.Align:=alLeft;
  FPackages.OnSelectElement:=@ElementSelected;
  FPackages.OnSelectModule:=@ModuleSelected;
  FPackages.OnSelectPackage:=@PackageSelected;
  FPackages.OnSelectTopic:=@TopicSelected;

  FSplitter:=TSplitter.Create(Self);
  FSPlitter.Parent:=Self;
  FSplitter.Align:=alLeft;
  FSplitter.Left:=1;
  FSplitter.Width:=5;

  FElement:=TElementEditor.Create(Self);
  FElement.Parent:=Self;
  FElement.Align:=AlClient;
  FElement.OnGetElementList:=@GetELementList;
  FElement.OnGetInitialDir:=@GetInitialDir;
  FElement.OnChange:=@ElementChanged;
end;



Procedure TEditorPage.ClearDocument;

begin
  if (FDocument<>nil) then
    begin
    FDocument.Free;
    FDocument:=Nil;
    end;
end;

procedure TEditorPage.UpdateTree;
begin
  FPackages.UpdateTree;
end;

function TEditorPage.CanInsertTag(TagType: TTagType): Boolean;
begin
  Result:=FElement.CanInsertTag(TagType);
end;

Procedure TEditorPage.LoadFromFile(FN : String; Const AStartNode : String = '');

Var
  F : TFileStream;

begin
  ClearDocument;
  F:=TFileStream.Create(UTF8ToSys(FN),fmOpenRead);
  Try
    SetFileName(FN);
    ReadXMLFile(FDocument,F);
    DisplayDocument(AStartNode);
    FPackages.ExpandTree;
  finally
    F.Free;
  end;
end;

Procedure TEditorPage.LoadFromStream(S : TStream);

begin
  ClearDocument;
  ReadXMLFile(FDocument,S);
  SetFileName(SNewDocument);
  DisplayDocument;
end;

Procedure TEditorPage.SetFileName(FN : String);

begin
  FFileName:=FN;
  Caption:=ChangeFileExt(ExtractFileName(FN),'');
end;

Function TEditorPage.MakeBackup(FN : String) : Boolean;

Var
  BN : String;

begin
  Result:=Not CreateBackup;
  If not Result then
    begin
    BN:=ChangeFileExt(FN,BackupExtension);
    Result:=RenameFileUTF8(FN,BN);
    end;
end;

Procedure TEditorPage.SaveToFile(FN : String);
begin
  MakeBackup(FN);
  if FN <> FFileName then SetFileName(FN);
  If FElement.Modified then FElement.Save;
  WriteXMLFile(FDocument, FN);
  Modified :=False;
  FElement.Refresh;
end;

function TEditorPage.FindElementNode(AParent : TDomElement; Const ANodeName : String; Const AElementName : string) : TDomElement;

Var
  N : TDomNode;

begin
  Result:=Nil;
  N:=AParent.FirstChild;
  While (Result=Nil) and (N<>Nil) do
    begin
    if (N.NodeType=ELEMENT_NODE) and (N.NodeName=ANodeName) then
       If (AElementName='') or (CompareText((N as TDomElement).AttribStrings['name'],AElementName)=0) then
         Result:=N as TDomElement;
    N:=N.NextSibling;
    end;
end;


function TEditorPage.FindElement(Const ElementName : String; Out PE,ME : TDomElement) : TDomElement;

  Function GetNextPart(Var N : String) : String;

  Var
    p : integer;

  begin
    P:=Pos('.',N);
    if P=0 then
      P:=Length(N)+1;
    Result:=Copy(N,1,P-1);
    Delete(N,1,P);
  end;

Var
  PN,N : String;

begin
  Result:=Nil;
  PE:=Nil;
  ME:=Nil;
  N:=ElementName;
  // Extract package name.
  PN:=GetNextPart(N);
  // Search package node
  PE:=FindElementNode(FDocument.DocumentElement,'package',PN);
  // if not found, assume first part is modulename in first package.
  if (PE=Nil) then
    begin
    PE:=FindElementNode(FDocument.DocumentElement,'package','');
    N:=ElementName;
    end;
  if (PE=Nil) then // No package node !
    exit;
  // Extract Module name
  PN:=GetNextPart(N);
  ME:=FindElementNode(PE,'module',PN);
  // if not found, assume elementname is element in first module.
  if (ME=Nil) then
    begin
    ME:=FindElementNode(PE,'module','');
    N:=ElementName;
    end;
  if (ME=Nil) then // No module node !
    exit;
  Result:=FindElementNode(ME,'element',N);
end;

Procedure TEditorPage.DisplayDocument(Const AStartNode : String = '');

Var
  PE,ME,EE : TDomElement;

begin
  EE:=Nil;
  if (AStartNode <> '') then
    begin
    EE:=FindElement(AStartNode,PE,ME);
    If (EE=Nil) then
      ShowMessage(Format(SStartNodeNotFound,[AStartNode]));
    end;
  FPackages.DescriptionNode:=FDocument.DocumentElement;
  if (EE<>Nil) then
    begin
    FPackages.CurrentPackage:=PE;
    FPackages.CurrentModule:=ME;
    FPackages.CurrentElement:=EE;
    end;
end;

procedure TEditorPage.ElementChanged(Sender: TObject);
begin
  if Sender=nil then ;
  TPackageEditor(FPackages).UpdateSelectedNodeStatus;
end;

procedure TEditorPage.SetParent(NewParent: TWinControl);
begin
  inherited SetParent(NewParent);
  if Assigned(NewParent) then
    FSplitter.Width:=5;
end;



Procedure TEditorPage.ElementSelected(Node : TDomElement) ;

Var
  OldNode : TDomElement;

begin
  OldNode:=FElement.Element;
  If OldNode<>Node then
    FElement.Element:=Node;
end;

Procedure TEditorPage.PackageSelected(Node : TDomElement) ;

begin
  ElementSelected(Node);
end;

Procedure TEditorPage.ModuleSelected(Node : TDomElement) ;

begin
  ElementSelected(Node);
end;

Procedure TEditorPage.TopicSelected(Node : TDomElement) ;

begin
  ElementSelected(Node);
end;


Procedure TEditorPage.InsertTag(TagType : TTagType);

begin
  FElement.InsertTag(TagType)
end;

Procedure TEditorPage.InsertLink(LinkTarget,LinkText : String);

begin
  FElement.InsertLink(LinkTarget,LinkText);
end;


Procedure TEditorPage.InsertTable(Cols,Rows : Integer; UseHeader : Boolean);

begin
  Felement.InsertTable(Cols,Rows,UseHeader);
end;


procedure TEditorPage.InsertShortPrintLink(pLinkTarget: string);
begin
  FElement.InsertPrintShortLink(pLinkTarget);
end;

procedure TEditorPage.InsertItemizeList(ItemsCount: Integer);
begin
  Felement.InsertItemizeList(ItemsCount);
end;

procedure TEditorPage.InsertEnumerateList(ItemsCount: Integer);
begin
  Felement.InsertEnumerateList(ItemsCount);
end;


Function TEditorPage.GetCurrentSelection : String;

begin
  Result:=FElement.CurrentSelection;
end;

Procedure TEditorPage.NewPackage(APackageName : String);

Var
  P : TDomElement;

begin
  P:=FDocument.CreateElement('package');
  P['name']:=APAckageName;
  FDocument.DocumentElement.AppendChild(P);
  FPackages.Refresh;
  FPackages.Modified:=True;
  CurrentPackage:=P;
end;

Function TEditorPage.FirstPackage : TDomElement;

Var
  N : TDomNode;

begin
  N:=FDocument.DocumentElement.FirstChild;
  While (N<>Nil) and Not IsPackageNode(N) do
    N:=N.NextSibling;
  Result:=TDomElement(N);
end;

Function TEditorPage.FirstModule(APackage : TDomElement) : TDomElement;

Var 
  N : TDomNode;

begin
  N:=APAckage.FirstChild;
  While (N<>Nil) and Not IsModuleNode(N) do
      N:=N.NextSibling;
  Result:=TDomElement(N);
end;

Procedure TEditorPage.NewModule(AModuleName : String);

Var 
  M,P : TDomElement;
  
begin
  If CurrentPackage<>Nil then
    P:=CurrentPackage
  else
    P:=FirstPackage;
  If (P=Nil) then  
    Raise Exception.CreateFmt(SErrNoPackageForModule,[AModuleName]);
  M:=FDocument.CreateElement('module');
  M['name']:=AModuleName;
  P.AppendChild(M);
  FPackages.Refresh;
  FPackages.Modified:=True;
  CurrentModule:=M;
end;

Procedure TEditorPage.NewTopic(ATopicName : String);

Var 
  T,M,P : TDomElement;
  
begin
  { 
    If currently a topic is selected, make a subtopic, or a sibling topic.
    If no topic is selected, then make a topic under the current module or 
    package. A menu to move topics up/down is needed...
  }
  if (CurrentTopic<>Nil) then
    begin
    M:=CurrentTopic.ParentNode as TDomElement;
    If (M.NodeName='module') or (M.NodeName='topic') then
      P:=M
    else
      P:=CurrentTopic;
    end
  else if (CurrentModule<>Nil) then
    P:=CurrentModule
  else if (CurrentPackage<>Nil) then
    P:=CurrentPackage
  else
    P:=FirstPackage;
  If (P=Nil) then  
    Raise Exception.CreateFmt(SErrNoNodeForTopic,[ATopicName]);
  T:=FDocument.CreateElement('topic');
  T['name']:=ATopicName;
  P.AppendChild(T);
  FPackages.Refresh;
  FPackages.Modified:=True;
  CurrentTopic:=T;
end;

Procedure TEditorPage.NewElement(AElementName : String);

Var 
  P,E,M : TDomElement;

begin
  If CurrentModule<>Nil then
    M:=CurrentModule
  else
    begin
    P:=FirstPackage;
    If P<>Nil then
      M:=FirstModule(P)
    else
      M:=Nil;
    If M<>Nil then
      CurrentModule:=M;  
    end;
  If (M=Nil) then  
    Raise Exception.CreateFmt(SErrNoModuleForElement,[AElementName]);
  E:=FDocument.CreateElement('element');
  E['name']:=AElementName;
  M.AppendChild(E);
  FPackages.AddElement(E);
end;

Function TEditorPage.GetCurrentPackage : TDomElement;

begin
  Result:=FPackages.CurrentPackage;
end;


Function TEditorPage.GetCurrentModule : TDomElement;

begin
  Result:=FPackages.CurrentModule;
end;


Function TEditorPage.GetCurrentTopic : TDomElement;

begin
  Result:=FPackages.CurrentTopic;
end;


Function TEditorPage.GetCurrentElement : TDomElement;
begin
  Result:=FElement.Element;
end;

Procedure TEditorPage.SetCurrentElement(Value : TDomElement);

begin
  FPackages.CurrentElement:=Value;
end;


Procedure TEditorPage.SetCurrentModule(Value : TDomElement);

begin
  FPackages.CurrentModule:=Value;
end;


Procedure TEditorPage.SetCurrentTopic(Value : TDomElement);

begin
  FPackages.CurrentTopic:=Value;
end;


Procedure TEditorPage.SetCurrentPackage(Value : TDomElement);

begin
  FPackages.CurrentPackage:=Value;
end;

Procedure TEditorPage.SetModified(Value : Boolean);

begin
  If Not Value then
    begin
    FPackages.Modified:=False;
    FElement.Modified:=False;
    FElement.SavedNode:=False;
    end;
end;

Function TEditorPage.GetModified : Boolean;

begin
  Result:=FPackages.Modified or
          FElement.Modified or
          FElement.SavedNode;
end;

Procedure TEditorPage.GetElementList(List : TStrings);

Var
  N : TDOmNode;

begin
 With List do
   begin
   Clear;
   If Assigned(CurrentModule) then
     begin
     N:=Currentmodule.FirstChild;
     While (N<>Nil) do
       begin
       If (N is TDomElement) and (N.NodeName='element') then
         Add(TDomElement(N)['name']);
       N:=N.NextSibling;  
       end;
     end;  
   end;
end;

function TEditorPage.GetInitialDir: String;
begin
  result := '';
  if FileExistsUTF8(FFileName) then
    Result := ExtractFilePath(FFileName);
end;

end.
