Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Mailing lists / pas2js / 2019-07

Pas2JS-Widgets with LFM streaming

6 messages · 2 people · –

Reply by email Subscribe to this list Your subscription

Read-only archive. Use j and k to move between messages.

Sven Barth

Hello together!

I've experimented a bit with Pas2JS and the Pas2JS-Widgets by heliosroots ( https://github.com/heliosroots/Pas2JS_Widget/tree/master ) and implemented loading of unmodified LFM files. It's a bit cumbersome for now, cause it needs changes in the project's HTML source as well as the main project file for each form added, but as a proof of concept it works. I've attached the patch that needs to be applied to the Pas2JS-Widget master.

To use it you proceed as follows:
- make sure you have installed Pas2JSDsgn (from Pas2JS) and Pas2JS_Designer_Package (from Pas2JS-Widgets)
- make sure you at least once opened Pas2JS_RTL (from Pas2JS) and Pas2JS_Widget (from Pas2JS-Widgets)
- create a new "Web Browser Application"
- add Pas2JS_RTL and Pas2JS_Widget as requirements
- add new forms using New -> Module -> Web Form (Pas2JS)
- remove the overridden Loaded method from any forms
- replace the main program's code with this (of course adjust the form/unit names to your needs):

=== code begin ===

program tp2j;
{$mode objfpc}
uses
   Forms, FormLoader, Interfaces, Unit1;
procedure DoInit(aData: TObject);
begin
   Application.Initialize;
   Application.CreateForm(TWForm1, WForm1);
   Application.Run;
end;
begin
   LoadForms([TWForm1], @DoInit, Nil);
end.

=== code end ===

- add a script tag in front of the project's script tag in the HTML file:   <script src="unit1.lfm" type="application/x-lazarus-form" id="TWForm1.lfm"></script>
- play around with the form (add components (from the Pas2JS component tab only!), add events, etc.)
- before compiling make sure that the project *does not* contain a requirement of Pas2JS_Designer_Package (the IDE currently adds this when adding a component)
- make sure that the *.LFM files are at the same location as the *.JS file
- load your application in the browser -> you should now see your form

A screenshot showing the resulting browser and the IDE is attached. :)

This can obviously be further improved:
- adding support also for frames and data modules
- caching the converted object stream (so that it doesn't need to be converted for each creation of a form)
- add some way of asynchronous loading?

And then of course the Pas2JS Widgets needs to be improved. :)

@heliosroots (in case you read this): do you have a more current version of your components? I've seen in one of your videos that you also have DB components available.

Regards,
Sven
-------------- next part --------------
diff --git a/pas2js/packages/widget/controls.pas b/pas2js/packages/widget/controls.pas
index 681a639..a020f44 100755
--- a/pas2js/packages/widget/controls.pas
+++ b/pas2js/packages/widget/controls.pas
@@ -264,6 +264,7 @@ type
     procedure MouseMove(Shift: TShiftState; X, Y: integer); virtual;
     procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer); virtual;
     procedure MouseWeel(Shift: TShiftState; WheelDelta: NativeInt; MousePos: TPoint; var Handled: boolean);
+    procedure SetParentComponent(Value: TComponent); override;
   protected
     property BorderStyle: TBorderStyle read FBorderStyle write SetBorderStyle;
     property TabOrder: NativeInt read FTabOrder write SetTabOrder;
@@ -1302,6 +1303,12 @@ begin
   end;
 end;
+procedure TControl.SetParentComponent(Value: TComponent);
+begin
+  if Value is TWinControl then
+    SetParent(TWinControl(Value));
+end;
+
 function TControl.HandleClick(AEvent: TJSMouseEvent): boolean;
 begin
   AEvent.StopPropagation;
diff --git a/pas2js/packages/widget/formloader.pas b/pas2js/packages/widget/formloader.pas
new file mode 100644
index 0000000..438f6dd
--- /dev/null
+++ b/pas2js/packages/widget/formloader.pas
@@ -0,0 +1,64 @@
+{
+  Based on Rtl.ScriptLoader (thus the same license as that (ToDo: which is?)).
+
+  Modifications by Sven Barth
+}
+unit FormLoader;
+
+interface
+
+uses
+  Forms;
+
+type
+  TLoadedCallBack = Reference to procedure(Data : TObject);
+  TProc = reference to procedure;
+  TCustomFormClassArray = array of TCustomFormClass;
+
+procedure LoadForms(aForms: TCustomFormClassArray; aCallback: TLoadedCallback; aData: TObject);
+
+implementation
+
+uses
+  SysUtils,
+  JS, Web;
+
+procedure LoadForms(aForms: TCustomFormClassArray; aCallback: TLoadedCallback; aData: TObject);
+
+  procedure Loader(aForm: TCustomFormClass; aHandler: TProc);
+  var
+    script: TJSElement;
+    req: TJSXMLHttpRequest;
+
+    procedure DoLoaded;
+    begin
+      script.textContent := req.responseText;
+      aHandler;
+    end;
+
+  var
+    src: String;
+  begin
+    script := document.getElementById(aForm.ClassName + '.lfm');
+    if not Assigned(script) then
+      raise Exception.CreateFmt('No script element found for ', [aForm.ClassName]);
+    src := script['src'];
+    req := TJSXMLHttpRequest.new;
+    req.AddEventListener('load', @DoLoaded);
+    req.Open('GET', src);
+    req.Send;
+  end;
+
+  procedure Run;
+  begin
+    if Length(aForms) > 0 then
+      Loader(TCustomFormClass(TJSArray(aForms).shift()), @Run)
+    else if Assigned(aCallback) then
+      aCallback(aData);
+  end;
+
+begin
+  Run;
+end;
+
+end.
diff --git a/pas2js/packages/widget/forms.pas b/pas2js/packages/widget/forms.pas
index e7f74b7..8f82ba1 100755
--- a/pas2js/packages/widget/forms.pas
+++ b/pas2js/packages/widget/forms.pas
@@ -128,10 +128,12 @@ type
   protected
     procedure Changed; override;
     function CreateHandleElement: TJSHTMLElement; override;
