Browse Source

inspector: started css rules inspector

mattias 3 weeks ago
parent
commit
4fd43bac8f

+ 2 - 0
demo/Inspector/.gitignore

@@ -0,0 +1,2 @@
+InspectorDemo
+InspectorDemo.app

+ 8 - 0
demo/Inspector/InspDemoForm1.lfm

@@ -0,0 +1,8 @@
+object DemoForm1: TDemoForm1
+  FormLeft = 168
+  FormTop = 236
+  FormWidth = 320
+  FormHeight = 600
+  OnCreate = DemoForm1Create
+  Visible = true
+end

+ 58 - 0
demo/Inspector/InspDemoForm1.pas

@@ -0,0 +1,58 @@
+unit InspDemoForm1;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, Fresnel.Forms, Fresnel.DOM, Fresnel.Controls;
+
+type
+
+  { TDemoForm1 }
+
+  TDemoForm1 = class(TFresnelForm)
+    procedure DemoForm1Create(Sender: TObject);
+  private
+  public
+    Body: TBody;
+    Div1: TDiv;
+    Label1: TLabel;
+  end;
+
+var
+  DemoForm1: TDemoForm1;
+
+implementation
+
+{$R *.lfm}
+
+{ TDemoForm1 }
+
+procedure TDemoForm1.DemoForm1Create(Sender: TObject);
+begin
+  Body:=TBody.Create(Self);
+  with Body do begin
+    Name:='Body';
+    Parent:=Self;
+  end;
+
+  Div1:=TDiv.Create(Self);
+  with Div1 do begin
+    Name:='Div1';
+    Parent:=Body;
+    Style:='Height: 100px; padding-left: 5px;';
+  end;
+
+  Label1:=TLabel.Create(Self);
+  with Label1 do begin
+    Name:='Label1';
+    Caption:='Hello';
+    Parent:=Div1;
+  end;
+
+  Stylesheet.Text:='div { padding: 2px; border: 3px solid blue; margin: 6px; unknown: none; right: bla; }';
+end;
+
+end.
+

+ 8 - 0
demo/Inspector/InspDemoStyles1.lfm

@@ -0,0 +1,8 @@
+object DemoStylesWindow: TDemoStylesWindow
+  FormLeft = 500
+  FormTop = 236
+  FormWidth = 320
+  FormHeight = 600
+  OnCreate = DemoStylesWindowCreate
+  Visible = true
+end

+ 51 - 0
demo/Inspector/InspDemoStyles1.pas

@@ -0,0 +1,51 @@
+unit InspDemoStyles1;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, Fresnel.Forms, Fresnel.DOM, Fresnel.Controls, Fresnel.CSSStyleInspector;
+
+type
+
+  { TDemoStylesWindow }
+
+  TDemoStylesWindow = class(TFresnelForm)
+    procedure DemoStylesWindowCreate(Sender: TObject);
+  private
+    FBody: TBody;
+    FCSSStyleInspector: TCSSStyleInspector;
+  public
+    property Body: TBody read FBody;
+    property CSSStyleInspector: TCSSStyleInspector read FCSSStyleInspector;
+  end;
+
+var
+  DemoStylesWindow: TDemoStylesWindow;
+
+implementation
+
+{$R *.lfm}
+
+{ TDemoForm1 }
+
+procedure TDemoStylesWindow.DemoStylesWindowCreate(Sender: TObject);
+begin
+  FBody:=TBody.Create(Self);
+  with Body do begin
+    Name:='Body';
+    Parent:=Self;
+  end;
+
+  FCSSStyleInspector:=TCSSStyleInspector.Create(Self);
+  with CSSStyleInspector do begin
+    Name:='CSSStyleInspector';
+    Parent:=Body;
+  end;
+
+  Stylesheet.Text:='div { padding: 2px; border: 3px solid blue; margin: 6px; }';
+end;
+
+end.
+

BIN
demo/Inspector/InspectorDemo.ico


