// SPDX-License-Identifier: LGPL-3.0-linking-exception
{ Useful tools for RTTI. Functions are used expecialy for save/load styles.
  Styles has construction similar to INI files:

  [Header]
  Author=Krzysztof Dibowski
  Description=My test style
  ControlClass=TBCButton

  [Properties]
  State.Border.Width=2
  .....

  But instead of IniFiles unit, we have own functions for read and write styles.

  ------------------------------------------------------------------------------
  originally written in 2012 by Krzysztof Dibowski dibowski at interia.pl
}
{******************************* CONTRIBUTOR(S) ******************************
- Edivando S. Santos Brasil | mailedivando@gmail.com
  (Compatibility with delphi VCL 11/2018)

***************************** END CONTRIBUTOR(S) *****************************}
unit BCRTTI;

{$I bgracontrols.inc}

interface

uses
  Classes;

type
  PBCStyleHeader = ^TBCStyleHeader;
  TBCStyleHeader = record
    Author: String;
    ControlClass: String;
    Description: String;
  end;

// Function return data of specified section (header, properties, etc).
// This is smart function, because it doesn't read whole file but read file
// line by line and return only needed section. So it should fastest for reading
// header info instead of TIniFile object which read, parse and index all file.
function GetSectionData(const AFileName, ASectionName: String): TStrings;
// Methods which read header from list or file and parse it into pascal record
procedure GetStyleHeader(const AFileName: String; AOutHeader: PBCStyleHeader);
// Function check if specified name is on ignored list
function IsPropIgnored(const AName: String): Boolean;
// Method load style saved by SaveStyle method
procedure LoadStyle(AControl: TObject; const AFileName: String; ALogs: TStrings = nil);
// Method save all (which are not on ignored list or readonly) public propertys to
// the output string list. This method have support for property
// tree (Propert1.Subpropert1.Color = 543467). Values are represented as "human readable"
// (e.g. Align = alClient). Header info is save too.
procedure SaveStyle(AControl: TObject; const AAuthor, ADescription: String;
  ATargetList: TStrings);

implementation

uses typinfo, variants, sysutils, {%H-}strutils;

const
  tIGNORED_PROPS: array[0..5] of string =
    ('name','caption','left','top','height','width');
  sSECTION_HEADER_NAME = 'HEADER';
  sSECTION_PROP_NAME   = 'PROPERTIES';
  sSECTION_HEADER = '['+sSECTION_HEADER_NAME+']';
  sSECTION_PROP   = '['+sSECTION_PROP_NAME+']';

procedure RemovePadChars(var S: String; const CSet: TSysCharset);
var
  I,J,K: LONGINT;
begin
 I:=Length(S);
 IF (I>0) Then
  Begin
   J:=I;
   While (j>0) and (S[J] IN CSet) DO DEC(J);
   if j=0 Then
     begin
       s:='';
       exit;
     end;
   k:=1;
   While (k<=I) And (S[k] IN CSet) DO
     INC(k);
   IF k>1 Then
     begin
       move(s[k],s[1],j-k+1);
       setlength(s,j-k+1);
     end
   else
     setlength(s,j);
  end;
end;

function TrimSet(const S: String;const CSet:TSysCharSet): String;
begin
  result:=s;
  RemovePadChars(result,cset);
end;

function IsPropIgnored(const AName: String): Boolean;
var
  i: Integer;
begin
  Result := False;
  for i := Low(tIGNORED_PROPS) to High(tIGNORED_PROPS) do
    if SameText(tIGNORED_PROPS[i],Trim(AName)) then
      Exit(True);
end;

procedure LoadStyle(AControl: TObject; const AFileName: String;
  ALogs: TStrings = nil);
var
  i, iDot: Integer;
  sPath, sVal: String;
  obj: TObject;
  sl: TStrings;
const
  sLOG_NO_PROP   = 'Can not find property "%s"';
  sLOG_SET_ERR   = 'Can not set value "%s" to property "%s"';
  sLOG_READ_ONLY = 'Property "%s" is read-only';

  procedure _AddLog(const AText: String);
  begin
    if ALogs<>nil then
      ALogs.Add(AText);
  end;

  function _ValidateProp(AObj: TObject; const APropName: String): Boolean;
  begin
    Result := True;
    // If can't find property
    if not IsPublishedProp(AObj,APropName) then
    begin
      _AddLog(Format(sLOG_NO_PROP,[APropName]));
      Exit(False);
    end;
    // If read-only property
    if (GetPropInfo(AObj,APropName)^.SetProc=nil) then
    begin
      _AddLog(Format(sLOG_READ_ONLY,[APropName]));
      Exit(False);
    end;
  end;

