Procházet zdrojové kódy

Work on adding CreateExtractionPage and also make some other improvements. Some todos left as mentioned in the code and also didn't actually test the page yet.

Martijn Laan před 9 měsíci
rodič
revize
dc634c99de

+ 9 - 0
ISHelp/isxclasses.pas

@@ -780,6 +780,15 @@ TDownloadWizardPage = class(TOutputProgressWizardPage)
   property ShowBaseNameInsteadOfUrl: Boolean; read write;
 end;
 
+TExtractionWizardPage = class(TOutputProgressWizardPage)
+  property AbortButton: TNewButton; read;
+  property AbortedByUser: Boolean; read;
+  procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean);
+  procedure Clear;
+  function Extract: Integer;
+  property ShowArchiveInsteadOfFile: Boolean; read write;
+end;
+
 TUIStateForm = class(TForm)
 end;
 

+ 23 - 20
ISHelp/isxclasses_wordlists_generated.pas

@@ -23,21 +23,22 @@ var
     'TComponent', 'TConstraintSize', 'TControl', 'TCursor', 'TCustomCheckBox', 'TCustomComboBox',
     'TCustomControl', 'TCustomEdit', 'TCustomFolderTreeView', 'TCustomLabel', 'TCustomLinkLabel',
     'TCustomListBox', 'TCustomMemo', 'TCustomPanel', 'TDownloadWizardPage', 'TDuplicates',
