{  $Id: installpkgsetdlg.pas 36692 2012-04-09 19:33:23Z vincents $  }
{
 /***************************************************************************
                         installpkgsetdlg.pas
                         --------------------


 ***************************************************************************/

 ***************************************************************************
 *                                                                         *
 *   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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.        *
 *                                                                         *
 ***************************************************************************

  Author: Mattias Gaertner

  Abstract:
    Dialog to edit the package set installed in the IDE.
}
unit InstallPkgSetDlg;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, contnrs, LCLProc, Forms, Controls, Graphics, Dialogs,
  KeywordFuncLists, StdCtrls, Buttons, FileUtil, ExtCtrls, ComCtrls, EditBtn,
  LCLType, AVL_Tree, Laz2_XMLCfg, TreeFilterEdit, PackageIntf, IDEImagesIntf,
  IDEHelpIntf, IDEDialogs, LazarusIDEStrConsts, EnvironmentOpts, InputHistory,
  LazConf, IDEProcs, PackageDefs, PackageSystem, PackageLinks,
  IDEContextHelpEdit;

type
  TOnCheckInstallPackageList =
    procedure(PkgIDs: TObjectList; RemoveConflicts: boolean; out Ok: boolean) of object;

  { TInstallPkgSetDialog }

  TInstallPkgSetDialog = class(TForm)
    AddToInstallButton: TBitBtn;
    AvailableTreeView: TTreeView;
    AvailablePkgGroupBox: TGroupBox;
    HelpButton: TBitBtn;
    CancelButton: TBitBtn;
    ExportButton: TButton;
    BtnPanel: TPanel;
    InstallTreeView: TTreeView;
    lblMiddle: TLabel;
    AvailableFilterEdit: TTreeFilterEdit;
    NoteLabel: TLabel;
    PkgInfoMemo: TMemo;
    PkgInfoGroupBox: TGroupBox;
    ImportButton: TButton;
    SaveAndExitButton: TBitBtn;
    InstallPkgGroupBox: TGroupBox;
    SaveAndRebuildButton: TBitBtn;
    UninstallButton: TBitBtn;
    procedure AddToInstallButtonClick(Sender: TObject);
    procedure AvailableTreeViewDblClick(Sender: TObject);
    procedure AvailableTreeViewKeyPress(Sender: TObject; var Key: char);
    procedure AvailableTreeViewSelectionChanged(Sender: TObject);
    procedure ExportButtonClick(Sender: TObject);
    procedure HelpButtonClick(Sender: TObject);
    procedure ImportButtonClick(Sender: TObject);
    procedure InstallButtonClick(Sender: TObject);
    procedure InstallTreeViewDblClick(Sender: TObject);
    procedure InstallPkgSetDialogCreate(Sender: TObject);
    procedure InstallPkgSetDialogDestroy(Sender: TObject);
    procedure InstallPkgSetDialogResize(Sender: TObject);
    procedure InstallTreeViewSelectionChanged(Sender: TObject);
    procedure SaveAndExitButtonClick(Sender: TObject);
    procedure UninstallButtonClick(Sender: TObject);
  private
    FNewInstalledPackages: TObjectList; // list of TLazPackageID (not TLazPackage)
    FOldInstalledPackages: TPkgDependency;
    FOnCheckInstallPackageList: TOnCheckInstallPackageList;
    fAvailablePackages: TAVLTree;// tree of TLazPackageID (all available packages and links)
    FRebuildIDE: boolean;
    FSelectedPkg: TLazPackage;
    ImgIndexPackage: integer;
    ImgIndexInstallPackage: integer;
    ImgIndexInstalledPackage: integer;
    ImgIndexUninstallPackage: integer;
    ImgIndexCirclePackage: integer;
    ImgIndexMissingPackage: integer;
    procedure SetOldInstalledPackages(const AValue: TPkgDependency);
    procedure AssignOldInstalledPackagesToList;
    function PackageInInstallList(PkgName: string): boolean;
    function GetPkgImgIndex(APackage: TLazPackage; InInstallList: boolean): integer;
    function GetAvailablePkgImageIndex(Str: String; Data: TObject; var AIsEnabled: Boolean): Integer;
    procedure UpdateAvailablePackages(Immediately: boolean = false);
    procedure UpdateNewInstalledPackages;
    procedure OnIteratePackages(APackageID: TLazPackageID);
    function DependencyToStr(Dependency: TPkgDependency): string;
    procedure ClearNewInstalledPackages;
    function CheckSelection: boolean;
    procedure UpdateButtonStates;
    procedure UpdatePackageInfo(Tree: TTreeView);
    function NewInstalledPackagesContains(APackageID: TLazPackageID): boolean;
    function IndexOfNewInstalledPackageID(APackageID: TLazPackageID): integer;
    function IndexOfNewInstalledPkgByName(const APackageName: string): integer;
    procedure SavePackageListToFile(const AFilename: string);
    procedure LoadPackageListFromFile(const AFilename: string);
    function ExtractNameFromPkgID(ID: string): string;
    procedure AddToInstall;
    procedure AddToUninstall;
  public
    function GetNewInstalledPackages: TObjectList;
    property OldInstalledPackages: TPkgDependency read FOldInstalledPackages
                                                  write SetOldInstalledPackages;
    property NewInstalledPackages: TObjectList read FNewInstalledPackages;
    property RebuildIDE: boolean read FRebuildIDE write FRebuildIDE;
    property OnCheckInstallPackageList: TOnCheckInstallPackageList
               read FOnCheckInstallPackageList write FOnCheckInstallPackageList;
  end;