+    procedure ProcessResource; virtual;
   protected
     class function GetControlClassDefaultSize: TSize; override;
   public
     constructor Create(AOwner: TComponent); override;
+    constructor CreateNew(AOwner: TComponent; Num: Integer = 0); virtual;
     destructor Destroy; override;
     procedure AfterConstruction; override;
     procedure BeforeDestruction; override;
@@ -216,12 +218,58 @@ type
     property OnUnload: TNotifyEvent read FOnUnload write FOnUnload;
   end;
+  { TWForm }
+
+  TWForm = class(TCustomForm)
+  published
+    property ActiveControl;
+    property Align;
+    property AlphaBlend;
+    property AlphaBlendValue;
+    property Caption;
+    property ClientHeight;
+    property ClientWidth;
+    property Color;
+    property Enabled;
+    property Font;
+    property HandleClass;
+    property HandleID;
+    property KeyPreview;
+    property ShowHint;
+    property Visible;
+    property OnActivate;
+    property OnClick;
+    property OnClose;
+    property OnCloseQuery;
+    property OnCreate;
+    property OnDblClick;
+    property OnDeactivate;
+    property OnDestroy;
+    property OnHide;
+    property OnKeyDown;
+    property OnKeyPress;
+    property OnKeyUp;
+    property OnMouseDown;
+    property OnMouseEnter;
+    property OnMouseLeave;
+    property OnMouseMove;
+    property OnMouseUp;
+    property OnMouseWheel;
+    property OnResize;
+    property OnScroll;
+    property OnShow;
+  end;
+  TWFormClass = class of TWForm;
+
 { TODO: TScreen }
 function Application: TApplication;
 implementation
+uses
+  LResources, LCLStrConsts;
+
 {$hints off}
 procedure DefaultModalProc(Sender: TObject; ModalResult: TModalResult);
@@ -591,6 +639,13 @@ begin
   Result := TJSHTMLElement(Document.CreateElement('div'));
 end;
+procedure TCustomForm.ProcessResource;
+begin
+  if not InitResourceComponent(Self, TWForm) then
+    raise EResNotFound.CreateFmt(
+      rsFormResourceSNotFoundForResourcelessFormsCreateNew, [ClassName]);
+end;
+
 class function TCustomForm.GetControlClassDefaultSize: TSize;
 begin
   Result.Cx := 320;
@@ -598,6 +653,25 @@ begin
 end;
 constructor TCustomForm.Create(AOwner: TComponent);
+begin
+  //GlobalNameSpace.BeginWrite;
+  try
+    CreateNew(AOwner, 1); // this calls BeginFormUpdate, which is ended in AfterConstruction
+    if (ClassType  TWForm) and not (csDesigning in ComponentState) then
+    begin
+      //Include(FFormState, fsCreating);
+      try
+        ProcessResource;
+      finally
+        //Exclude(FFormState, fsCreating);
+      end;
+    end;
+  finally
+    //GlobalNameSpace.EndWrite;
+  end;
+end;
+
+constructor TCustomForm.CreateNew(AOwner: TComponent; Num: Integer);
 begin
   inherited Create(AOwner);
   FActiveControl := nil;
diff --git a/pas2js/packages/widget/graphics.pas b/pas2js/packages/widget/graphics.pas
index 4f41764..6c433cd 100755
--- a/pas2js/packages/widget/graphics.pas
+++ b/pas2js/packages/widget/graphics.pas
@@ -279,6 +279,10 @@ function JSColor(const AColor: TColor): string;
 function JSFont(const AFont: TFont): string;
 function JSMeasureText(const AText: string; const AFontName: string; const AFontSize: NativeInt; const AFixedWidth: NativeInt = 0): TSize; overload;
+function IdentEntry(Entry: Longint; out MapEntry: TIdentMapEntry): boolean;
+function ColorToIdent(Color: Longint; out Ident: String): Boolean;
+function IdentToColor(const Ident: string; out Color: Longint): Boolean;
+function ColorIndex(Color: Longint; out Index: Integer): Boolean;
 implementation
@@ -948,5 +952,108 @@ begin
   Result := TextExtent(AText).Cx;
 end;