+ 91 - 0
demo/Inspector/InspectorDemo.lpi

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="Button Generator"/>
+      <ResourceType Value="res"/>
+      <UseXPManifest Value="True"/>
+      <Icon Value="0"/>
+    </General>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <RequiredPackages>
+      <Item>
+        <PackageName Value="FresnelDemoComps"/>
+        <DefaultFilename Value="../democomps/fresneldemocomps.lpk" Prefer="True"/>
+      </Item>
+      <Item>
+        <PackageName Value="Fresnel"/>
+        <DefaultFilename Value="../../src/fresnel.lpk" Prefer="True"/>
+      </Item>
+    </RequiredPackages>
+    <Units>
+      <Unit>
+        <Filename Value="InspectorDemo.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="InspDemoForm1.pas"/>
+        <IsPartOfProject Value="True"/>
+        <ComponentName Value="FresnelForm1"/>
+        <ResourceBaseClass Value="Other"/>
+        <ResourceBaseClassname Value="TFresnelForm"/>
+      </Unit>
+      <Unit>
+        <Filename Value="InspDemoStyles1.pas"/>
+        <IsPartOfProject Value="True"/>
+        <HasResources Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="fresnel.cssstyleinspector.pas"/>
+        <IsPartOfProject Value="True"/>
+        <UnitName Value="Fresnel.CSSStyleInspector"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="InspectorDemo"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+    <Linking>
+      <Options>
+        <Win32>
+          <GraphicApplication Value="True"/>
+        </Win32>
+      </Options>
+    </Linking>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 25 - 0
demo/Inspector/InspectorDemo.lpr

@@ -0,0 +1,25 @@
+program InspectorDemo;
+
+{$mode objfpc}{$H+}
+
+{$IFDEF Windows}
+  {$AppType Console}
+{$ENDIF}
+
+uses
+  {$IFDEF UNIX}
+  cthreads,
+  {$ENDIF}
+  Fresnel, // this includes the Fresnel widgetset
+  Fresnel.Forms, InspDemoForm1, InspDemoStyles1;
+
+{$R *.res}
+
+begin
+  Application.Initialize;
+  Application.CreateForm(TDemoStylesWindow, DemoStylesWindow);
+  Application.CreateForm(TDemoForm1, DemoForm1);
+  DemoStylesWindow.CSSStyleInspector.Target:=DemoForm1.Div1;
+  Application.Run;
+end.
+

BIN
demo/Inspector/InspectorDemo.res


+ 318 - 0
demo/Inspector/fresnel.cssstyleinspector.pas