function ShowEditInstallPkgsDialog(OldInstalledPackages: TPkgDependency;
  CheckInstallPackageList: TOnCheckInstallPackageList;
  var NewInstalledPackages: TObjectList; // list of TLazPackageID (must be freed)
  var RebuildIDE: boolean): TModalResult;

implementation

{$R *.lfm}

function ShowEditInstallPkgsDialog(OldInstalledPackages: TPkgDependency;
  CheckInstallPackageList: TOnCheckInstallPackageList;
  var NewInstalledPackages: TObjectList; // list of TLazPackageID
  var RebuildIDE: boolean): TModalResult;
var
  InstallPkgSetDialog: TInstallPkgSetDialog;
begin
  NewInstalledPackages:=nil;
  InstallPkgSetDialog:=TInstallPkgSetDialog.Create(nil);
  try
    InstallPkgSetDialog.OldInstalledPackages:=OldInstalledPackages;
//    InstallPkgSetDialog.UpdateAvailablePackages;
    InstallPkgSetDialog.UpdateButtonStates;
    InstallPkgSetDialog.OnCheckInstallPackageList:=CheckInstallPackageList;
    Result:=InstallPkgSetDialog.ShowModal;
    NewInstalledPackages:=InstallPkgSetDialog.GetNewInstalledPackages;
    RebuildIDE:=InstallPkgSetDialog.RebuildIDE;
  finally
    InstallPkgSetDialog.Free;
  end;
end;

{ TInstallPkgSetDialog }

procedure TInstallPkgSetDialog.InstallPkgSetDialogCreate(Sender: TObject);
begin
  InstallTreeView.Images := IDEImages.Images_16;
  AvailableTreeView.Images := IDEImages.Images_16;
  ImgIndexPackage := IDEImages.LoadImage(16, 'item_package');
  ImgIndexInstalledPackage := IDEImages.LoadImage(16, 'pkg_installed');
  ImgIndexInstallPackage := IDEImages.LoadImage(16, 'pkg_package_autoinstall');
  ImgIndexUninstallPackage := IDEImages.LoadImage(16, 'pkg_package_uninstall');
  ImgIndexCirclePackage := IDEImages.LoadImage(16, 'pkg_package_circle');
  ImgIndexMissingPackage := IDEImages.LoadImage(16, 'pkg_conflict');

  Caption:=lisInstallUninstallPackages;
  NoteLabel.Caption:=lisToInstallYouMustCompileAndRestartTheIDE;

  AvailablePkgGroupBox.Caption:=lisDoNotInstall;
  AvailableFilterEdit.OnGetImageIndex:=@GetAvailablePkgImageIndex;

  ExportButton.Caption:=lisExportList;
  ImportButton.Caption:=lisImportList;
  UninstallButton.Caption:=lisUninstallSelection;
  UninstallButton.LoadGlyphFromLazarusResource('arrow_right');
  InstallPkgGroupBox.Caption:=lisPckEditInstall;
  AddToInstallButton.Caption:=lisInstallSelection;
  AddToInstallButton.LoadGlyphFromLazarusResource('arrow_left');
  PkgInfoGroupBox.Caption := lisPackageInfo;
  SaveAndRebuildButton.Caption:=lisSaveAndRebuildIDE;
  SaveAndExitButton.Caption:=lisSaveAndExitDialog;
  HelpButton.Caption:=lisMenuHelp;
  CancelButton.Caption:=lisCancel;

  fAvailablePackages:=TAVLTree.Create(@CompareLazPackageIDNames);
  FNewInstalledPackages:=TObjectList.Create(true);
  ActiveControl:=AvailableFilterEdit;
  PkgInfoMemo.Clear;