+const
+ Colors: array[0..46] of TIdentMapEntry = (
+ // standard colors
+ (Value: clBlack; Name: 'clBlack'),
+ (Value: clMaroon; Name: 'clMaroon'),
+ (Value: clGreen; Name: 'clGreen'),
+ (Value: clOlive; Name: 'clOlive'),
+ (Value: clNavy; Name: 'clNavy'),
+ (Value: clPurple; Name: 'clPurple'),
+ (Value: clTeal; Name: 'clTeal'),
+ (Value: clGray; Name: 'clGray'),
+ (Value: clSilver; Name: 'clSilver'),
+ (Value: clRed; Name: 'clRed'),
+ (Value: clLime; Name: 'clLime'),
+ (Value: clYellow; Name: 'clYellow'),
+ (Value: clBlue; Name: 'clBlue'),
+ (Value: clFuchsia; Name: 'clFuchsia'),
+ (Value: clAqua; Name: 'clAqua'),
+ (Value: clWhite; Name: 'clWhite'),
+
+ // extended colors
+ (Value: clMoneyGreen; Name: 'clMoneyGreen'),
+ (Value: clSkyBlue; Name: 'clSkyBlue'),
+ (Value: clCream; Name: 'clCream'),
+ (Value: clMedGray; Name: 'clMedGray'),
+
+ // special colors
+ (Value: clNone; Name: 'clNone'),
+ (Value: clDefault; Name: 'clDefault'),
+
+ // system colors
+ (Value: clScrollBar; Name: 'clScrollBar'),
+ (Value: clBackground; Name: 'clBackground'),
+ (Value: clActiveCaption; Name: 'clActiveCaption'),
+ (Value: clInactiveCaption; Name: 'clInactiveCaption'),
+ (Value: clMenu; Name: 'clMenu'),
+ //(Value: clMenuBar; Name: 'clMenuBar'),
+ //(Value: clMenuHighlight; Name: 'clMenuHighlight'),
+ (Value: clMenuText; Name: 'clMenuText'),
+ (Value: clWindow; Name: 'clWindow'),
+ (Value: clWindowFrame; Name: 'clWindowFrame'),
+ (Value: clWindowText; Name: 'clWindowText'),
+ (Value: clCaptionText; Name: 'clCaptionText'),
+ (Value: clActiveBorder; Name: 'clActiveBorder'),
+ (Value: clInactiveBorder; Name: 'clInactiveBorder'),
+ (Value: clAppWorkspace; Name: 'clAppWorkspace'),
+ (Value: clHighlight; Name: 'clHighlight'),
+ (Value: clHighlightText; Name: 'clHighlightText'),
+ (Value: clBtnFace; Name: 'clBtnFace'),
+ (Value: clBtnShadow; Name: 'clBtnShadow'),
+ (Value: clGrayText; Name: 'clGrayText'),
+ (Value: clBtnText; Name: 'clBtnText'),
+ (Value: clInactiveCaptionText; Name: 'clInactiveCaptionText'),
+ (Value: clBtnHighlight; Name: 'clBtnHighlight'),
+ (Value: cl3DDkShadow; Name: 'cl3DDkShadow'),
+ (Value: cl3DLight; Name: 'cl3DLight'),
+ (Value: clInfoText; Name: 'clInfoText'),
+ (Value: clInfoBk; Name: 'clInfoBk')
+
+ //(Value: clHotLight; Name: 'clHotLight'),
+ //(Value: clGradientActiveCaption; Name: 'clGradientActiveCaption'),
+ //(Value: clGradientInactiveCaption; Name: 'clGradientInactiveCaption'), +
+ // one our special color
+ //(Value: clForm; Name: 'clForm')
+ );
+
+function IdentEntry(Entry: Longint; out MapEntry: TIdentMapEntry): boolean; +begin
+ Result := False;
+ if (Entry >= 0) and (Entry <= High(Colors)) then
+ begin
+ MapEntry := Colors[Entry];
+ Result := True;
+ end;
+end;
+
+function ColorToIdent(Color: Longint; out Ident: String): Boolean; +begin
+ Result := IntToIdent(Color, Ident, Colors);
+end;
+
+function IdentToColor(const Ident: string; out Color: Longint): Boolean; +begin
+ Result := IdentToInt(Ident, Color, Colors);
+end;
+
+function ColorIndex(Color: Longint; out Index: Integer): Boolean; +var
+ i: integer;
+begin
+ for i := Low(Colors) to High(Colors) do
+ if Colors[i].Value = Color then
+ begin
+ Result := True;
+ Index := i;
+ exit;
+ end;
+ Result := False;
+end;
+
+initialization
+ RegisterIntegerConsts(TypeInfo(TColor), TIdentToInt(@IdentToColor), TIntToIdent(@ColorToIdent)); end.