@@ -0,0 +1,318 @@
+unit Fresnel.CSSStyleInspector;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpCSSTree, fpCSSResParser, fpCSSResolver, Fresnel.DOM, Fresnel.Classes,
+  Fresnel.Controls, Fresnel.Events, FCL.Events;
+
+type
+
+  { TCSSStyleInspector }
+
+  TCSSStyleInspector = class(TDiv)
+  private
+    FRulesDiv: TDiv;
+    FLock: integer;
+    FInspectNeeded: boolean;
+    FTarget: TFresnelElement;
+    procedure OnTargetDomChanged(Event: TAbstractEvent);
+    procedure SetTarget(const AValue: TFresnelElement);
+  protected
+    procedure Inspect;
+    procedure AddRule(Row: integer; Selectors, Origin: TCSSString; RuleEl: TCSSRuleElement);
+    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
+  public
+    const
+      RulesDivClass = 'CSSStyleInspRules';
+      RuleDivClass = 'CSSStyleInspRule';
+      RuleHeaderDivClass = 'CSSStyleInspHeader';
+      RuleSelectorLabelClass = 'CSSStyleInspSelector';
+      RuleBracketLabelClass = 'CSSStyleInspRuleBracket';
+      RuleOriginLabelClass = 'CSSStyleInspOrigin';
+      RuleFooterDivClass = 'CSSStyleInspFooter';
+  public
+    constructor Create(AOwner: TComponent); override;
+    destructor Destroy; override;
+    class function GetCSSTypeStyle: TCSSString; override;
+    procedure BeginUpdate;
+    procedure EndUpdate;
+    property Target: TFresnelElement read FTarget write SetTarget;
+    property RulesDiv: TDiv read FRulesDiv;
+  end;
+
+implementation
+
+{ TCSSStyleInspector }
+
+procedure TCSSStyleInspector.SetTarget(const AValue: TFresnelElement);
+var
+  VP: TFresnelViewport;
+begin
+  if FTarget=AValue then Exit;
+  FTarget:=AValue;
+  if FTarget<>nil then
+  begin
+    if (FTarget=Self) or Contains(FTarget) then
+      raise EFresnel.Create('20250912162555');
+    FreeNotification(FTarget);
+
+    VP:=Target.Viewport;
+    if VP<>nil then
+    begin
+      VP.AddEventListener(evtViewportCSSApplied,@OnTargetDomChanged);
+      if VP.Rendered then
+        Inspect;
+    end;
+  end;
+end;
+
+procedure TCSSStyleInspector.OnTargetDomChanged(Event: TAbstractEvent);
+begin
+  writeln('TCSSStyleInspector.OnTargetDomChanged ',Event.Sender<>nil);
+  Inspect;
+end;
+
+procedure TCSSStyleInspector.Inspect;
+var
+  VP: TFresnelViewport;
+  Rules: TCSSSharedRuleList;
+  aTargetValues: TCSSAttributeValues;
+  aRule: TCSSSharedRule;
+  RuleEl: TCSSRuleElement;
+  i, j, Row: Integer;
+  aStyleSheet: TCSSResolver.TStyleSheet;
+  RuleOrigin, Selectors: TCSSString;
+  aSelector: TCSSElement;
+begin
+  if csDestroying in ComponentState then
+    exit;
+  if FLock>0 then
+  begin
+    FInspectNeeded:=true;
+    exit;
+  end;
+  FInspectNeeded:=false;
+
+  if (Target=nil) or (Target=Self) or Contains(Target) then
+  begin
+    // clear
+    exit;
+  end;
+
+  writeln('TCSSStyleInspector.Inspect ',Target.GetPath);
+
+  VP:=Target.Viewport;
+
+  aTargetValues:=TCSSAttributeValues.Create;
+  try
+    VP.Resolver.Compute(Target,Target.StyleElement,Rules,aTargetValues);
+
+    Row:=0;
+
+    // Element.Style aka inline style
+    AddRule(Row,'Element.Style','',Target.StyleElement);
+    inc(Row);
+
+    // CSS rules
+    for i:=length(Rules.Rules)-1 downto 0 do
+    begin
+      aRule:=Rules.Rules[i];
+      RuleEl:=aRule.Rule;
+
+      // find source
+      RuleOrigin:=RuleEl.SourceFileName;
+      aStyleSheet:=VP.Resolver.FindStyleSheetWithElement(RuleEl);
+      if aStyleSheet<>nil then
+      begin
+        if aStyleSheet.Origin=cssoUserAgent then begin
+          RuleOrigin:=aStyleSheet.Name; // usually the classname
+        end else if aStyleSheet.Origin=cssoAuthor then begin
+          // stylesheet of viewport aka form
+          RuleOrigin:=VP.Name;
+        end else begin
+          writeln('TCSSStyleInspector.Inspect unknown stylesheet ',aStyleSheet.Origin,' Name=',aStyleSheet.Name);
+          // todo: check application stylesheets
+        end;
+      end;
+      RuleOrigin+='('+IntToStr(RuleEl.SourceRow)+','+IntToStr(RuleEl.SourceCol)+')';
+
+      Selectors:='';
+      for j:=0 to RuleEl.SelectorCount-1 do
+      begin
+        aSelector:=RuleEl.Selectors[j];
+        if Selectors>'' then Selectors+=', ';
+        Selectors+=aSelector.AsFormattedString;
+      end;
+      writeln('TCSSStyleInspector.Inspect ',i,' Spec=',aRule.Specificity,' Selector="',Selectors,'" Src=',RuleOrigin);
+
+      AddRule(Row,Selectors,RuleOrigin,RuleEl);
+      inc(Row);
+    end;
+
+    // delete old nodes
+    while RulesDiv.NodeCount>Row do
+      RulesDiv.Nodes[RulesDiv.NodeCount-1].Free;
+  finally
+    aTargetValues.Free;
+  end;
+end;
+
+procedure TCSSStyleInspector.AddRule(Row: integer; Selectors, Origin: TCSSString;
+  RuleEl: TCSSRuleElement);
+var
+  i, j: Integer;
+  ChildEl, KeyEl: TCSSElement;
+  DeclEl: TCSSDeclarationElement;
+  ResolvedKeyEl: TCSSResolvedIdentifierElement;
+  Src: String;
+  CurDiv, HeaderDiv, FooterDiv: TDiv;
+  BracketCloseLabel, BracketOpenLabel, SelectorLabel, OriginLabel: TLabel;
+begin
+  writeln('TCSSStyleInspector.AddRule Row=',Row,' Selector="',Selectors,'" Origin="',Origin,'"');
+
+  if Row<RulesDiv.NodeCount then
+  begin
+    CurDiv:=RulesDiv.Nodes[Row] as TDiv;
+    HeaderDiv:=CurDiv.Nodes[0] as TDiv;
+    SelectorLabel:=HeaderDiv.Nodes[0] as TLabel;
+    SelectorLabel.Caption:=Selectors;
+    OriginLabel:=HeaderDiv.Nodes[2] as TLabel;
+    OriginLabel.Caption:=Origin;
+  end else begin
+    CurDiv:=TDiv.Create(Self);
+    CurDiv.CSSClasses.Text:=RuleDivClass;
+    CurDiv.Parent:=RulesDiv;
+
+    // header: "selector { origin"
+    HeaderDiv:=TDiv.Create(Self);
+    HeaderDiv.CSSClasses.Text:=RuleHeaderDivClass;
+    HeaderDiv.Parent:=CurDiv;
+
+    SelectorLabel:=TLabel.Create(Self);
+    SelectorLabel.CSSClasses.Text:=RuleBracketLabelClass;
+    SelectorLabel.Caption:=Selectors;
+    SelectorLabel.Parent:=HeaderDiv;
+
+    BracketOpenLabel:=TLabel.Create(Self);
+    BracketOpenLabel.CSSClasses.Text:=RuleBracketLabelClass;
+    BracketOpenLabel.Caption:='{';
+    BracketOpenLabel.Parent:=HeaderDiv;
+
+    OriginLabel:=TLabel.Create(Self);
+    OriginLabel.CSSClasses.Text:=RuleOriginLabelClass;
+    OriginLabel.Caption:=Origin;
+    OriginLabel.Parent:=HeaderDiv;
+
+    // footer: "}"
+    FooterDiv:=TDiv.Create(Self);
+    FooterDiv.CSSClasses.Text:=RuleHeaderDivClass;
+    FooterDiv.Parent:=CurDiv;
+
+    BracketCloseLabel:=TLabel.Create(Self);
+    BracketCloseLabel.CSSClasses.Text:=RuleBracketLabelClass;
+    BracketCloseLabel.Caption:='}';
+    BracketCloseLabel.Parent:=FooterDiv;
+  end;
+
+  // header: "selector {    Origin"
+
+  if RuleEl<>nil then
+  begin
+    for i:=0 to RuleEl.ChildCount-1 do
+    begin
+      ChildEl:=RuleEl.Children[i];
+      //writeln('  Child ',i,' ',ChildEl.ClassName);
+      if ChildEl is TCSSDeclarationElement then
+      begin
+        DeclEl:=TCSSDeclarationElement(ChildEl);
+        Src:='';
+        if DeclEl.KeyCount<>1 then
+        begin
+          // todo: multi key declaration
+          continue;
+        end;
+        KeyEl:=DeclEl.Keys[0];
+        if KeyEl is TCSSResolvedIdentifierElement then
+        begin
+          ResolvedKeyEl:=TCSSResolvedIdentifierElement(KeyEl);
+          Src+=ResolvedKeyEl.Name;
+
+          Src+=' : ';
+          For j:=0 to DeclEl.ChildCount-1 do
+          begin
+            if j>0 then
+              Src+=', ';
+            Src+=DeclEl.Children[j].AsFormattedString;
+          end;
+          if DeclEl.IsImportant then
+            Src+=' !important';
+          if ResolvedKeyEl.NumericalID<=CSSIDNone then
+          begin
+            // unknown attribute
+            writeln('TCSSStyleInspector.AddRule Unknown attribute: ',i,' "',Src,'"');
+          end else begin
+            // todo: check for validity
+            // todo: check if already set
+            // todo: split shorthands
+            writeln('TCSSStyleInspector.AddRule Known: ',i,' "',Src,'"');
+          end;
+        end else begin
+          // todo: unknown declaration
+          writeln('TCSSStyleInspector.AddRule Invalid: ',i,' "',DeclEl.AsFormattedString,'"');
+        end;
+        // todo: !important
+      end;
+    end;
+  end;
+end;
+
+procedure TCSSStyleInspector.Notification(AComponent: TComponent; Operation: TOperation);
+begin
+  inherited Notification(AComponent, Operation);
+  if Operation=opRemove then
+  begin
+    if Target=AComponent then
+      Target:=nil;
+  end;
+end;
+
+constructor TCSSStyleInspector.Create(AOwner: TComponent);
+begin
+  inherited Create(AOwner);
+
+  FRulesDiv:=TDiv.Create(Self);
+  with RulesDiv do begin
+    CSSClasses.Text:=RulesDivClass;
+    Parent:=Self;
+  end;
+end;
+
+destructor TCSSStyleInspector.Destroy;
+begin
+  inherited Destroy;
+end;
+
+class function TCSSStyleInspector.GetCSSTypeStyle: TCSSString;
+begin
+  Result:='.'+RulesDivClass+' {  }';
+end;
+
+procedure TCSSStyleInspector.BeginUpdate;
+begin
+  inc(FLock);
+end;
+
+procedure TCSSStyleInspector.EndUpdate;
+begin
+  if FLock=0 then
+    raise EFresnel.Create('20250912163434');
+  dec(FLock);
+  if FLock=0 then
+    Inspect;
+end;
+
+end.
+