end;

procedure TInstallPkgSetDialog.InstallButtonClick(Sender: TObject);
begin
  if not CheckSelection then exit;
  RebuildIDE:=true;
  ModalResult:=mrOk;
end;

procedure TInstallPkgSetDialog.InstallTreeViewDblClick(Sender: TObject);
begin
  AddToUninstall;
end;

procedure TInstallPkgSetDialog.AvailableTreeViewSelectionChanged(Sender: TObject);
begin
  UpdateButtonStates;
  UpdatePackageInfo(AvailableTreeView);
end;

procedure TInstallPkgSetDialog.ExportButtonClick(Sender: TObject);
var
  SaveDialog: TSaveDialog;
  AFilename: string;
begin
  SaveDialog:=TSaveDialog.Create(nil);
  try
    InputHistories.ApplyFileDialogSettings(SaveDialog);
    SaveDialog.InitialDir:=GetPrimaryConfigPath;
    SaveDialog.Title:=lisExportPackageListXml;
    SaveDialog.Options:=SaveDialog.Options+[ofPathMustExist];
    if SaveDialog.Execute then begin
      AFilename:=CleanAndExpandFilename(SaveDialog.Filename);
      if ExtractFileExt(AFilename)='' then
        AFilename:=AFilename+'.xml';
      SavePackageListToFile(AFilename);
    end;
    InputHistories.StoreFileDialogSettings(SaveDialog);
  finally
    SaveDialog.Free;
  end;
end;

procedure TInstallPkgSetDialog.HelpButtonClick(Sender: TObject);
begin
  LazarusHelp.ShowHelpForIDEControl(Self);
end;

procedure TInstallPkgSetDialog.ImportButtonClick(Sender: TObject);
var
  OpenDialog: TOpenDialog;
  AFilename: string;
begin
  OpenDialog:=TOpenDialog.Create(nil);
  try
    InputHistories.ApplyFileDialogSettings(OpenDialog);
    OpenDialog.InitialDir:=GetPrimaryConfigPath;
    OpenDialog.Title:=lisImportPackageListXml;
    OpenDialog.Options:=OpenDialog.Options+[ofPathMustExist,ofFileMustExist];
    if OpenDialog.Execute then begin
      AFilename:=CleanAndExpandFilename(OpenDialog.Filename);
      LoadPackageListFromFile(AFilename);
    end;
    InputHistories.StoreFileDialogSettings(OpenDialog);
  finally
    OpenDialog.Free;
  end;
end;

procedure TInstallPkgSetDialog.AddToInstallButtonClick(Sender: TObject);
begin
  AddToInstall;
end;

procedure TInstallPkgSetDialog.AvailableTreeViewDblClick(Sender: TObject);
begin
  AddToInstall;
end;

procedure TInstallPkgSetDialog.AvailableTreeViewKeyPress(Sender: TObject; var Key: char);
begin
  if Key = char(VK_RETURN) then
    AddToInstall;
end;

procedure TInstallPkgSetDialog.InstallPkgSetDialogDestroy(Sender: TObject);
begin
  ClearNewInstalledPackages;
  FNewInstalledPackages.Free;
  fAvailablePackages.Free;
end;

procedure TInstallPkgSetDialog.InstallPkgSetDialogResize(Sender: TObject);
var
  w: Integer;
begin
  w:=ClientWidth div 2-InstallPkgGroupBox.BorderSpacing.Left*3;
  if w<1 then w:=1;
  InstallPkgGroupBox.Width:=w;
end;