diff --git a/pas2js/packages/widget/lclstrconsts.pas b/pas2js/packages/widget/lclstrconsts.pas new file mode 100644
index 0000000..5869992
--- /dev/null
+++ b/pas2js/packages/widget/lclstrconsts.pas
@@ -0,0 +1,32 @@
+{
+ /***************************************************************************
+ lclstrconsts.pas
+ ----------------
+ This unit contains all resource strings of the LCL (not interfaces) +
+
+ ***************************************************************************/ +
+ *****************************************************************************
+ This file is part of the Lazarus Component Library (LCL) +
+ See the file COPYING.modifiedLGPL.txt, included in this distribution,
+ for details about the license.
+ ***************************************************************************** +}
+unit LCLStrConsts;
+
+{$mode objfpc}{$H+}
+
+interface
+
+resourcestring
+ rsFormResourceSNotFoundForResourcelessFormsCreateNew = 'Form resource %s '
+ +'not found. For resourceless forms CreateNew constructor must be used.'
+ +' See the global variable RequireDerivedFormResource.';
+ rsFormStreamingError = 'Form streaming "%s" error: %s'; +
+implementation
+
+end.
+
diff --git a/pas2js/packages/widget/lresources.pas b/pas2js/packages/widget/lresources.pas new file mode 100644
index 0000000..5d4c4a1
--- /dev/null
+++ b/pas2js/packages/widget/lresources.pas
@@ -0,0 +1,129 @@
+{
+ Note: This file is based on LCL's LResources unit. +
+ Original Author: Mattias Gaertner
+
+ *****************************************************************************
+ This file is part of the Lazarus Component Library (LCL) +
+ See the file COPYING.modifiedLGPL.txt, included in this distribution,
+ for details about the license.
+ ***************************************************************************** +
+ Abstract:
+ This unit maintains and stores all lazarus resources in the global list
+ named LazarusResources and provides methods and types to stream components. +
+ A lazarus resource is an ansistring, with a name and a valuetype. Both, name
+ and valuetype, are ansistrings as well.
+ Lazarus resources are normally included via an include directive in the
+ initialization part of a unit. To create such include files use the
+ BinaryToLazarusResourceCode procedure.
+ To create a LRS file from an LFM file use the LFMtoLRSfile function which
+ transforms the LFM text to binary format and stores it as Lazarus resource
+ include file.
+}
+unit LResources;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+ Classes;
+
+function InitResourceComponent(Instance: TComponent;
+ RootAncestor: TClass):Boolean;
+function InitLazResourceComponent(Instance: TComponent;
+ RootAncestor: TClass): Boolean; +
+implementation
+
+uses
+ Web, SysUtils,
+ LCLStrConsts;
+
+function InitResourceComponent(Instance: TComponent; RootAncestor: TClass
+ ): Boolean;
+begin
+ Result := InitLazResourceComponent(Instance, RootAncestor); +end;
+
+function InitLazResourceComponent(Instance: TComponent; RootAncestor: TClass
+ ): Boolean;
+
+ function InitComponent(ClassType: TClass): Boolean;
+ var
+ ResName: String;
+ Stream: TStream;
+ BinStream: TMemoryStream;
+ Reader: TReader;
+ script: TJSElement;
+ begin
+ //DebugLn(['[InitComponent] ClassType=',ClassType.Classname,' Instance=',DbgsName(Instance),' RootAncestor=',DbgsName(RootAncestor),' ClassType.ClassParent=',DbgsName(ClassType.ClassParent)]);
+ Result := False;
+ if (ClassType = TComponent) or (ClassType = RootAncestor) then
+ Exit;
+ if Assigned(ClassType.ClassParent) then
+ Result := InitComponent(ClassType.ClassParent); +
+ Stream := nil;
+ ResName := ClassType.ClassName;
+
+ script := Document.getElementById(ResName + '.lfm');
+ if Assigned(script) and (script.textContent '') then
+ Stream := TStringStream.Create(script.textContent); +
+ if Stream = nil then
+ Exit;
+
+ try
+ //DebugLn('Form Stream "',ClassType.ClassName,'"');
+ try
+ BinStream := TMemoryStream.Create;
+ try
+ ObjectTextToBinary(Stream, BinStream);
+
+ BinStream.Position := 0;
+
+ Reader := TReader.Create(BinStream);
+ try
+ Reader.ReadRootComponent(Instance);
+ finally
+ Reader.Free;
+ end;
+ finally
+ BinStream.Free;
+ end;
+ except
+ on E: Exception do begin
+ Writeln(Format(rsFormStreamingError,[ClassType.ClassName,E.Message]));
+ raise;//exit;
+ end;
+ end;
+ finally
+ Stream.Free;
+ end;
+ Result := True;
+ end;
+
+
+begin
+ if Instance.ComponentState * [csLoading, csInline] []
+ then begin
+ // global loading not needed
+ Result := InitComponent(Instance.ClassType);
+ end
+ else try
+ //BeginGlobalLoading;
+ Result := InitComponent(Instance.ClassType);
+ //NotifyGlobalLoading;
+ finally
+ //EndGlobalLoading;
+ end;
+end;
+
+initialization
+ RegisterInitComponentHandler(TComponent, @InitResourceComponent); +end.
+
diff --git a/pas2js/packages/widget/pas2js_widget.lpk b/pas2js/packages/widget/pas2js_widget.lpk index b98a54a..180af11 100755
--- a/pas2js/packages/widget/pas2js_widget.lpk
+++ b/pas2js/packages/widget/pas2js_widget.lpk
@@ -60,7 +60,7 @@
SOFTWARE.
} "/>
<Version Major="1"/>
- <Files Count="15">
+ <Files Count="18">
<Item1>
<Filename Value="webextra.pas"/>
<UnitName Value="WebExtra"/>
@@ -121,6 +121,18 @@
<Filename Value="webctrls.pas"/>
<UnitName Value="WebCtrls"/>
</Item15>
+ <Item16>
+ <Filename Value="lclstrconsts.pas"/>
+ <UnitName Value="lclstrconsts"/>
+ </Item16>
+ <Item17>
+ <Filename Value="formloader.pas"/>
+ <UnitName Value="FormLoader"/>
+ </Item17>
+ <Item18>
+ <Filename Value="lresources.pas"/>
+ <UnitName Value="LResources"/>
+ </Item18>
</Files>
<RequiredPkgs Count="1">
<Item1>
diff --git a/pas2js/packages/widget/webctrls.pas b/pas2js/packages/widget/webctrls.pas index 3e70bf7..d6aab47 100755
--- a/pas2js/packages/widget/webctrls.pas
+++ b/pas2js/packages/widget/webctrls.pas
@@ -44,49 +44,6 @@ uses

 type
-  { TWForm }
-
-  TWForm = class(TCustomForm)
-  published
-    property ActiveControl;
-    property Align;
-    property AlphaBlend;
-    property AlphaBlendValue;
-    property Caption;
-    property ClientHeight;
-    property ClientWidth;
-    property Color;
-    property Enabled;
-    property Font;
-    property HandleClass;
-    property HandleID;
-    property KeyPreview;
-    property ShowHint;
-    property Visible;
-    property OnActivate;
-    property OnClick;
-    property OnClose;
-    property OnCloseQuery;
-    property OnCreate;
-    property OnDblClick;
-    property OnDeactivate;
-    property OnDestroy;
-    property OnHide;
-    property OnKeyDown;
-    property OnKeyPress;
-    property OnKeyUp;
-    property OnMouseDown;
-    property OnMouseEnter;
-    property OnMouseLeave;
-    property OnMouseMove;
-    property OnMouseUp;
-    property OnMouseWheel;
-    property OnResize;
-    property OnScroll;
-    property OnShow;
-  end;
-  TWFormClass = class of TWForm;
-
   { TWFrame }