begin
  if not FileExists(AFileName) then
    Exit;

  if ALogs<>nil then
    ALogs.Clear;

  sl := GetSectionData(AFileName, sSECTION_PROP_NAME);
  try
    for i:=0 to Pred(sl.Count) do
    begin
      // Full path with hierarchy tree
      sPath := Trim(sl.Names[i]);
      // "Human readable" value
      sVal := Trim(sl.ValueFromIndex[i]);
      iDot := Pos('.', sPath);
      // If simple property then write it value
      if iDot=0 then
      begin
        if not _ValidateProp(AControl,sPath) then
          Continue;
        // Writting property value
        try
          SetPropValue(AControl,sPath,sVal)
        except
          _AddLog(Format(sLOG_SET_ERR,[sVal, sPath]));
        end
      end
      else
      begin
        //... else we must go down in hierarchy tree to the last
        // object and then write value to property
        obj := AControl;
        while iDot>0 do
        begin
          if not _ValidateProp(obj,Copy(sPath,1,iDot-1)) then
          begin
            obj := nil;
            Break;
          end;

          obj := GetObjectProp(obj,Copy(sPath,1,iDot-1));

          Delete(sPath,1,iDot);
          iDot := Pos('.', sPath);
        end;

        // If no dots, then this word is property name
        if (obj<>nil) and (sPath<>'') and _ValidateProp(obj,sPath) then
        begin
          try
            SetPropValue(obj,sPath,sVal)
          except
            _AddLog(Format(sLOG_SET_ERR,[sVal, sPath]));
          end
        end;
      end;
    end;
  finally
    sl.Free;
  end;
end;

procedure SaveStyle(AControl: TObject; const AAuthor, ADescription: String;
  ATargetList: TStrings);

  procedure _SaveProp(AObj: TObject; APath: String = '');
  var
    iCount, i: Integer;
    lst: TPropList;
    s: String;
  begin
    if AObj=nil then Exit;

    iCount := GetPropList(PTypeInfo(AObj.ClassInfo), tkProperties, @lst);
    for i := 0 to Pred(iCount) do
      { Notice:
        - IsPublishedProp return true for ALL public properties, not only
          for properties in Published section. For saving styles, we don't need
          all public properties, but only published (visible in object inspector).
          I don't know if this is a bug, I leave it. Maybe it will start
          working in future ;)
        - Second argument check if property should be ignored (but only from root tree),
          because we can't save basic properties of control like Name, Top, Left etc.
        - SetProc<>nil mean "not read only"
      }
      if IsPublishedProp(AObj,lst[i]^.Name) and
         ((AControl<>AObj) or (not IsPropIgnored(lst[i]^.Name))) and
         (lst[i]^.SetProc<>nil)
      then
      begin
        // Building property tree
        if APath=''
        then s := lst[i]^.Name
        else s := APath+'.'+lst[i]^.Name;

        // If property has subproperty, then we start recurrence to
        // build hierarchy tree.
        if (lst[i]^.PropType^.Kind = tkClass) then
          _SaveProp(GetObjectProp(AObj,lst[i]),s)
        else
        begin
          // We are in bottom node, so we can save final property with value
          s := s + ' = ' + String(GetPropValue(AObj,lst[i]^.Name,True));
          ATargetList.Add(s);
        end;
      end;
  end;
begin
  if ATargetList=nil then
    Exit;
  ATargetList.Clear;

  ATargetList.Add(sSECTION_HEADER);
  ATargetList.Add('Author='+AAuthor);
  ATargetList.Add('Description='+ADescription);
  ATargetList.Add('ControlClass='+AControl.ClassName);
  ATargetList.Add('');
  ATargetList.Add(sSECTION_PROP);
  _SaveProp(AControl);
end;

function GetSectionData(const AFileName, ASectionName: String): TStrings;
var
  f: TextFile;
  s: String;
  bReading: Boolean;
begin
  Result := TStringList.Create;
  Result.Clear;

  if (not FileExists(AFileName)) or (ASectionName='') then
    Exit;

  AssignFile(f,AFileName);
  try
    Reset(f);
    bReading := False;
    while not EOF(f) do
    begin
      ReadLn(f,s);
      s := Trim(s);
      if s='' then
        Continue;

      // If current line is section tag
      if s[1]='[' then
      begin
        // If we currently reading section then we read it all and we must
        // break because another section occur
        if bReading then
        begin
          bReading := False;
          Break;
        end
        else
        // Otherwise if this is section we are looking for, then set flag
        // to "start reading"
        if SameText(ASectionName,TrimSet(s,['[',']'])) then
          bReading := True;
      end else
      // Read section line
      if bReading then
        Result.Add(s);
    end;
  finally
    CloseFile(f);
  end;
end;

procedure GetStyleHeader(const AFileName: String; AOutHeader: PBCStyleHeader);
var sl: TStrings;
begin
  if (AOutHeader=nil) or (not FileExists(AFileName)) then
    Exit;

  sl := GetSectionData(AFileName,sSECTION_HEADER_NAME);
  try
    // Header info (with format Author=Foo) should be at the top of file
    with AOutHeader^ do
    begin
      Author       := sl.Values['Author'];
      Description  := sl.Values['Description'];
      ControlClass := sl.Values['ControlClass'];
    end;
  finally
    sl.Free;
  end;
end;

end.