procedure TInstallPkgSetDialog.InstallTreeViewSelectionChanged(Sender: TObject);
begin
  UpdateButtonStates;
  UpdatePackageInfo(InstallTreeView);
end;

procedure TInstallPkgSetDialog.SaveAndExitButtonClick(Sender: TObject);
begin
  if not CheckSelection then exit;
  RebuildIDE:=false;
  ModalResult:=mrOk;
end;

procedure TInstallPkgSetDialog.UninstallButtonClick(Sender: TObject);
begin
  AddToUninstall;
end;

procedure TInstallPkgSetDialog.SetOldInstalledPackages(const AValue: TPkgDependency);
begin
  if FOldInstalledPackages=AValue then exit;
  FOldInstalledPackages:=AValue;
  AssignOldInstalledPackagesToList;
end;

procedure TInstallPkgSetDialog.AssignOldInstalledPackagesToList;
var
  Dependency: TPkgDependency;
  Cnt: Integer;
  NewPackageID: TLazPackageID;
begin
  ClearNewInstalledPackages;
  Cnt:=0;
  Dependency:=OldInstalledPackages;
  while Dependency<>nil do begin
    NewPackageID:=TLazPackageID.Create;
    if (Dependency.LoadPackageResult=lprSuccess)
    and (Dependency.RequiredPackage<>nil) then begin
      // packages can be freed while the dialog runs => use packageid instead
      NewPackageID.AssignID(Dependency.RequiredPackage);
    end else begin
      NewPackageID.Name:=Dependency.PackageName;
    end;
    FNewInstalledPackages.Add(NewPackageID);
    Dependency:=Dependency.NextRequiresDependency;
    inc(Cnt);
  end;
  UpdateNewInstalledPackages;
end;

function TInstallPkgSetDialog.PackageInInstallList(PkgName: string): boolean;
var
  i: Integer;
begin
  //for i:=0 to InstallTreeView.Items.TopLvlCount-1 do
    //debugln(['TInstallPkgSetDialog.PackageInInstallList ',i,' ',ExtractNameFromPkgID(InstallTreeView.Items.TopLvlItems[i].Text),' ',PkgName]);
  for i:=0 to InstallTreeView.Items.TopLvlCount-1 do
    if SysUtils.CompareText(
      ExtractNameFromPkgID(InstallTreeView.Items.TopLvlItems[i].Text),PkgName)=0
    then
      exit(true);
  Result:=false;
end;

function TInstallPkgSetDialog.GetPkgImgIndex(APackage: TLazPackage;
  InInstallList: boolean): integer;
begin
  if APackage.Installed<>pitNope then begin
    // is not currently installed
    if InInstallList then begin
      // is installed and will be installed
      Result:=ImgIndexPackage;
    end
    else begin
      // is installed and will be uninstalled
      Result:=ImgIndexUninstallPackage;
    end;
  end else begin
    // is currently installed
    if InInstallList then begin
      // is not installed and will be installed
      Result:=ImgIndexInstallPackage;
    end
    else begin
      // is not installed and will be not be installed
      Result:=ImgIndexPackage;
    end;
  end;
end;

function TInstallPkgSetDialog.GetAvailablePkgImageIndex(Str: String; Data: TObject;
                                               var AIsEnabled: Boolean): Integer;
begin
  Result:=ImgIndexPackage;
  if (Data is TLazPackage) then
    Result:=GetPkgImgIndex(TLazPackage(Data),false);
end;

procedure TInstallPkgSetDialog.UpdateAvailablePackages(Immediately: boolean);
var
  ANode: TAVLTreeNode;
  Pkg: TLazPackageID;
  PkgName: String;
  DuplCheck: TStringList;  // Add pkg names also here to filter out duplicates.
  FilteredBranch: TTreeFilterBranch;
begin
  DuplCheck:=TStringList.Create;
  try
    if fAvailablePackages.Count=0 then
      PackageGraph.IteratePackages(fpfSearchAllExisting,@OnIteratePackages);
    FilteredBranch := AvailableFilterEdit.GetBranch(Nil); // All items are top level.
    ANode:=fAvailablePackages.FindLowest;
    while ANode<>nil do begin
      Pkg:=TLazPackageID(ANode.Data);
      if (not (Pkg is TLazPackage))
      or (TLazPackage(Pkg).PackageType in [lptDesignTime,lptRunAndDesignTime])
      then begin
        if (not PackageInInstallList(Pkg.Name)) then begin
          PkgName:=Pkg.IDAsString;
          if (DuplCheck.IndexOf(PkgName)<0) then begin
            DuplCheck.Add(PkgName);
            FilteredBranch.AddNodeData(PkgName, Pkg);
          end;
        end;
      end;
      ANode:=fAvailablePackages.FindSuccessor(ANode);
    end;
    AvailableFilterEdit.InvalidateFilter;
  finally
    DuplCheck.Free;
  end;