TWFrame = class(TCustomFrame)
-------------- next part --------------
A non-text attachment was scrubbed...
Name: streaming.png
Type: image/png
Size: 217641 bytes
Desc: not available
URL: <http://lists.freepascal.org/pipermail/pas2js/attachments/20190726/40baa152/attachment-0001.png>

Reply by email

Michael Van Canneyt
to Sven Barth
Sven Barth wrote 8 lines
Hello together! I've experimented a bit with Pas2JS and the Pas2JS-Widgets by heliosroots ( https://github.com/heliosroots/Pas2JS_Widget/tree/master ) and implemented loading of unmodified LFM files. It's a bit cumbersome for now, cause it needs changes in the project's HTML source as well as the main project file for each form added, but as a proof of concept it works. I've attached the patch that needs to be applied to the Pas2JS-Widget master.

Impressive :-)

Show quoted text 34 lines
To use it you proceed as follows: - make sure you have installed Pas2JSDsgn (from Pas2JS) and Pas2JS_Designer_Package (from Pas2JS-Widgets) - make sure you at least once opened Pas2JS_RTL (from Pas2JS) and Pas2JS_Widget (from Pas2JS-Widgets) - create a new "Web Browser Application" - add Pas2JS_RTL and Pas2JS_Widget as requirements - add new forms using New -> Module -> Web Form (Pas2JS) - remove the overridden Loaded method from any forms - replace the main program's code with this (of course adjust the form/unit names to your needs): === code begin === program tp2j; {$mode objfpc} uses   Forms, FormLoader, Interfaces, Unit1; procedure DoInit(aData: TObject); begin   Application.Initialize;   Application.CreateForm(TWForm1, WForm1);   Application.Run; end; begin   LoadForms([TWForm1], @DoInit, Nil); end. === code end ===

I think you can avoid this change by checking the
'Run rtl when all page resources are fully loaded' options when creating your project.

This will change the script tag to

   window.addEventListener("load", rtl.run);

so the program only runs when all forms are actually loaded, avoiding the need to wait for the load.

The changes to the html could be implemented without too much effort in the IDE package.

Another approach would be to simply embed the .lfm in the html page in a script tag.

Show quoted text 13 lines
- add a script tag in front of the project's script tag in the HTML file:   <script src="unit1.lfm" type="application/x-lazarus-form" id="TWForm1.lfm"></script> - play around with the form (add components (from the Pas2JS component tab only!), add events, etc.) - before compiling make sure that the project *does not* contain a requirement of Pas2JS_Designer_Package (the IDE currently adds this when adding a component) - make sure that the *.LFM files are at the same location as the *.JS file - load your application in the browser -> you should now see your form A screenshot showing the resulting browser and the IDE is attached. :)

Nice job :-)

Show quoted text 6 lines
This can obviously be further improved: - adding support also for frames and data modules - caching the converted object stream (so that it doesn't need to be converted for each creation of a form) - add some way of asynchronous loading?

There is the dynload unit which can help in this.

Nice ! slowly we go ahead. And more surprises are still coming... :-)

Michael.

Reply by email

Sven Barth
to Michael Van Canneyt
Michael Van Canneyt wrote 16 lines
On Fri, 26 Jul 2019, Sven Barth via Pas2js wrote: > Hello together! > > I've experimented a bit with Pas2JS and the Pas2JS-Widgets by > heliosroots ( > https://github.com/heliosroots/Pas2JS_Widget/tree/master ) and > implemented loading of unmodified LFM files. It's a bit cumbersome > for now, cause it needs changes in the project's HTML source as well > as the main project file for each form added, but as a proof of > concept it works. I've attached the patch that needs to be applied to > the Pas2JS-Widget master. Impressive :-)

Thanks, though without your streaming implementation it would not have been possible that easily. ;)

Show quoted text 43 lines
> > To use it you proceed as follows: > - make sure you have installed Pas2JSDsgn (from Pas2JS) and > Pas2JS_Designer_Package (from Pas2JS-Widgets) > - make sure you at least once opened Pas2JS_RTL (from Pas2JS) and > Pas2JS_Widget (from Pas2JS-Widgets) > - create a new "Web Browser Application" > - add Pas2JS_RTL and Pas2JS_Widget as requirements > - add new forms using New -> Module -> Web Form (Pas2JS) > - remove the overridden Loaded method from any forms > - replace the main program's code with this (of course adjust the > form/unit names to your needs): > > === code begin === > > program tp2j; > > {$mode objfpc} > > uses >   Forms, FormLoader, Interfaces, Unit1; > > procedure DoInit(aData: TObject); > begin >   Application.Initialize; >   Application.CreateForm(TWForm1, WForm1); >   Application.Run; > end; > > begin >   LoadForms([TWForm1], @DoInit, Nil); > end. > > === code end === I think you can avoid this change by checking the 'Run rtl when all page resources are fully loaded' options when creating your project. This will change the script tag to   window.addEventListener("load", rtl.run); so the program only runs when all forms are actually loaded, avoiding the need to wait for the load.