-    'TEdit', 'TEditCharCase', 'TEShiftState', 'TFileStream', 'TFolderRenameEvent', 'TFolderTreeView',
-    'TFont', 'TFontStyle', 'TFontStyles', 'TForm', 'TFormBorderStyle', 'TFormStyle', 'TGraphic',
-    'TGraphicControl', 'TGraphicsObject', 'THandleStream', 'TInputDirWizardPage', 'TInputFileWizardPage',
-    'TInputOptionWizardPage', 'TInputQueryWizardPage', 'TKeyEvent', 'TKeyPressEvent', 'TLabel',
-    'TLinkLabel', 'TListBox', 'TListBoxStyle', 'TMainForm', 'TMemo', 'TNewButton', 'TNewCheckBox',
-    'TNewCheckListBox', 'TNewComboBox', 'TNewEdit', 'TNewLinkLabel', 'TNewListBox', 'TNewMemo',
-    'TNewNotebook', 'TNewNotebookPage', 'TNewProgressBar', 'TNewProgressBarState', 'TNewProgressBarStyle',
-    'TNewRadioButton', 'TNewStaticText', 'TNotifyEvent', 'TObject', 'TOutputMarqueeProgressWizardPage',
-    'TOutputMsgMemoWizardPage', 'TOutputMsgWizardPage', 'TOutputProgressWizardPage', 'TPanel',
-    'TPanelBevel', 'TPasswordEdit', 'TPen', 'TPenMode', 'TPenStyle', 'TPersistent', 'TPosition',
-    'TRadioButton', 'TRichEditViewer', 'TScrollingWinControl', 'TScrollStyle', 'TSetupForm',
-    'TShiftState', 'TSizeConstraints', 'TStartMenuFolderTreeView', 'TStream', 'TStringList',
-    'TStrings', 'TStringStream', 'TSysLinkEvent', 'TSysLinkType', 'TUIStateForm', 'TUninstallProgressForm',
-    'TWinControl', 'TWizardForm', 'TWizardPage', 'TWizardPageButtonEvent', 'TWizardPageCancelEvent',
-    'TWizardPageNotifyEvent', 'TWizardPageShouldSkipEvent'
+    'TEdit', 'TEditCharCase', 'TEShiftState', 'TExtractionWizardPage', 'TFileStream', 'TFolderRenameEvent',
+    'TFolderTreeView', 'TFont', 'TFontStyle', 'TFontStyles', 'TForm', 'TFormBorderStyle',
+    'TFormStyle', 'TGraphic', 'TGraphicControl', 'TGraphicsObject', 'THandleStream', 'TInputDirWizardPage',
+    'TInputFileWizardPage', 'TInputOptionWizardPage', 'TInputQueryWizardPage', 'TKeyEvent',
+    'TKeyPressEvent', 'TLabel', 'TLinkLabel', 'TListBox', 'TListBoxStyle', 'TMainForm',
+    'TMemo', 'TNewButton', 'TNewCheckBox', 'TNewCheckListBox', 'TNewComboBox', 'TNewEdit',
+    'TNewLinkLabel', 'TNewListBox', 'TNewMemo', 'TNewNotebook', 'TNewNotebookPage', 'TNewProgressBar',
+    'TNewProgressBarState', 'TNewProgressBarStyle', 'TNewRadioButton', 'TNewStaticText',
+    'TNotifyEvent', 'TObject', 'TOutputMarqueeProgressWizardPage', 'TOutputMsgMemoWizardPage',
+    'TOutputMsgWizardPage', 'TOutputProgressWizardPage', 'TPanel', 'TPanelBevel', 'TPasswordEdit',
+    'TPen', 'TPenMode', 'TPenStyle', 'TPersistent', 'TPosition', 'TRadioButton', 'TRichEditViewer',
+    'TScrollingWinControl', 'TScrollStyle', 'TSetupForm', 'TShiftState', 'TSizeConstraints',
+    'TStartMenuFolderTreeView', 'TStream', 'TStringList', 'TStrings', 'TStringStream',
+    'TSysLinkEvent', 'TSysLinkType', 'TUIStateForm', 'TUninstallProgressForm', 'TWinControl',
+    'TWizardForm', 'TWizardPage', 'TWizardPageButtonEvent', 'TWizardPageCancelEvent', 'TWizardPageNotifyEvent',
+    'TWizardPageShouldSkipEvent'
   ];
 
   PascalEnumValues_Isxclasses: array of AnsiString = [
@@ -89,6 +90,7 @@ var
     'function CheckItem(Index: Integer; AOperation: TCheckItemOperation): Boolean;',
     'function CopyFrom(Source: TStream; ByteCount: Int64; BufferSize: Integer): Int64;',
     'function Download: Int64;',
+    'function Extract: Integer;',
     'function Find(S: String; var Index: Integer): Boolean;',
     'function FindComponent(AName: String): TComponent;',
     'function FindNextPage(CurPage: TNewNotebookPage; GoForward: Boolean): TNewNotebookPage;',
@@ -102,6 +104,7 @@ var
     'function TextHeight(Text: String): Integer;',
     'function TextWidth(Text: String): Integer;',
     'function Write(Buffer: AnyString; ByteCount: Longint): Longint;',
+    'procedure Add(ArchiveFileName, DestDir: String; FullPaths: Boolean);',
     'procedure Add(Url, BaseName, RequiredSHA256OfFile: String);',
     'procedure AddEx(Url, BaseName, RequiredSHA256OfFile, UserName, Password: String);',
     'procedure AddStrings(Strings: TStrings);',
@@ -195,11 +198,11 @@ var
     'SelectComponentsPage', 'SelectDirBitmapImage', 'SelectDirBrowseLabel', 'SelectDirLabel',
     'SelectDirPage', 'Selected', 'SelectedValueIndex', 'SelectGroupBitmapImage', 'SelectProgramGroupPage',
     'SelectStartMenuFolderBrowseLabel', 'SelectStartMenuFolderLabel', 'SelectTasksLabel',
-    'SelectTasksPage', 'SelLength', 'SelStart', 'SelText', 'Shape', 'ShowAccelChar', 'ShowBaseNameInsteadOfUrl',
-    'ShowHint', 'Showing', 'ShowLines', 'Size', 'SizeAndCenterOnShow', 'Sorted', 'State',
-    'StatusLabel', 'Stretch', 'Strings', 'Style', 'SubCaptionLabel', 'SubItemFontStyle',
-    'Surface', 'SurfaceColor', 'SurfaceHeight', 'SurfaceWidth', 'TabOrder', 'TabStop',
-    'Tag', 'TasksList', 'Text', 'Top', 'TypesCombo', 'UseRichEdit', 'UserInfoNameEdit',
+    'SelectTasksPage', 'SelLength', 'SelStart', 'SelText', 'Shape', 'ShowAccelChar', 'ShowArchiveInsteadOfFile',
+    'ShowBaseNameInsteadOfUrl', 'ShowHint', 'Showing', 'ShowLines', 'Size', 'SizeAndCenterOnShow',
+    'Sorted', 'State', 'StatusLabel', 'Stretch', 'Strings', 'Style', 'SubCaptionLabel',
+    'SubItemFontStyle', 'Surface', 'SurfaceColor', 'SurfaceHeight', 'SurfaceWidth', 'TabOrder',
+    'TabStop', 'Tag', 'TasksList', 'Text', 'Top', 'TypesCombo', 'UseRichEdit', 'UserInfoNameEdit',
     'UserInfoNameLabel', 'UserInfoOrgEdit', 'UserInfoOrgLabel', 'UserInfoPage', 'UserInfoSerialEdit',
     'UserInfoSerialLabel', 'UseVisualStyle', 'Values', 'Visible', 'WantReturns', 'WantTabs',
     'WelcomeLabel1', 'WelcomeLabel2', 'WelcomePage', 'Width', 'WizardBitmapImage', 'WizardBitmapImage2',

+ 50 - 6
ISHelp/isxfunc.xml

@@ -1795,7 +1795,8 @@ end;</pre></example>
 <link topic="isxfunc_DownloadTemporaryFileSize">DownloadTemporaryFileSize</link><br />
 <link topic="isxfunc_DownloadTemporaryFileDate">DownloadTemporaryFileDate</link><br />
 <link topic="isxfunc_CreateDownloadPage">CreateDownloadPage</link><br />
-<link topic="isxfunc_ExtractTemporaryFile">ExtractTemporaryFile</link></p></seealso>
+<link topic="isxfunc_ExtractTemporaryFile">ExtractTemporaryFile</link><br />
+<link topic="isxfunc_Extract7ZipArchive">Extract7ZipArchive</link></p></seealso>
         <example><pre>
 [Code]
 function OnDownloadProgress(const Url, Filename: String; const Progress, ProgressMax: Int64): Boolean;
@@ -1841,11 +1842,16 @@ end;</pre>
 <p>See <link topic="isxfunc_DownloadTemporaryFile">DownloadTemporaryFile</link> for other considerations.</p></description>
       </function>
       <function>
-        <name>Extract7ZipFile</name>
-        <prototype>function Extract7ZipFile(const FileName, DestDir: String; const FullPaths: Boolean): Integer;</prototype>
-        <description><p>Extracts the specified 7-Zip archive to the specified directory, with or without using path names. Returns zero if successful, nonzero otherwise</p>
-<p>The archive must not be encrypted.</p></description>
-        <remarks><p>Uses an embedded version of the &quot;7z ANSI-C Decoder&quot; from the LZMA SDK by Igor Pavlov, as-is, except that Unicode support and error messages were improved and that it outputs memory requirements.</p>
+        <name>Extract7ZipArchive</name>
+        <prototype>function Extract7ZipArchive(const ArchiveFileName, DestDir: String; const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress): Integer;</prototype>
+        <description><p>Extracts the specified 7-Zip archive to the specified directory, with or without using path names.</p>
+<p>Returns zero if successful, nonzero otherwise</p>
+<p>The archive must not be encrypted.</p>
+<p>Set OnExtractionProgress to a function to be informed of progress, or <tt>nil</tt> otherwise.</p></description>
+        <remarks><p>TOnExtractionProgress is defined as:</p>
+<p><tt>TOnExtractionProgress = function(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean;</tt></p>
+<p>Return True to allow the extraction to continue, False otherwise.</p>
+<p><tt>Extract7ZipArchive</tt> uses an embedded version of the &quot;7z ANSI-C Decoder&quot; from the LZMA SDK by Igor Pavlov, as-is, except that Unicode support and error messages were improved and that it outputs memory requirements.</p>
 <p>All output of the decoder is logged if logging is enabled, including error messages but excluding empty lines.</p>
 <p>The decoder has the following limitations, as written by Igor Pavlov in the LZMA SDK:<br /><br />
 -It reads only &quot;FileName&quot;, &quot;Size&quot;, &quot;LastWriteTime&quot; and &quot;CRC&quot; information for each file in archive.<br />
@@ -1858,6 +1864,27 @@ You can create .7z archive with 7z.exe, 7za.exe or 7zr.exe:<br /><br />
 If you have big number of files in archive, and you need fast extracting, you can use partly-solid archives:<br /><br />
 7za.exe a archive.7z *.htm -ms=512K -r -mx -m0fb=255 -m0d=512K<br /><br />
 In that example 7-Zip will use 512KB solid blocks. So it needs to decompress only 512KB for extracting one file from such archive.</p></remarks>
+        <seealso><p><link topic="isxfunc_CreateExtractionPage">CreateExtractionPage</link><br />
+<link topic="isxfunc_CreateDownloadPage">CreateDownloadPage</link><br />
+<link topic="isxfunc_DownloadTemporaryFile">DownloadTemporaryFile</link><br />
+<link topic="isxfunc_ExtractTemporaryFile">ExtractTemporaryFile</link></p></seealso>
+        <example><pre>
+[Code]
+function OnExtractionProgress(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean;
+begin
+  Log(Format('  %s\%s: %d of %d bytes done.', [ArchiveFileName, FileName, Progress, ProgressMax]))
+  Result := True;
+end;
+
+function InitializeSetup: Boolean;
+begin
+  try
+    Result := Extract7ZipArchive(ExpandConstant('{tmp}\Archive.7z'), ExpandConstant('{app}'), True, @OnExtractionProgress) = 0;
+  except
+    Log(GetExceptionMessage);
+    Result := False;
+  end;
+end;</pre></example>
       </function>
     </subcategory>
     <subcategory>
@@ -2621,6 +2648,23 @@ Page := CreateOutputMsgMemoPage(wpWelcome,
         <example><p>See <i>CodeDownloadFiles.iss</i> for an example.</p></example>
         <seealso><p><link topic="scriptclasses" anchor="TDownloadWizardPage">TDownloadWizardPage</link><br />
 <link topic="isxfunc_DownloadTemporaryFile">DownloadTemporaryFile</link><br />
+<link topic="isxfunc_CreateOutputProgressPage">CreateOutputProgressPage</link></p></seealso>
+      </function>
+      <function>
+        <name>CreateExtractionPage</name>
+        <prototype>function CreateExtractionPage(const ACaption, ADescription: String; const OnExtractionProgress: TOnExtractionProgress): ExtractionWizardPage;</prototype>
+        <description><p>Creates a wizard page to extract 7-Zip archives and show progress.</p>
+<p>Set OnExtractionProgress to a function to be informed of progress, or <tt>nil</tt> otherwise.</p>    
+<p>Unlike the other types of wizard pages, progress pages are not displayed as part of the normal page sequence (note that there is no <tt>AfterID</tt> parameter). A progress page can only be displayed programmatically by calling its <tt>Show</tt> method.</p></description>
+        <remarks><p>Call the <tt>Show</tt> method to activate and show the page. When you're finished with it, call the <tt>Hide</tt> method to revert to the previous page.</p>
+<p>Always put the <tt>Hide</tt> call inside the <tt>finally</tt> part of a <tt>try..finally</tt> language construct, as demonstrated in <i>CodeDownloadFiles.iss</i>. Not calling <tt>Hide</tt> will result in the wizard being permanently stuck on the progress page.</p>
+<p>To add a new archive to extract, call the <tt>Add</tt> method. Always call the <tt>Clear</tt> method before adding the first file.</p>
+<p>To start the extraction, call the <tt>Extract</tt> method. An exception will be raised if there was an error. Otherwise, <tt>Extract</tt> returns the number of archives extracted.</p>
+<p>Set the <tt>ShowArchiveInsteadFile</tt> property to <tt>True</tt> to show the name of the archive which is being extracted to the user instead of the names of the files inside the archive.</p>
+<p>See <link topic="isxfunc_Extract7ZipArchive">Extract7ZipArchive</link> for other considerations and the definition of <tt>TOnExtractionProgress</tt>.</p></remarks>
+        <example><p>See <i>CodeDownloadFiles.iss</i> for an example of <tt>CreateDownloadPage</tt> which works very similar to <tt>CreateExtractionPage</tt>.</p></example>
+        <seealso><p><link topic="scriptclasses" anchor="TExtractionWizardPage">TExtractionWizardPage</link><br />
+<link topic="isxfunc_Extract7ZipArchive">Extract7ZipArchive</link><br />
 <link topic="isxfunc_CreateOutputProgressPage">CreateOutputProgressPage</link></p></seealso>
       </function>
       <function>

+ 15 - 0
Projects/Src/Compiler.ScriptClasses.pas

@@ -567,6 +567,20 @@ begin
   end;
 end;
 
+procedure RegisterExtractionWizardPage_C(Cl: TPSPascalCompiler);
+begin
+  with CL.AddClassN(Cl.FindClass('TOutputProgressWizardPage'),'TExtractionWizardPage') do
+  begin
+    RegisterProperty('AbortButton', 'TNewButton', iptr);
+    RegisterProperty('AbortedByUser', 'Boolean', iptr);
+    RegisterProperty('ShowArchiveInsteadOfFile', 'Boolean', iptrw);
+    RegisterMethod('procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean)');
+    RegisterMethod('procedure Clear');
+    RegisterMethod('function Extract: Integer');
+    RegisterMethod('procedure Show'); { Without this TOutputProgressWizardPage's Show will be called }
+  end;
+end;
+
 procedure RegisterHandCursor_C(Cl: TPSPascalCompiler);
 begin
   cl.AddConstantN('crHand', 'Integer').Value.ts32 := crHand;
@@ -675,6 +689,7 @@ begin
   RegisterOutputProgressWizardPage_C(Cl);
   RegisterOutputMarqueeProgressWizardPage_C(Cl);
   RegisterDownloadWizardPage_C(Cl);
+  RegisterExtractionWizardPage_C(Cl);
 
   RegisterHandCursor_C(Cl);
   

+ 1 - 0
Projects/Src/Compiler.ScriptFunc.pas

@@ -139,6 +139,7 @@ begin
     'end');
 
   RegisterType('TOnDownloadProgress', 'function(const Url, FileName: string; const Progress, ProgressMax: Int64): Boolean;');
+  RegisterType('TOnExtractionProgress', 'function(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean;');
   RegisterType('TOnLog', 'procedure(const S: String; const Error, FirstLine: Boolean);');
 
   for var ScriptFuncTable in ScriptFuncTables do

+ 64 - 23
Projects/Src/Compression.SevenZipDecoder.pas

@@ -12,18 +12,29 @@ unit Compression.SevenZipDecoder;
 
 interface
 
-function SevenZipDecode(const FileName, DestDir: String;
-  const FullPaths: Boolean): Integer;
+type
+  TOnExtractionProgress = function(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean of object;
+
+function Extract7ZipArchive(const ArchiveFileName, DestDir: String;
+  const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress): Integer;
 
 implementation
 
 uses
   Windows, SysUtils, Forms,
   PathFunc,
-  Setup.LoggingFunc, Setup.MainFunc;
+  Setup.LoggingFunc, Setup.MainFunc, Setup.InstFunc;
+
+type
+  TSevenZipDecodeState = record
+    ExpandedDestDir: String;
+    LogBuffer: AnsiString;
+    OnExtractionProgress: TOnExtractionProgress;
+    LastReportedProgress, LastReportedProgressMax: UInt64;
+  end;
 
 var
-  ExpandedDestDir: String;
+  State: TSevenZipDecodeState;
 
 { Compiled by Visual Studio 2022 using compile.bat
   To enable source debugging recompile using compile-bcc32c.bat and turn off the VISUALSTUDIO define below
@@ -37,7 +48,7 @@ function __CreateDirectoryW(lpPathName: LPCWSTR;
   lpSecurityAttributes: PSecurityAttributes): BOOL; cdecl;
 begin
   var ExpandedDir: String;
-  if PathExpand(lpPathName, ExpandedDir) and  PathStartsWith(ExpandedDir, ExpandedDestDir) then
+  if PathExpand(lpPathName, ExpandedDir) and  PathStartsWith(ExpandedDir, State.ExpandedDestDir) then
     Result := CreateDirectoryW(PChar(ExpandedDir), lpSecurityAttributes)
   else begin
     Result := False;
@@ -61,7 +72,7 @@ function __CreateFileW(lpFileName: LPCWSTR; dwDesiredAccess, dwShareMode: DWORD;
   hTemplateFile: THandle): THandle; cdecl;
 begin
   var ExpandedFileName: String;
-  if PathExpand(lpFileName, ExpandedFileName) and PathStartsWith(ExpandedFileName, ExpandedDestDir) then
+  if PathExpand(lpFileName, ExpandedFileName) and PathStartsWith(ExpandedFileName, State.ExpandedDestDir) then
     Result := CreateFileW(PChar(ExpandedFileName), dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile)
   else begin
     Result := INVALID_HANDLE_VALUE;
@@ -189,9 +200,6 @@ begin
     Setup.LoggingFunc.Log(UTF8ToString(S));
 end;
 
-var
-  LogBuffer: AnsiString;
-
 function __fputs(str: PAnsiChar; unused: Pointer): Integer; cdecl;
 
   function FindNewLine(const S: AnsiString): Integer;
@@ -206,14 +214,14 @@ function __fputs(str: PAnsiChar; unused: Pointer): Integer; cdecl;
 
 begin
   try
-    LogBuffer := LogBuffer + str;
-    var P := FindNewLine(LogBuffer);
+    State.LogBuffer := State.LogBuffer + str;
+    var P := FindNewLine(State.LogBuffer);
     while P <> 0 do begin
-      Log(Copy(LogBuffer, 1, P-1));
-      if (LogBuffer[P] = #13) and (P < Length(LogBuffer)) and (LogBuffer[P+1] = #10) then
+      Log(Copy(State.LogBuffer, 1, P-1));
+      if (State.LogBuffer[P] = #13) and (P < Length(State.LogBuffer)) and (State.LogBuffer[P+1] = #10) then
         Inc(P);
-      Delete(LogBuffer, 1, P);
-      P := FindNewLine(LogBuffer);
+      Delete(State.LogBuffer, 1, P);
+      P := FindNewLine(State.LogBuffer);
     end;
     Result := 0;
   except
@@ -223,23 +231,56 @@ end;
 
 procedure _ReportProgress(const FileName: PChar; const Progress, ProgressMax: UInt64; var Abort: Bool); cdecl;
 begin
-  //Setup.LoggingFunc.Log(Format('%s: %d of %d', [FileName, Progress, ProgressMax]));
+  if Assigned(State.OnExtractionProgress) then begin
+    { Make sure script isn't called crazy often because that would slow the download significantly. Only report:
+      -At start or finish
+      -Or if somehow Progress decreased or Max changed
+      -Or if at least 512 KB progress was made since last report
+    }
+    if (Progress = 0) or (Progress = ProgressMax) or
+       (Progress < State.LastReportedProgress) or (ProgressMax <> State.LastReportedProgressMax) or
+       ((Progress - State.LastReportedProgress) > 524288) then begin
+      try
+        var ArchiveFileName := '?'; //todo: fix
+        if not State.OnExtractionProgress(ArchiveFileName, FileName, Progress, ProgressMax) then
+          Abort := True;
+      finally
+        State.LastReportedProgress := Progress;
+        State.LastReportedProgressMax := ProgressMax;
+      end;
+    end;
+  end;
+
   if not Abort and DownloadTemporaryFileOrSevenZipDecodeProcessMessages then
     Application.ProcessMessages;
 end;
 
-function SevenZipDecode(const FileName, DestDir: String;
-  const FullPaths: Boolean): Integer;
+function Extract7ZipArchive(const ArchiveFileName, DestDir: String;
+  const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress): Integer;
 begin
+  if ArchiveFileName = '' then
+    InternalError('Extract7ZipArchive: Invalid ArchiveFileName value');
+  if DestDir = '' then
+    InternalError('Extract7ZipArchive: Invalid DestDir value');
+
+  LogFmt('Extracting 7-Zip archive %s to %s. Full paths? %s', [ArchiveFileName, DestDir, SYesNo[FullPaths]]);
+
   var SaveCurDir := GetCurrentDir;
   if not SetCurrentDir(DestDir) then
     Exit(-1);
   try
-    LogBuffer := '';
-    ExpandedDestDir := AddBackslash(PathExpand(DestDir));
-    Result := IS_7zDec(PChar(FileName), FullPaths);
-    if LogBuffer <> '' then
-      Log(LogBuffer);
+    State.LogBuffer := '';
+    State.ExpandedDestDir := AddBackslash(PathExpand(DestDir));
+    State.OnExtractionProgress := OnExtractionProgress;
+    State.LastReportedProgress := 0;
+    State.LastReportedProgressMax := 0;
+
+    Result := IS_7zDec(PChar(ArchiveFileName), FullPaths);
+
+    //todo: throw exception on Result <> 0 like DownloadTemporaryFile uses exceptions?
+
+    if State.LogBuffer <> '' then
+      Log(State.LogBuffer);
   finally
     SetCurrentDir(SaveCurDir);
   end;

+ 1 - 1
Projects/Src/IDE.ScintStylerInnoSetup.pas

@@ -455,7 +455,7 @@ const
     'TArrayOfString', 'TArrayOfChar', 'TArrayOfBoolean', 'TArrayOfInteger', 'DWORD',
     'UINT', 'BOOL', 'DWORD_PTR', 'UINT_PTR', 'INT_PTR', 'TFileTime',
     'TExecWait', 'TExecOutput', 'TFindRec', 'TWindowsVersion',
-    'TOnDownloadProgress', 'TOnLog'
+    'TOnDownloadProgress', 'TOnExtractionProgress', 'TOnLog'
     { ScriptClasses: see PascalTypes_Isxclasses in isxclasses_wordlists_generated }
   ];
 

+ 12 - 0
Projects/Src/Setup.ScriptClasses.pas

@@ -343,6 +343,17 @@ begin
   end;
 end;
 
+procedure RegisterExtractionWizardPage_R(CL: TPSRuntimeClassImporter);
+begin
+  with CL.Add(TExtractionWizardPage) do
+  begin
+    RegisterMethod(@TExtractionWizardPage.Add, 'Add');
+    RegisterMethod(@TExtractionWizardPage.Clear, 'Clear');
+    RegisterMethod(@TExtractionWizardPage.Extract, 'Extract');
+    RegisterMethod(@TExtractionWizardPage.Show, 'Show');
+  end;
+end;
+
 procedure RegisterHandCursor_R(Cl: TPSRuntimeClassImporter);
 const
   IDC_HAND = MakeIntResource(32649);
@@ -447,6 +458,7 @@ begin
     RegisterOutputProgressWizardPage_R(Cl);
     RegisterOutputMarqueeProgressWizardPage_R(Cl);
     RegisterDownloadWizardPage_R(Cl);
+    RegisterExtractionWizardPage_R(Cl);
 
     RegisterHandCursor_R(Cl);
 

+ 157 - 18
Projects/Src/Setup.ScriptDlg.pas

@@ -12,8 +12,8 @@ unit Setup.ScriptDlg;
 interface
 
 uses
-  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, StdCtrls, Contnrs,
-  Setup.WizardForm, Setup.Install,
+  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, StdCtrls, Contnrs, Generics.Collections,
+  Setup.WizardForm, Setup.Install, Compression.SevenZipDecoder,
   NewCheckListBox, NewStaticText, NewProgressBar, PasswordEdit, RichEditViewer,
   BidiCtrls, TaskbarProgressFunc;
 
@@ -172,9 +172,14 @@ type
       procedure SetProgress(const Position, Max: Longint);
   end;
 
+  TDownloadFile = class
+    Url, BaseName, RequiredSHA256OfFile, UserName, Password: String;
+  end;
+  TDownloadFiles = TObjectList<TDownloadFile>;
+
   TDownloadWizardPage = class(TOutputProgressWizardPage)
     private
-      FFiles: TObjectList;
+      FFiles: TDownloadFiles;
       FOnDownloadProgress: TOnDownloadProgress;
       FShowBaseNameInsteadOfUrl: Boolean;
       FAbortButton: TNewButton;
@@ -198,6 +203,37 @@ type
       property ShowBaseNameInsteadOfUrl: Boolean read FShowBaseNameInsteadOfUrl write FShowBaseNameInsteadOfUrl;
   end;
   
+  TArchive = class
+    FileName, DestDir: String;
+    FullPaths: Boolean;
+  end;
+  TArchives = TObjectList<TArchive>;
+
+  TExtractionWizardPage = class(TOutputProgressWizardPage)
+    private
+      FArchives: TArchives;
+      FOnExtractionProgress: TOnExtractionProgress;
+      FShowArchiveInsteadOfFile: Boolean;
+      FAbortButton: TNewButton;
+      FShowProgressControlsOnNextProgress, FAbortedByUser: Boolean;
+      procedure AbortButtonClick(Sender: TObject);
+      function InternalOnExtractionProgress(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean;
+      procedure ShowProgressControls(const AVisible: Boolean);
+    public
+      constructor Create(AOwner: TComponent); override;
+      destructor Destroy; override;
+      procedure Initialize; override;
+      procedure Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean);
+      procedure Clear;
+      function Extract: Integer;
+      property OnExtractionProgress: TOnExtractionProgress write FOnExtractionProgress;
+      procedure Show; override;
+    published
+      property AbortButton: TNewButton read FAbortButton;
+      property AbortedByUser: Boolean read FAbortedByUser;
+      property ShowArchiveInsteadOfFile: Boolean read FShowArchiveInsteadOfFile write FShowArchiveInsteadOfFile;
+  end;
+
 implementation
 
 uses
@@ -920,11 +956,6 @@ end;
 
 {--- Download ---}
 
-type
-  TDownloadFile = class
-    Url, BaseName, RequiredSHA256OfFile, UserName, Password: String;
-  end;
-
 procedure TDownloadWizardPage.AbortButtonClick(Sender: TObject);
 begin
   FAbortedByUser := LoggedMsgBox(SetupMessages[msgStopDownload], '', mbConfirmation, MB_YESNO, True, ID_YES) = IDYES;
@@ -970,7 +1001,7 @@ constructor TDownloadWizardPage.Create(AOwner: TComponent);
 begin
   inherited;
   FUseMarqueeStyle := True;
-  FFiles := TObjectList.Create;
+  FFiles := TDownloadFiles.Create;
 end;
 
 destructor TDownloadWizardPage.Destroy;
@@ -1019,10 +1050,8 @@ begin
 end;
 
 procedure TDownloadWizardPage.AddEx(const Url, BaseName, RequiredSHA256OfFile, UserName, Password: String);
-var
-  F: TDownloadFile;
 begin
-  F := TDownloadFile.Create;
+  var F := TDownloadFile.Create;
   F.Url := Url;
   F.BaseName := BaseName;
   F.RequiredSHA256OfFile := RequiredSHA256OfFile;
@@ -1037,15 +1066,11 @@ begin
 end;
 
 function TDownloadWizardPage.Download: Int64;
-var
-  F: TDownloadFile;
-  I: Integer;
 begin
   FAbortedByUser := False;
-  
+
   Result := 0;
-  for I := 0 to FFiles.Count-1 do begin
-    F := TDownloadFile(FFiles[I]);
+  for var F in FFiles do begin
     { Don't need to set DownloadTemporaryFileProcessMessages before downloading since we already process messages ourselves. }
     SetDownloadCredentials(F.UserName, F.Password);
     Result := Result + DownloadTemporaryFile(F.Url, F.BaseName, F.RequiredSHA256OfFile, InternalOnDownloadProgress);
@@ -1053,4 +1078,118 @@ begin
   SetDownloadCredentials('', '');
 end;
 
+{--- Extraction ---}
+
+procedure TExtractionWizardPage.AbortButtonClick(Sender: TObject);
+begin
+  //todo: fix msg!
+  FAbortedByUser := LoggedMsgBox(SetupMessages[msgStopDownload], '', mbConfirmation, MB_YESNO, True, ID_YES) = IDYES;
+end;
+
+function TExtractionWizardPage.InternalOnExtractionProgress(const ArchiveFileName, FileName: string; const Progress, ProgressMax: Int64): Boolean;
+var
+  Progress32, ProgressMax32: LongInt;
+begin
+  if FAbortedByUser then begin
+    Log('Need to abort extraction.');
+    Result := False;
+  end else begin
+    Log(Format('  %d bytes done.', [Progress]));
+
+    FMsg2Label.Caption := IfThen(FShowArchiveInsteadOfFile, ArchiveFileName, FileName);
+    if ProgressMax > MaxLongInt then begin
+      Progress32 := Round((Progress / ProgressMax) * MaxLongInt);
+      ProgressMax32 := MaxLongInt;
+    end else begin
+      Progress32 := Progress;
+      ProgressMax32 := ProgressMax;
+    end;
+    SetProgress(Progress32, ProgressMax32); { This will process messages which we need for the abort button to work }
+
+    if FShowProgressControlsOnNextProgress then begin
+      ShowProgressControls(True);
+      FShowProgressControlsOnNextProgress := False;
+      ProcessMsgs;
+    end;
+
+    if Assigned(FOnExtractionProgress) then
+      Result := FOnExtractionProgress(ArchiveFileName, FileName, Progress, ProgressMax)
+    else
+      Result := True;
+  end;
+end;
+
+constructor TExtractionWizardPage.Create(AOwner: TComponent);
+begin
+  inherited;
+  FUseMarqueeStyle := True;
+  FArchives := TArchives.Create;
+end;
+
+destructor TExtractionWizardPage.Destroy;
+begin
+  FArchives.Free;
+  inherited;
+end;
+
+procedure TExtractionWizardPage.Initialize;
+begin
+  inherited;
+
+  FMsg1Label.Caption := SetupMessages[msgDownloadingLabel]; //todo: fix message
+
+  FAbortButton := TNewButton.Create(Self);
+  with FAbortButton do begin
+    Caption := SetupMessages[msgButtonStopDownload]; //todo: fix message
+    Top := FProgressBar.Top + FProgressBar.Height + WizardForm.ScalePixelsY(8);
+    Width := WizardForm.CalculateButtonWidth([Caption]);
+    Anchors := [akLeft, akTop];
+    Height := WizardForm.CancelButton.Height;
+    OnClick := AbortButtonClick;
+  end;
+  SetCtlParent(FAbortButton, Surface);
+end;
+
+procedure TExtractionWizardPage.Show;
+begin
+  if WizardForm.CurPageID <> ID then begin
+    ShowProgressControls(False);
+    FShowProgressControlsOnNextProgress := True;
+  end;
+  inherited;
+end;
+
+procedure TExtractionWizardPage.ShowProgressControls(const AVisible: Boolean);
+begin
+  FMsg2Label.Visible := AVisible;
+  FProgressBar.Visible := AVisible;
+  FAbortButton.Visible := AVisible;
+end;
+
+procedure TExtractionWizardPage.Add(const ArchiveFileName, DestDir: String; const FullPaths: Boolean);
+begin
+  var A := TArchive.Create;
+  A.FileName := ArchiveFileName;
+  A.DestDir := DestDir;
+  A.FullPaths := FullPaths;
+  FArchives.Add(A);
+end;
+
+procedure TExtractionWizardPage.Clear;
+begin
+  FArchives.Clear;
+end;
+
+function TExtractionWizardPage.Extract: Integer;
+begin
+  FAbortedByUser := False;
+
+  Result := 0;
+  for var A in FArchives do begin
+    { Don't need to set DownloadTemporaryFileOrSevenZipDecodeProcessMessages before extraction since we already process messages ourselves. }
+    if Extract7ZipArchive(A.FileName, A.DestDir, A.FullPaths, InternalOnExtractionProgress) = 0 then
+      Inc(Result);
+  end;
+end;
+
 end.

+ 44 - 26
Projects/Src/Setup.ScriptFunc.pas

@@ -129,18 +129,6 @@ end;
 function ScriptDlgProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean;
 var
   PStart: Cardinal;
-  NewPage: TWizardPage;
-  NewInputQueryPage: TInputQueryWizardPage;
-  NewInputOptionPage: TInputOptionWizardPage;
-  NewInputDirPage: TInputDirWizardPage;
-  NewInputFilePage: TInputFileWizardPage;
-  NewOutputMsgPage: TOutputMsgWizardPage;
-  NewOutputMsgMemoPage: TOutputMsgMemoWizardPage;
-  NewOutputProgressPage: TOutputProgressWizardPage;
-  NewOutputMarqueeProgressPage: TOutputMarqueeProgressWizardPage;
-  NewDownloadPage: TDownloadWizardPage;
-  OnDownloadProgress: TOnDownloadProgress;
-  NewSetupForm: TSetupForm;
 begin
   PStart := Stack.Count-1;
   Result := True;
@@ -156,7 +144,7 @@ begin
   end else if Proc.Name = 'CREATECUSTOMPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewPage := TWizardPage.Create(GetWizardForm);
+    var NewPage := TWizardPage.Create(GetWizardForm);
     try
       NewPage.Caption := Stack.GetString(PStart-2);
       NewPage.Description := Stack.GetString(PStart-3);
@@ -169,7 +157,7 @@ begin
   end else if Proc.Name = 'CREATEINPUTQUERYPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewInputQueryPage := TInputQueryWizardPage.Create(GetWizardForm);
+    var NewInputQueryPage := TInputQueryWizardPage.Create(GetWizardForm);
     try
       NewInputQueryPage.Caption := Stack.GetString(PStart-2);
       NewInputQueryPage.Description := Stack.GetString(PStart-3);
@@ -183,7 +171,7 @@ begin
   end else if Proc.Name = 'CREATEINPUTOPTIONPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewInputOptionPage := TInputOptionWizardPage.Create(GetWizardForm);
+    var NewInputOptionPage := TInputOptionWizardPage.Create(GetWizardForm);
     try
       NewInputOptionPage.Caption := Stack.GetString(PStart-2);
       NewInputOptionPage.Description := Stack.GetString(PStart-3);
@@ -198,7 +186,7 @@ begin
   end else if Proc.Name = 'CREATEINPUTDIRPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewInputDirPage := TInputDirWizardPage.Create(GetWizardForm);
+    var NewInputDirPage := TInputDirWizardPage.Create(GetWizardForm);
     try
       NewInputDirPage.Caption := Stack.GetString(PStart-2);
       NewInputDirPage.Description := Stack.GetString(PStart-3);
@@ -213,7 +201,7 @@ begin
   end else if Proc.Name = 'CREATEINPUTFILEPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewInputFilePage := TInputFileWizardPage.Create(GetWizardForm);
+    var NewInputFilePage := TInputFileWizardPage.Create(GetWizardForm);
     try
       NewInputFilePage.Caption := Stack.GetString(PStart-2);
       NewInputFilePage.Description := Stack.GetString(PStart-3);
@@ -227,7 +215,7 @@ begin
   end else if Proc.Name = 'CREATEOUTPUTMSGPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewOutputMsgPage := TOutputMsgWizardPage.Create(GetWizardForm);
+    var NewOutputMsgPage := TOutputMsgWizardPage.Create(GetWizardForm);
     try
       NewOutputMsgPage.Caption := Stack.GetString(PStart-2);
       NewOutputMsgPage.Description := Stack.GetString(PStart-3);
@@ -241,7 +229,7 @@ begin
   end else if Proc.Name = 'CREATEOUTPUTMSGMEMOPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewOutputMsgMemoPage := TOutputMsgMemoWizardPage.Create(GetWizardForm);
+    var NewOutputMsgMemoPage := TOutputMsgMemoWizardPage.Create(GetWizardForm);
     try
       NewOutputMsgMemoPage.Caption := Stack.GetString(PStart-2);
       NewOutputMsgMemoPage.Description := Stack.GetString(PStart-3);
@@ -256,7 +244,7 @@ begin
   end else if Proc.Name = 'CREATEOUTPUTPROGRESSPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewOutputProgressPage := TOutputProgressWizardPage.Create(GetWizardForm);
+    var NewOutputProgressPage := TOutputProgressWizardPage.Create(GetWizardForm);
     try
       NewOutputProgressPage.Caption := Stack.GetString(PStart-1);
       NewOutputProgressPage.Description := Stack.GetString(PStart-2);
@@ -270,7 +258,7 @@ begin
   end else if Proc.Name = 'CREATEOUTPUTMARQUEEPROGRESSPAGE' then begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
-    NewOutputMarqueeProgressPage := TOutputMarqueeProgressWizardPage.Create(GetWizardForm);
+    var NewOutputMarqueeProgressPage := TOutputMarqueeProgressWizardPage.Create(GetWizardForm);
     try
       NewOutputMarqueeProgressPage.Caption := Stack.GetString(PStart-1);
       NewOutputMarqueeProgressPage.Description := Stack.GetString(PStart-2);
@@ -285,12 +273,13 @@ begin
     if IsUninstaller then
       NoUninstallFuncError(Proc.Name);
     var P: PPSVariantProcPtr := Stack.Items[PStart-3];
+    var OnDownloadProgress: TOnDownloadProgress;
     { ProcNo 0 means nil was passed by the script }
     if P.ProcNo <> 0 then
       OnDownloadProgress := TOnDownloadProgress(Caller.GetProcAsMethod(P.ProcNo))
     else
       OnDownloadProgress := nil;
-    NewDownloadPage := TDownloadWizardPage.Create(GetWizardForm);
+    var NewDownloadPage := TDownloadWizardPage.Create(GetWizardForm);
     try
       NewDownloadPage.Caption := Stack.GetString(PStart-1);
       NewDownloadPage.Description := Stack.GetString(PStart-2);
@@ -302,6 +291,28 @@ begin
       raise;
     end;
     Stack.SetClass(PStart, NewDownloadPage);
+  end else if Proc.Name = 'CREATEXTRACTIONPAGE' then begin
+    if IsUninstaller then
+      NoUninstallFuncError(Proc.Name);
+    var P: PPSVariantProcPtr := Stack.Items[PStart-3];
+    var OnExtractionProgress: TOnExtractionProgress;
+    { ProcNo 0 means nil was passed by the script }
+    if P.ProcNo <> 0 then
+      OnExtractionProgress := TOnExtractionProgress(Caller.GetProcAsMethod(P.ProcNo))
+    else
+      OnExtractionProgress := nil;
+    var NewExtractionPage := TExtractionWizardPage.Create(GetWizardForm);
+    try
+      NewExtractionPage.Caption := Stack.GetString(PStart-1);
+      NewExtractionPage.Description := Stack.GetString(PStart-2);
+      GetWizardForm.AddPage(NewExtractionPage, -1);
+      NewExtractionPage.Initialize;
+      NewExtractionPage.OnExtractionProgress := OnExtractionProgress;
+    except
+      NewExtractionPage.Free;
+      raise;
+    end;
+    Stack.SetClass(PStart, NewExtractionPage);
   end else if Proc.Name = 'SCALEX' then begin
     InitializeScaleBaseUnits;
     Stack.SetInt(PStart, MulDiv(Stack.GetInt(PStart-1), ScaleBaseUnitX, OrigBaseUnitX));
@@ -309,7 +320,7 @@ begin
     InitializeScaleBaseUnits;
     Stack.SetInt(PStart, MulDiv(Stack.GetInt(PStart-1), ScaleBaseUnitY, OrigBaseUnitY));
   end else if Proc.Name = 'CREATECUSTOMFORM' then begin
-    NewSetupForm := TSetupForm.CreateNew(nil);
+    var NewSetupForm := TSetupForm.CreateNew(nil);
     try
       NewSetupForm.AutoScroll := False;
       NewSetupForm.BorderStyle := bsDialog;
@@ -773,7 +784,6 @@ end;
 function InstallProc(Caller: TPSExec; Proc: TPSExternalProcRec; Global, Stack: TPSStack): Boolean;
 var
   PStart: Cardinal;
-  OnDownloadProgress: TOnDownloadProgress;
 begin
   if IsUninstaller then
     NoUninstallFuncError(Proc.Name);
@@ -787,6 +797,7 @@ begin
     Stack.SetInt(PStart, ExtractTemporaryFiles(Stack.GetString(PStart-1)));
   end else if Proc.Name = 'DOWNLOADTEMPORARYFILE' then begin
     var P: PPSVariantProcPtr := Stack.Items[PStart-4];
+    var OnDownloadProgress: TOnDownloadProgress;
     { ProcNo 0 means nil was passed by the script }
     if P.ProcNo <> 0 then
       OnDownloadProgress := TOnDownloadProgress(Caller.GetProcAsMethod(P.ProcNo))
@@ -2106,8 +2117,15 @@ begin
     for I := 0 to N-1 do
       AscendingTrySizes[I] := VNGetInt(PSGetArrayField(Arr, I));
     Stack.SetBool(PStart, TBitmapImage(Stack.GetClass(PStart-1)).InitializeFromIcon(0, PChar(Stack.GetString(PStart-2)), Stack.GetInt(PStart-3), AscendingTrySizes));
-  end else if Proc.Name = 'EXTRACT7ZIPFILE' then begin
-    Stack.SetInt(PStart, SevenZipDecode(Stack.GetString(PStart-1), Stack.GetString(PStart-2), Stack.GetBool(PStart-3)));
+  end else if Proc.Name = 'EXTRACT7ZIPARCHIVE' then begin
+    var P: PPSVariantProcPtr := Stack.Items[PStart-4];
+    var OnExtractionProgress: TOnExtractionProgress;
+    { ProcNo 0 means nil was passed by the script }
+    if P.ProcNo <> 0 then
+      OnExtractionProgress := TOnExtractionProgress(Caller.GetProcAsMethod(P.ProcNo))
+    else
+      OnExtractionProgress := nil;
+    Stack.SetInt(PStart, Extract7ZipArchive(Stack.GetString(PStart-1), Stack.GetString(PStart-2), Stack.GetBool(PStart-3), OnExtractionProgress));
   end else if Proc.Name = 'DEBUGGING' then begin
     Stack.SetBool(PStart, Debugging);
   end else

+ 2 - 1
Projects/Src/Shared.ScriptFunc.pas

@@ -221,6 +221,7 @@ initialization
     'function CreateOutputProgressPage(const ACaption, ADescription: String): TOutputProgressWizardPage;',
     'function CreateOutputMarqueeProgressPage(const ACaption, ADescription: String): TOutputMarqueeProgressWizardPage;',
     'function CreateDownloadPage(const ACaption, ADescription: String; const OnDownloadProgress: TOnDownloadProgress): TDownloadWizardPage;',
+    'function CreateExtractionPage(const ACaption, ADescription: String; const OnExtractionProgress: TOnExtractionProgress): TExtractionWizardPage;',
     'function ScaleX(X: Integer): Integer;',
     'function ScaleY(Y: Integer): Integer;',
     'function CreateCustomForm: TSetupForm;'
@@ -539,7 +540,7 @@ initialization
     'function IsDotNetInstalled(const MinVersion: TDotNetVersion; const MinServicePack: Cardinal): Boolean;',
     'function IsMsiProductInstalled(const UpgradeCode: String; const PackedMinVersion: Int64): Boolean;',
     'function InitializeBitmapImageFromIcon(const BitmapImage: TBitmapImage; const IconFilename: String; const BkColor: TColor; const AscendingTrySizes: TArrayOfInteger): Boolean;',
-    'function Extract7ZipFile(const FileName, DestDir: String; const FullPaths: Boolean): Integer;',
+    'function Extract7ZipArchive(const FileName, DestDir: String; const FullPaths: Boolean; const OnExtractionProgress: TOnExtractionProgress): Integer;',
     'function Debugging: Boolean;'
   ];
 

+ 1 - 1
whatsnew.htm

@@ -95,7 +95,7 @@ For conditions of distribution and use, see <a href="files/is/license.txt">LICEN
   <li>Added a dark mode version of the documentation, automatically used by the Compiler IDE if a dark theme is chosen.</li>
   <li>Pascal Scripting changes:
   <ul>
-    <il>Added new <tt>Extract7ZipFile</tt> support function to extract a 7-Zip archive, based on the &quot;7z ANSI-C Decoder&quot; from the LZMA SDK by Igor Pavlov. See the new <a href="https://jrsoftware.org/ishelp/index.php?topic=isxfunc_extract7zipfile">help topic</a> for information about its limitations.</il>
+    <li>Added new <tt>Extract7ZipArchive</tt> support function to extract a 7-Zip archive, based on the &quot;7z ANSI-C Decoder&quot; from the LZMA SDK by Igor Pavlov. See the new <a href="https://jrsoftware.org/ishelp/index.php?topic=isxfunc_extract7ziparchive">help topic</a> for information about its limitations.<br />Added new <tt>CreateExtractionPage</tt> support function to easily show the extraction progress to the user.</li>
     <li>Added new <tt>ExecAndCaptureOutput</tt> support function to execute a program or batch file and capture its <i>stdout</i> and <i>stderr</i> outputs separately.</li>
     <li>Output logging now raises an exception if there was an error setting up output redirection (which should be very rare). The <i>PowerShell.iss</i> example script has been updated to catch the exception.</li>
     <li><tt>TInputDirWizardPage</tt>: Added new <tt>NewFolderName</tt> property to update the initial value passed to <tt>CreateInputDirPage</tt>.</li>