end;

procedure TInstallPkgSetDialog.UpdateNewInstalledPackages;
var
  s: String;
  NewPackageID: TLazPackageID;
  i: Integer;
  sl: TStringList;
  TVNode: TTreeNode;
  APackage: TLazPackage;
  ImgIndex: LongInt;
begin
  sl:=TStringList.Create;
  for i:=0 to FNewInstalledPackages.Count-1 do begin
    NewPackageID:=TLazPackageID(FNewInstalledPackages[i]);
    APackage:=PackageGraph.FindPackageWithName(NewPackageID.Name,nil);
    if APackage<>nil then
      NewPackageID:=APackage;
    s:=NewPackageID.IDAsString;
    sl.AddObject(s,NewPackageID);
  end;
  sl.Sort;
  InstallTreeView.BeginUpdate;
  InstallTreeView.Items.Clear;
  for i:=0 to sl.Count-1 do begin
    TVNode:=InstallTreeView.Items.Add(nil,sl[i]);
    NewPackageID:=TLazPackageID(sl.Objects[i]);
    ImgIndex:=ImgIndexInstallPackage;
    if NewPackageID is TLazPackage then begin
      APackage:=TLazPackage(NewPackageID);
      ImgIndex:=GetPkgImgIndex(APackage,true);
    end;
    TVNode.ImageIndex:=ImgIndex;
    TVNode.SelectedIndex:=ImgIndex;
  end;
  InstallTreeView.EndUpdate;
  sl.Free;
  UpdateAvailablePackages;
end;

procedure TInstallPkgSetDialog.OnIteratePackages(APackageID: TLazPackageID);
begin
  //debugln('TInstallPkgSetDialog.OnIteratePackages ',APackageID.IDAsString);
  if (fAvailablePackages.Find(APackageID)=nil) then
    fAvailablePackages.Add(APackageID);
end;

function TInstallPkgSetDialog.DependencyToStr(Dependency: TPkgDependency): string;
begin
  Result:='';
  if Dependency=nil then exit;
  if (Dependency.LoadPackageResult=lprSuccess)
  and (Dependency.RequiredPackage<>nil) then
    Result:=Dependency.RequiredPackage.IDAsString
  else
    Result:=Dependency.PackageName;
end;

procedure TInstallPkgSetDialog.ClearNewInstalledPackages;
begin
  FNewInstalledPackages.Clear;
end;

function TInstallPkgSetDialog.CheckSelection: boolean;
begin
  OnCheckInstallPackageList(FNewInstalledPackages,true,Result);
end;

procedure TInstallPkgSetDialog.UpdateButtonStates;
var
  Cnt: Integer;
  Dependency: TPkgDependency;
  s: String;
  ListChanged: Boolean;
begin
  UninstallButton.Enabled:=InstallTreeView.Selected<>nil;
  AddToInstallButton.Enabled:=AvailableTreeView.Selected<>nil;
  // check for changes
  ListChanged:=false;
  Cnt:=0;
  Dependency:=OldInstalledPackages;
  while Dependency<>nil do begin
    s:=Dependency.PackageName;
    if not PackageInInstallList(s) then begin
      ListChanged:=true;
      break;
    end;
    Dependency:=Dependency.NextRequiresDependency;
    inc(Cnt);
  end;
  if InstallTreeView.Items.TopLvlCount<>Cnt then
    ListChanged:=true;
  SaveAndExitButton.Enabled:=ListChanged;
  SaveAndRebuildButton.Enabled:=ListChanged;
end;

procedure TInstallPkgSetDialog.UpdatePackageInfo(Tree: TTreeView);
var
  InfoStr: string;

  procedure AddState(const NewState: string);
  begin
    if (InfoStr<>'') and (InfoStr[length(InfoStr)]<>' ') then
      InfoStr:=InfoStr+', ';
    InfoStr:=InfoStr+NewState;
  end;