Sadly this does not work. I don't even see the browser fetching the file... :'(

Show quoted text 6 lines
The changes to the html could be implemented without too much effort in the IDE package. Another approach would be to simply embed the .lfm in the html page in a script tag.

I personally would prefer to keep them separated to make versioning easier.

However what might be interesting (even for further functionality) might be to run the HTML page through a template processor (*cough* fptemplate *cough*) and replace some tag with the contents of all forms. Or something like that...

Show quoted text 17 lines
> > - add a script tag in front of the project's script tag in the HTML > file:   <script src="unit1.lfm" type="application/x-lazarus-form" > id="TWForm1.lfm"></script> > - play around with the form (add components (from the Pas2JS > component tab only!), add events, etc.) > - before compiling make sure that the project *does not* contain a > requirement of Pas2JS_Designer_Package (the IDE currently adds this > when adding a component) > - make sure that the *.LFM files are at the same location as the *.JS > file > - load your application in the browser -> you should now see your form > > A screenshot showing the resulting browser and the IDE is attached. :) Nice job :-)

Yes. Though I'd really like to know how TMS dealt with the sizes. Currently the problem is when I do this on a 92 DPI Lazarus (my Win7 at work) I get buttons and labels that are too small for their contents and on my HighDPI Win10 the button and label looks huge compared to the content... (obviously the widgets need to deal with the different sizes/take into account the design DPI, but the principal problem of WYSIWYG remains)

Show quoted text 9 lines
> > This can obviously be further improved: > - adding support also for frames and data modules > - caching the converted object stream (so that it doesn't need to be > converted for each creation of a form) > - add some way of asynchronous loading? There is the dynload unit which can help in this.

I don't see a DynLoad unit. You mean Rtl.ScriptLoader and/or Rtl.UnitLoader? It's the former my FormLoader unit is based on. But loading the content itself is not the problem (I've done that part already ;) ). My (conceptual) problem is that the code is usually synchronous. E.g. take the default main program:

=== code begin ===

begin
   Application.Initialize;
   Application.CreateForm(TWForm1, WForm1);
   Application.Run;
end.

=== code end ===

Or the way to show another form:

=== code begin ===

SomeVar := TWForm2.Create(Application);
SomeVar.Show;

=== code end ===

In both cases it makes no sense to continue with the code after CreateForm or Create as the resource must be there to show something useful. Currently the loading inside TCustomForm looks like this:

=== code begin ===

procedure TCustomForm.ProcessResource;
begin
   if not InitResourceComponent(Self, TWForm) then
     raise EResNotFound.CreateFmt(
       rsFormResourceSNotFoundForResourcelessFormsCreateNew, [ClassName]);
end;
constructor TCustomForm.Create(AOwner: TComponent);
begin
   //GlobalNameSpace.BeginWrite;
   try
     CreateNew(AOwner, 1); // this calls BeginFormUpdate, which is ended
in AfterConstruction
     if (ClassType  TWForm) and not (csDesigning in ComponentState) then
     begin
       //Include(FFormState, fsCreating);
       try
         ProcessResource;
       finally
         //Exclude(FFormState, fsCreating);
       end;
     end;
   finally
     //GlobalNameSpace.EndWrite;
   end;
end;

=== code end ===

And InitResourceComponent is this:

=== code begin ===

function InitResourceComponent(Instance: TComponent; RootAncestor: TClass
   ): Boolean;
begin
   Result := InitLazResourceComponent(Instance, RootAncestor);
end;
function InitLazResourceComponent(Instance: TComponent; RootAncestor: TClass
   ): Boolean;
   function InitComponent(ClassType: TClass): Boolean;
   var
     ResName: String;
     Stream: TStream;
     BinStream: TMemoryStream;
     Reader: TReader;
     script: TJSElement;
   begin
     //DebugLn(['[InitComponent] ClassType=',ClassType.Classname,'
Instance=',DbgsName(Instance),' RootAncestor=',DbgsName(RootAncestor),'
ClassType.ClassParent=',DbgsName(ClassType.ClassParent)]);
     Result := False;
     if (ClassType = TComponent) or (ClassType = RootAncestor) then
       Exit;
     if Assigned(ClassType.ClassParent) then
       Result := InitComponent(ClassType.ClassParent);
     Stream := nil;
     ResName := ClassType.ClassName;
     (*************)
     script := Document.getElementById(ResName + '.lfm');
     if Assigned(script) and (script.textContent  '') then
       Stream := TStringStream.Create(script.textContent);
     if Stream = nil then
       Exit;
     try
       //DebugLn('Form Stream "',ClassType.ClassName,'"');
       try
       BinStream := TMemoryStream.Create;
       try
         ObjectTextToBinary(Stream, BinStream);
         BinStream.Position := 0;
         Reader := TReader.Create(BinStream);
         try
           Reader.ReadRootComponent(Instance);
         finally
           Reader.Free;
         end;
       finally
         BinStream.Free;
       end;
       except
         on E: Exception do begin
Writeln(Format(rsFormStreamingError,[ClassType.ClassName,E.Message]));
           raise;//exit;
         end;
       end;
     finally
       Stream.Free;
     end;
     Result := True;
   end;
begin
   if Instance.ComponentState * [csLoading, csInline]  []
   then begin
     // global loading not needed
     Result := InitComponent(Instance.ClassType);
   end
   else try
     //BeginGlobalLoading;
     Result := InitComponent(Instance.ClassType);
     //NotifyGlobalLoading;
   finally
     //EndGlobalLoading;
   end;
end;

=== code end ===

In the block marked with (*************) I'd essentially need to load the form's resource dynamically from the server (the loading itself isn't the problem) and then wait until all has been received. My knowledge in JS isn't as good yet as to know the correct approach for that, my gut tells me that something akin to "async/await" would be necessary here.

Nice ! slowly we go ahead. And more surprises are still coming... :-)

Definitely looking forward to that. :)

Regards,
Sven

Reply by email

Michael Van Canneyt
to Sven Barth
Sven Barth wrote 20 lines
Am 27.07.2019 um 09:33 schrieb Michael Van Canneyt: > > > On Fri, 26 Jul 2019, Sven Barth via Pas2js wrote: > >> Hello together! >> >> I've experimented a bit with Pas2JS and the Pas2JS-Widgets by >> heliosroots ( >> https://github.com/heliosroots/Pas2JS_Widget/tree/master ) and >> implemented loading of unmodified LFM files. It's a bit cumbersome >> for now, cause it needs changes in the project's HTML source as well >> as the main project file for each form added, but as a proof of >> concept it works. I've attached the patch that needs to be applied to >> the Pas2JS-Widget master. > > Impressive :-) Thanks, though without your streaming implementation it would not have been possible that easily. ;)