var
  PkgName: String;
  PkgID: TLazPackageID;
  Author: String;
  Description: String;
  PkgLink: TPackageLink;
  XMLConfig: TXMLConfig;
begin
  if Tree = nil then Exit;
  PkgName := '';
  if Tree.Selected <> nil then
    PkgName := Tree.Selected.Text;

  if PkgName = '' then Exit;
  if Assigned(FSelectedPkg) and (PkgName = FSelectedPkg.IDAsString) then Exit;
    
  PkgInfoMemo.Clear;
  PkgID := TLazPackageID.Create;
  try
    PkgID.StringToID(PkgName);
    FSelectedPkg := PackageGraph.FindPackageWithID(PkgID);

    Author:='';
    Description:='';
    if FSelectedPkg <> nil then begin
      Author:=FSelectedPkg.Author;
      Description:=FSelectedPkg.Description;
    end else begin
      // package not loaded -> read values from .lpk
      PkgLink:=PkgLinks.FindLinkWithPackageID(PkgID);
      if (PkgLink<>nil) and FileExistsCached(PkgLink.GetEffectiveFilename) then begin
        // load the package file
        try
          XMLConfig:=TXMLConfig.Create(PkgLink.GetEffectiveFilename);
          try
            Author:=XMLConfig.GetValue('Package/Author/Value','');
            Description:=XMLConfig.GetValue('Package/Description/Value','');
          finally
            XMLConfig.Free;
          end;
        except
          on E: Exception do begin
            DebugLn('TInstallPkgSetDialog.UpdatePackageInfo ERROR: ',E.Message);
          end;
        end;
      end;
    end;
    if Author<>'' then
      PkgInfoMemo.Lines.Add(lisPckOptsAuthor + ': ' + Author);
    if Description<>'' then
      PkgInfoMemo.Lines.Add(lisPckOptsDescriptionAbstract
                            + ': ' + Description);
    if FSelectedPkg<>nil then
    begin
      PkgInfoMemo.Lines.Add(Format(lisOIPFilename, [FSelectedPkg.Filename]));
      InfoStr:=lisCurrentState;
      if FSelectedPkg.Installed<>pitNope then
        AddState(lisInstalled)
      else
        AddState(lisNotInstalled);
      if FSelectedPkg.AutoCreated then
        AddState(lisPckExplAutoCreated);
      PkgInfoMemo.Lines.Add(InfoStr);
    end;
  finally
    PkgId.Free;
  end;
end;

function TInstallPkgSetDialog.NewInstalledPackagesContains(
  APackageID: TLazPackageID): boolean;
begin
  Result:=IndexOfNewInstalledPackageID(APackageID)>=0;
end;

function TInstallPkgSetDialog.IndexOfNewInstalledPackageID(
  APackageID: TLazPackageID): integer;
begin
  Result:=FNewInstalledPackages.Count-1;
  while (Result>=0)
  and (TLazPackageID(FNewInstalledPackages[Result]).Compare(APackageID)<>0) do
    dec(Result);
end;

function TInstallPkgSetDialog.IndexOfNewInstalledPkgByName(
  const APackageName: string): integer;
begin
  Result:=FNewInstalledPackages.Count-1;
  while (Result>=0)
  and (CompareText(TLazPackageID(FNewInstalledPackages[Result]).Name,
       APackageName)<>0)
  do
    dec(Result);
end;

procedure TInstallPkgSetDialog.SavePackageListToFile(const AFilename: string);
var
  XMLConfig: TXMLConfig;
  i: Integer;
  LazPackageID: TLazPackageID;
begin
  try
    XMLConfig:=TXMLConfig.CreateClean(AFilename);
    try
      XMLConfig.SetDeleteValue('Packages/Count',FNewInstalledPackages.Count,0);
      for i:=0 to FNewInstalledPackages.Count-1 do begin
        LazPackageID:=TLazPackageID(FNewInstalledPackages[i]);
        XMLConfig.SetDeleteValue('Packages/Item'+IntToStr(i)+'/ID',
                                 LazPackageID.IDAsString,'');
      end;
      InvalidateFileStateCache;
      XMLConfig.Flush;
    finally
      XMLConfig.Free;
    end;
  except
    on E: Exception do begin
      MessageDlg(lisCodeToolsDefsWriteError,
        Format(lisErrorWritingPackageListToFile, [#13, AFilename, #13, E.Message
          ]), mtError, [mbCancel], 0);
    end;
  end;
end;

procedure TInstallPkgSetDialog.LoadPackageListFromFile(const AFilename: string);
  
  function PkgNameExists(List: TObjectList; ID: TLazPackageID): boolean;
  var
    i: Integer;
    LazPackageID: TLazPackageID;
  begin
    if List<>nil then
      for i:=0 to List.Count-1 do begin
        LazPackageID:=TLazPackageID(List[i]);
        if CompareText(LazPackageID.Name,ID.Name)=0 then begin
          Result:=true;
          exit;
        end;
      end;
    Result:=false;
  end;
  
var
  XMLConfig: TXMLConfig;
  i: Integer;
  LazPackageID: TLazPackageID;
  NewCount: LongInt;
  NewList: TObjectList;
  ID: String;
begin
  NewList:=nil;
  LazPackageID:=nil;
  try
    XMLConfig:=TXMLConfig.Create(AFilename);
    try
      NewCount:=XMLConfig.GetValue('Packages/Count',0);
      LazPackageID:=TLazPackageID.Create;
      for i:=0 to NewCount-1 do begin
        // get ID
        ID:=XMLConfig.GetValue('Packages/Item'+IntToStr(i)+'/ID','');
        if ID='' then continue;
        // parse ID
        if not LazPackageID.StringToID(ID) then continue;
        // ignore doubles
        if PkgNameExists(NewList,LazPackageID) then continue;
        // add
        if NewList=nil then NewList:=TObjectList.Create(true);
        NewList.Add(LazPackageID);
        LazPackageID:=TLazPackageID.Create;
      end;
      // clean up old list
      ClearNewInstalledPackages;
      FNewInstalledPackages.Free;
      // assign new list
      FNewInstalledPackages:=NewList;
      NewList:=nil;
      UpdateNewInstalledPackages;
      UpdateButtonStates;
    finally
      XMLConfig.Free;
      LazPackageID.Free;
      NewList.Free;
    end;
  except
    on E: Exception do begin
      MessageDlg(lisCodeToolsDefsReadError,
        Format(lisErrorReadingPackageListFromFile, [#13, AFilename, #13,
          E.Message]), mtError, [mbCancel], 0);
    end;
  end;
end;

function TInstallPkgSetDialog.ExtractNameFromPkgID(ID: string): string;
var
  p: Integer;
begin
  p:=1;
  while (p<=length(ID)) and IsIdentChar[ID[p]] do inc(p);
  Result:=copy(ID,1,p-1);
end;

procedure TInstallPkgSetDialog.AddToInstall;
var
  i: Integer;
  NewPackageID: TLazPackageID;
  j: LongInt;
  APackage: TLazPackage;
  Additions: TObjectList;
  TVNode: TTreeNode;
  PkgName: String;
  ConflictDep: TPkgDependency;