Well, with small steps we go ahead, each contributing where he can and wishes...

Show quoted text 30 lines
>> program tp2j; >> >> {$mode objfpc} >> >> uses >>   Forms, FormLoader, Interfaces, Unit1; >> >> procedure DoInit(aData: TObject); >> begin >>   Application.Initialize; >>   Application.CreateForm(TWForm1, WForm1); >>   Application.Run; >> end; >> >> begin >>   LoadForms([TWForm1], @DoInit, Nil); >> end. >> >> === code end === > > I think you can avoid this change by checking the 'Run rtl when all > page resources are fully loaded' options when creating your project. > > This will change the script tag to >   window.addEventListener("load", rtl.run); > so the program only runs when all forms are actually loaded, avoiding the > need to wait for the load. Sadly this does not work. I don't even see the browser fetching the file... :'(

Strange.
Probably because it does not recognize the type so it refuses to load it automatically. Maybe a <link> element is better ?

Show quoted text 9 lines
> > The changes to the html could be implemented without too much effort > in the IDE package. > > Another approach would be to simply embed the .lfm in the html page in a > script tag. I personally would prefer to keep them separated to make versioning easier.

I understand this.
Both approaches have their use.

Show quoted text 5 lines
However what might be interesting (even for further functionality) might be to run the HTML page through a template processor (*cough* fptemplate *cough*) and replace some tag with the contents of all forms. Or something like that...

I had the exact same thought when I wrote

"The changes to the html could be implemented without too much effort in the IDE package."

:-)

Show quoted text 23 lines
>> - add a script tag in front of the project's script tag in the HTML >> file:   <script src="unit1.lfm" type="application/x-lazarus-form" >> id="TWForm1.lfm"></script> >> - play around with the form (add components (from the Pas2JS >> component tab only!), add events, etc.) >> - before compiling make sure that the project *does not* contain a >> requirement of Pas2JS_Designer_Package (the IDE currently adds this >> when adding a component) >> - make sure that the *.LFM files are at the same location as the *.JS >> file >> - load your application in the browser -> you should now see your form >> >> A screenshot showing the resulting browser and the IDE is attached. :) > > Nice job :-) Yes. Though I'd really like to know how TMS dealt with the sizes. Currently the problem is when I do this on a 92 DPI Lazarus (my Win7 at work) I get buttons and labels that are too small for their contents and on my HighDPI Win10 the button and label looks huge compared to the content... (obviously the widgets need to deal with the different sizes/take into account the design DPI, but the principal problem of WYSIWYG remains)

This is the problem of VCL/LCL : an outdated concept of (fixed) layout.

Show quoted text 10 lines
>> This can obviously be further improved: >> - adding support also for frames and data modules >> - caching the converted object stream (so that it doesn't need to be >> converted for each creation of a form) >> - add some way of asynchronous loading? > > There is the dynload unit which can help in this. I don't see a DynLoad unit. You mean Rtl.ScriptLoader and/or Rtl.UnitLoader?

Sorry, yes. The name changed after I did the initial implementation, and the old name is still in my head, sorry :(

Show quoted text 5 lines
It's the former my FormLoader unit is based on. But loading the content itself is not the problem (I've done that part already ;) ). My (conceptual) problem is that the code is usually synchronous. E.g. take the default main program:

[snip]

Show quoted text 7 lines
In the block marked with (*************) I'd essentially need to load the form's resource dynamically from the server (the loading itself isn't the problem) and then wait until all has been received. My knowledge in JS isn't as good yet as to know the correct approach for that, my gut tells me that something akin to "async/await" would be necessary here.

You can't run await in the main loop so that would not work as you want it. There are no shortcuts for async, it is something to live with.

In essence, TApplication.CreateForm() will need to be async.

so a

TFormLoadedEvent = Procedure(Sender : TObject; aForm : TCustomForm) of Object;

Property Tapplication.OnFormLoaded : TFormLoadedEvent

is needed, allowing you to check that the form is loaded.

TApplication.Run then needs to wait till the main form is loaded before showing it.

Michael.

Reply by email

Sven Barth
to Michael Van Canneyt
Michael Van Canneyt wrote 35 lines
>>> program tp2j; >>> >>> {$mode objfpc} >>> >>> uses >>>   Forms, FormLoader, Interfaces, Unit1; >>> >>> procedure DoInit(aData: TObject); >>> begin >>>   Application.Initialize; >>>   Application.CreateForm(TWForm1, WForm1); >>>   Application.Run; >>> end; >>> >>> begin >>>   LoadForms([TWForm1], @DoInit, Nil); >>> end. >>> >>> === code end === >> >> I think you can avoid this change by checking the 'Run rtl when all >> page resources are fully loaded' options when creating your project. >> >> This will change the script tag to >>   window.addEventListener("load", rtl.run); >> so the program only runs when all forms are actually loaded, >> avoiding the >> need to wait for the load. > > Sadly this does not work. I don't even see the browser fetching the > file... :'( Strange. Probably because it does not recognize the type so it refuses to load it automatically.

That's what I assume as well.

Maybe a <link> element is better ?

Nope, no difference. It seems injecting directly into the HTML or dynamic loading are the only possibilities if I want to use the LFM format as is.

Show quoted text 13 lines
> > However what might be interesting (even for further functionality) > might be to run the HTML page through a template processor (*cough* > fptemplate *cough*) and replace some tag with the contents of all > forms. Or something like that... I had the exact same thought when I wrote "The changes to the html could be implemented without too much effort in the IDE package." :-)

;)