begin
  Additions:=TObjectList.Create(false);
  NewPackageID:=TLazPackageID.Create;
  try
    for i:=0 to AvailableTreeView.Items.TopLvlCount-1 do begin
      TVNode:=AvailableTreeView.Items.TopLvlItems[i];
      if not TVNode.MultiSelected then continue;
      PkgName:=TVNode.Text;
      // check string
      if not NewPackageID.StringToID(PkgName) then begin
        TVNode.Selected:=false;
        debugln('TInstallPkgSetDialog.AddToInstallButtonClick invalid ID: ',
                PkgName);
        continue;
      end;
      // check if already in list
      if NewInstalledPackagesContains(NewPackageID) then begin
        MessageDlg(lisDuplicate,
          Format(lisThePackageIsAlreadyInTheList, [NewPackageID.Name]), mtError,
          [mbCancel],0);
        TVNode.Selected:=false;
        exit;
      end;
      // check if a package with same name is already in the list
      j:=IndexOfNewInstalledPkgByName(NewPackageID.Name);
      if j>=0 then begin
        MessageDlg(lisConflict,
          Format(lisThereIsAlreadyAPackageInTheList, [NewPackageID.Name]),
          mtError,[mbCancel],0);
        TVNode.Selected:=false;
        exit;
      end;
      // check if package is loaded and has some attributes that prevents
      // installation in the IDE
      APackage:=PackageGraph.FindPackageWithID(NewPackageID);
      if APackage<>nil then begin
        if APackage.PackageType in [lptRunTime,lptRunTimeOnly] then begin
          IDEMessageDialog(lisNotADesigntimePackage,
            Format(lisThePackageIsNotADesignTimePackageItCanNotBeInstall, [
              APackage.IDAsString]), mtError,
            [mbCancel]);
          TVNode.Selected:=false;
          exit;
        end;
        ConflictDep:=PackageGraph.FindRuntimePkgOnlyRecursively(
          APackage.FirstRequiredDependency);
        if ConflictDep<>nil then begin
          IDEMessageDialog(lisNotADesigntimePackage,
            Format(lisThePackageCanNotBeInstalledBecauseItRequiresWhichI, [
              APackage.Name, ConflictDep.AsString]),
            mtError,[mbCancel]);
          TVNode.Selected:=false;
          exit;
        end;
      end;

      // ok => add to list
      Additions.Add(NewPackageID);
      NewPackageID:=TLazPackageID.Create;
    end;
    // all ok => add to list
    for i:=0 to Additions.Count-1 do
      FNewInstalledPackages.Add(Additions[i]);
    Additions.Clear;
    UpdateNewInstalledPackages;
    UpdateButtonStates;
  finally
    // clean up
    NewPackageID.Free;
    for i:=0 to Additions.Count-1 do
      TObject(Additions[i]).Free;
    Additions.Free;
  end;
end;

procedure TInstallPkgSetDialog.AddToUninstall;
var
  i: Integer;
  OldPackageID: TLazPackageID;
  APackage: TLazPackage;
  Deletions: TObjectList;
  DelPackageID: TLazPackageID;
  j: LongInt;
  TVNode: TTreeNode;
  PkgName: String;
begin
  OldPackageID := nil;
  Deletions:=TObjectList.Create(true);
  try
    for i:=0 to InstallTreeView.Items.TopLvlCount-1 do begin
      TVNode:=InstallTreeView.Items.TopLvlItems[i];
      if not TVNode.MultiSelected then continue;
      if OldPackageID = nil then
        OldPackageID:=TLazPackageID.Create;
      PkgName:=TVNode.Text;
      if not OldPackageID.StringToID(PkgName) then begin
        TVNode.Selected:=false;
        debugln('TInstallPkgSetDialog.AddToUninstallButtonClick invalid ID: ',
                PkgName);
        continue;
      end;
      // ok => add to deletions
      Deletions.Add(OldPackageID);
      DelPackageID:=OldPackageID;
      OldPackageID := nil;
      // get package
      APackage:=PackageGraph.FindPackageWithID(DelPackageID);
      if APackage<>nil then begin
        // check if package is a base package
        if APackage.AutoCreated
        or PackageGraph.IsStaticBasePackage(APackage.Name) then begin
          TVNode.Selected:=false;
          MessageDlg(lisUninstallImpossible,
            Format(lisThePackageCanNotBeUninstalledBecauseItIsNeededByTh, [
              APackage.Name]), mtError, [mbCancel], 0);
          exit;
        end;
      end;
    end;

    // ok => remove from list
    InstallTreeView.Selected:=nil;
    for i:=0 to Deletions.Count-1 do begin
      DelPackageID:=TLazPackageID(Deletions[i]);
      j:=IndexOfNewInstalledPackageID(DelPackageID);
      FNewInstalledPackages.Delete(j);
    end;

    UpdateNewInstalledPackages;
    UpdateButtonStates;
  finally
    OldPackageID.Free;
    Deletions.Free;
  end;
end;

function TInstallPkgSetDialog.GetNewInstalledPackages: TObjectList;
var
  i: Integer;
  NewPackageID: TLazPackageID;
begin
  Result:=TObjectList.Create(true);
  for i:=0 to FNewInstalledPackages.Count-1 do begin
    NewPackageID:=TLazPackageID.Create;
    NewPackageID.AssignID(TLazPackageID(FNewInstalledPackages[i]));
    Result.Add(NewPackageID);
  end;
end;

end.