Show quoted text 26 lines
>>> - add a script tag in front of the project's script tag in the HTML >>> file:   <script src="unit1.lfm" type="application/x-lazarus-form" >>> id="TWForm1.lfm"></script> >>> - play around with the form (add components (from the Pas2JS >>> component tab only!), add events, etc.) >>> - before compiling make sure that the project *does not* contain a >>> requirement of Pas2JS_Designer_Package (the IDE currently adds this >>> when adding a component) >>> - make sure that the *.LFM files are at the same location as the >>> *.JS file >>> - load your application in the browser -> you should now see your form >>> >>> A screenshot showing the resulting browser and the IDE is attached. :) >> >> Nice job :-) > > Yes. Though I'd really like to know how TMS dealt with the sizes. > Currently the problem is when I do this on a 92 DPI Lazarus (my Win7 > at work) I get buttons and labels that are too small for their > contents and on my HighDPI Win10 the button and label looks huge > compared to the content... (obviously the widgets need to deal with > the different sizes/take into account the design DPI, but the > principal problem of WYSIWYG remains) This is the problem of VCL/LCL : an outdated concept of (fixed) layout.

Which brings me back to the question: how did TMS solve this? After all they're using the normal form designer as well...

Show quoted text 31 lines
> It's the former my FormLoader unit is based on. But loading the > content itself is not the problem (I've done that part already ;) ). > My (conceptual) problem is that the code is usually synchronous. E.g. > take the default main program: >  [snip] > > In the block marked with (*************) I'd essentially need to load > the form's resource dynamically from the server (the loading itself > isn't the problem) and then wait until all has been received. My > knowledge in JS isn't as good yet as to know the correct approach for > that, my gut tells me that something akin to "async/await" would be > necessary here. You can't run await in the main loop so that would not work as you want it. There are no shortcuts for async, it is something to live with. In essence, TApplication.CreateForm() will need to be async. so a TFormLoadedEvent = Procedure(Sender : TObject; aForm : TCustomForm) of Object; Property Tapplication.OnFormLoaded : TFormLoadedEvent is needed, allowing you to check that the form is loaded. TApplication.Run then needs to wait till the main form is loaded before showing it.

But how can TApplication.Run wait when waiting in the main thread is frowned upon or not really working?

Though I just noticed that TApplication.Run looks like this:

=== code begin ===

procedure TApplication.Run;
begin
   { TODO: Add a background to the application }
   RegisterHandleEvents;
   LoadIcon;
   if (Assigned(FMainForm)) then
   begin
     FMainForm.Show;
   end;
end;

=== code end ===

So it might be useful to kick out the if-clause and only rely on asynchronous loading.

Of course for creating forms in another form one would need to call TApplication.CreateForm as well instead of calling TSomeForm.Create if one wants to use a form with a LFM. Then TApplication.CreateForm would need a callback parameter which would contain the code after the form was successfully instantiated...

Maybe I can cook something up with that...

Of course there is more trouble ahead if a form depends e.g. on a data module and thus the latter must be loaded before the former... Oh well, no one said that it would be easy...

Regards,
Sven

Reply by email

Michael Van Canneyt
to Sven Barth
Sven Barth wrote 6 lines
That's what I assume as well. > Maybe a <link> element is better ? Nope, no difference. It seems injecting directly into the HTML or dynamic loading are the only possibilities if I want to use the LFM format as is.

Looks like it, yes :(

My personal preference would be a data-formdata-formname attribute, and base64-encode the LFM.

Show quoted text 12 lines
>> Yes. Though I'd really like to know how TMS dealt with the sizes. >> Currently the problem is when I do this on a 92 DPI Lazarus (my Win7 at >> work) I get buttons and labels that are too small for their contents and >> on my HighDPI Win10 the button and label looks huge compared to the >> content... (obviously the widgets need to deal with the different >> sizes/take into account the design DPI, but the principal problem of >> WYSIWYG remains) > > This is the problem of VCL/LCL : an outdated concept of (fixed) layout. Which brings me back to the question: how did TMS solve this? After all they're using the normal form designer as well...

I have no idea. Maybe they didn't solve it yet.

Show quoted text 25 lines
> TApplication.Run then needs to wait till the main form is loaded before > showing it. But how can TApplication.Run wait when waiting in the main thread is frowned upon or not really working? Though I just noticed that TApplication.Run looks like this: === code begin === procedure TApplication.Run; begin   { TODO: Add a background to the application }   RegisterHandleEvents;   LoadIcon;   if (Assigned(FMainForm)) then   begin     FMainForm.Show;   end; end; === code end === So it might be useful to kick out the if-clause and only rely on asynchronous loading.

Exactly what I was getting at. And if it works asynchronously, it can work synchronously as well.

Of course there is more trouble ahead if a form depends e.g. on a data module and thus the latter must be loaded before the former... Oh well, no one said that it would be easy...

No, it is not.
That is why the rtl.unitloader does a dependency check and recursive loading...

Michael.

Reply by email