Recent

Author Topic: Can SynEdit Do This (Music Chords)??  (Read 6546 times)

pixelink

  • Hero Member
  • *****
  • Posts: 1260
Can SynEdit Do This (Music Chords)??
« on: February 18, 2017, 04:16:27 am »
Below I made a Lyric Editor last year using .NET and a source editor from SyncFusion.

I have SynEdit installed in LAZ

Can I do something (as far the editor part) just like what you see below in image??

I would need to be able to have custom tags that highlight and "auto complete" code completion for the chords.

Possible?
« Last Edit: February 20, 2017, 11:06:48 am by technipixel »
Can't Type - Forgetful - Had Stroke = Forgive this old man!
LAZ 2.2.0 •  VSSTUDIO(.Net) 2022 • Win10 • 16G RAM • Nvida GForce RTX 2060

Pascal

  • Hero Member
  • *****
  • Posts: 932
Re: Can SynEdit Do This??
« Reply #1 on: February 18, 2017, 06:10:03 am »
Yes, definitely.
You can use TSynMarkup for the highlighting of the chords and TSynCompletion for the code completion.
I've build a simple Markup that sticks to the originaly marked text and flows with the text while editing (I can post it if you are interested).
laz trunk x64 - fpc trunk i386 (cross x64) - Windows 10 Pro x64 (21H2)

guest58172

  • Guest
Re: Can SynEdit Do This??
« Reply #2 on: February 18, 2017, 08:12:38 am »
You'll even be able to have folds for the sections, red wave for unclosed chords and probably other pretty stuffs

pixelink

  • Hero Member
  • *****
  • Posts: 1260
Re: Can SynEdit Do This??
« Reply #3 on: February 18, 2017, 02:32:39 pm »
Yes, definitely.
You can use TSynMarkup for the highlighting of the chords and TSynCompletion for the code completion.
I've build a simple Markup that sticks to the originaly marked text and flows with the text while editing (I can post it if you are interested).

Sure.... because I haven't got an idea how to even work with SynEdit yet.

Q's
1) Where is this markup coded?

2) And how do I detect PROPER chords and highlight them using "splits.

This is my markup config file (XML) for SyncFusion's Editor

Code: Pascal  [Select][+][-]
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <ArrayOfConfigLanguage>
  3. <ConfigLanguage name="MusicChords" CaseInsensitive="true">
  4. <formats>
  5. <format name = "Text" Font="Courier New, 12pt" FontColor="Black" />
  6. <format name = "Operator" Font="Courier New, 12pt" FontColor="LightGray" />
  7. <format name = "Chord" Font="Courier New, 12pt" FontColor="Red" BackColor="White" ForeColor="Gray"/>
  8. </formats>
  9. <lexems>
  10. <lexem BeginBlock = "[" EndBlock="]" Type="Operator" IsEndRegex="true" IsComplex="true">
  11. <SubLexems>
  12. <lexem BeginBlock = "[A-Gabdfgijlmstu1-79#/]" IsBeginRegex="true" Type="Custom" FormatName="Chord"/>
  13. </SubLexems>
  14. </lexem>
  15. </lexems>
  16. <splits>
  17. <split IsRegex = "true" >[A-Gabdfgijlmstu1-79#/]</split>
  18. </splits>
  19. </ConfigLanguage>
  20. </ArrayOfConfigLanguage>
  21.  
Can't Type - Forgetful - Had Stroke = Forgive this old man!
LAZ 2.2.0 •  VSSTUDIO(.Net) 2022 • Win10 • 16G RAM • Nvida GForce RTX 2060

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 9867
  • Debugger - SynEdit - and more
    • wiki
Re: Can SynEdit Do This??
« Reply #4 on: February 18, 2017, 05:45:48 pm »
Actually I think you should go for a highlighter, not markup.

Read the Highlighter tutorial http://wiki.lazarus.freepascal.org/SynEdit_Highlighter

I am not exactly sure what you want to do?

1) If the text contains  [#A] this should be highlighted (textcolor or background, or frame, or bold .....) / (possible)

2) If the text contains [#A] then you only want to display #A (with highlight) (not possible)
2a) <red>[#a]</red> without displaying the tags  (not possible)

The 1st is the typical use of a highlighter.

The 2nd is not possible (at least not without major work)

Basically every char in the text, will be displayed on the screen. But you can color it as you like. (If you can write code, that will detect it)



Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 9867
  • Debugger - SynEdit - and more
    • wiki
Re: Can SynEdit Do This??
« Reply #5 on: February 18, 2017, 05:50:21 pm »
Since you seem to have a fixed list of strings (maybe 10 to 20 chords), there are other opitons

1) Search for SynAnySyn (a highlighter, that can be given a list of words)

2) There is a markup that does this too. TSynEditMarkupHighlightAllMulti
search the forum.

Also http://forum.lazarus-ide.org/index.php/topic,35530.msg235064.html#msg235064 (ignore the first bit of that post / 2nd half only)

Martin_fr

  • Administrator
  • Hero Member
  • *
  • Posts: 9867
  • Debugger - SynEdit - and more
    • wiki
Re: Can SynEdit Do This??
« Reply #6 on: February 18, 2017, 05:51:42 pm »
look through the IDE example/synedit folder for auto-completion

Pascal

  • Hero Member
  • *****
  • Posts: 932
Re: Can SynEdit Do This??
« Reply #7 on: February 19, 2017, 08:11:36 am »
Martin, highlighter wouldn't work here as you can not recognize "A", "D" and so on correctly.

Here is my Markup:
Code: Pascal  [Select][+][-]
  1. unit primeMarkup;
  2.  
  3. {$mode objfpc}{$H+}
  4.  
  5. interface
  6.  
  7. uses
  8.   Classes, SysUtils, SynEditMarkupHighAll, LazSynEditText, SynEditMiscClasses,
  9.   Graphics;
  10.  
  11. type
  12.   { TprimeMarkupBase }
  13.  
  14.   TprimeMarkupBase = class(TSynEditMarkupHighlightMatches)
  15.   private
  16.     procedure DoLineChange(Sender: TSynEditStrings; p_LinePos, p_BytePos, p_Count,
  17.       p_LineBrkCnt: Integer; p_Text: String);
  18.   protected
  19.     procedure SetLines(const p_new: TSynEditStrings); override;
  20.   public
  21.     constructor Create(p_SynEdit : TSynEditBase);
  22.     destructor Destroy; override;
  23.     procedure AddMark(p_Start, p_Finish: TPoint);
  24.     procedure ClearMarks;
  25.     property Matches;
  26.   end;
  27.  
  28. implementation
  29.  
  30. uses
  31.   LCLProc;
  32.  
  33. { TprimeMarkupBase }
  34.  
  35. procedure TprimeMarkupBase.DoLineChange(Sender: TSynEditStrings; p_LinePos,
  36.   p_BytePos, p_Count, p_LineBrkCnt: Integer; p_Text: String);
  37. var
  38.   i, end_BytePos: Integer;
  39.   m: TSynMarkupHighAllMatch;
  40.   abs_Count, abs_LineBrkCnt: Integer;
  41.   min, max: Integer;
  42.   changed, StartMoved, EndMoved: Boolean;
  43. begin
  44.   if Matches.Count = 0 then exit;
  45.   min := MaxInt;
  46.   max := Integer($ffffffff);
  47.   {$IFDEF primeMarkupDebug}
  48.   DebugLn('*** %s.DoLineChange l=%d x=%d c=%d crlf=%d t=%s', [ClassName, p_LinePos, p_BytePos, p_Count, p_LineBrkCnt, p_Text]);
  49.   {$ENDIF}
  50.   if (p_Count < 0)
  51.   or (p_LineBrkCnt < 0) then begin
  52.     // ggf. Marks löschen
  53.     abs_Count := Abs(p_Count);
  54.     abs_LineBrkCnt := Abs(p_LineBrkCnt);
  55.     end_BytePos := p_BytePos + abs_Count;
  56.     i := Matches.Count - 1;
  57.     while i >= 0 do begin
  58.       m := Matches[i];
  59.       changed := false;
  60.       StartMoved := false;
  61.       EndMoved := false;
  62.       if p_Count < 0 then begin
  63.         // Zeichen entfernt
  64.         if (m.StartPoint.y = p_LinePos)
  65.         and (m.StartPoint.x >= p_BytePos) then begin
  66.           // Änderung beginnt in der Zeile vor der Markierung
  67.           // -> Startposition anpassen
  68.           if (m.StartPoint.x >= end_BytePos) then begin
  69.             // komplett vor der Markierung
  70.             dec(m.StartPoint.x, abs_Count);
  71.           end else begin
  72.             // Änderung reicht in die Markierung
  73.             m.StartPoint.x := p_BytePos;
  74.           end;
  75.           Matches.StartPoint[i] := m.StartPoint;
  76.  
  77.           if (m.EndPoint.y = p_LinePos) then begin
  78.             // Ende der Markierung ist in der gleichen Zeile
  79.             if (m.EndPoint.x > end_BytePos) then begin
  80.               // und komplett vor dem Ende der Markierung
  81.               // -> also auch Endposition anpassen
  82.               dec(m.EndPoint.x, abs_Count);
  83.               Matches.EndPoint[i] := m.EndPoint;
  84.             end else begin
  85.               // und komplett nach dem Ende der Markierung
  86.               // -> Markierung entfernen
  87.               Matches.Delete(i);
  88.             end;
  89.           end;
  90.           changed := true;
  91.         end else if (m.EndPoint.y = p_LinePos)
  92.         and (m.EndPoint.x > p_BytePos) then begin
  93.           // Änderung ist in der Zeile vor dem Ende der Markierung
  94.           if (m.EndPoint.x > end_BytePos) then begin
  95.             // und komplett vor dem Ende der Markierung
  96.             // -> also auch Endposition anpassen
  97.             dec(m.EndPoint.x, abs_Count);
  98.           end else begin
  99.             // und reicht über das Ende der Markierung hinaus
  100.             // -> Endposition kürzen
  101.             m.EndPoint.x := p_BytePos;
  102.           end;
  103.           Matches.EndPoint[i] := m.EndPoint;
  104.           changed := true;
  105.         end;
  106.       end;
  107.       if p_LineBrkCnt < 0 then begin
  108.         // Zeilen entfernt
  109.         if p_Count <> 0 then DebugLn('*****'#13'***** %s.DoLineChange l=%d x=%d c=%d crlf=%d t=%s    !!! c != 0'#13'*****', [ClassName, p_LinePos, p_BytePos, p_Count, p_LineBrkCnt, p_Text]);
  110.  
  111.         if (m.StartPoint.y = p_LinePos + abs_LineBrkCnt) then begin
  112.           // Zeilenzusammenführung:
  113.           // Markup aus der nächsten Zeile wird in diese Zeile geholt
  114.           // Start
  115.           inc(m.StartPoint.x, p_BytePos - 1);
  116.           dec(m.StartPoint.y, abs_LineBrkCnt);
  117.           changed := true;
  118.           StartMoved := true;
  119.           // End
  120.           if (m.EndPoint.y = p_LinePos + abs_LineBrkCnt) then
  121.             inc(m.EndPoint.x, p_BytePos - 1);
  122.           dec(m.EndPoint.y, abs_LineBrkCnt);
  123.           EndMoved := true;
  124.         end else if (m.EndPoint.y = p_LinePos + abs_LineBrkCnt) then begin
  125.           // Zeilenzusammenführung:
  126.           // Markup aus der nächsten Zeile wird in diese Zeile geholt
  127.           inc(m.EndPoint.x, p_BytePos - 1);
  128.           dec(m.EndPoint.y, abs_LineBrkCnt);
  129.           changed := true;
  130.           EndMoved := true;
  131.         end;
  132.  
  133.         // y anpassen
  134.         if (m.StartPoint.y > p_LinePos)
  135.         and not StartMoved then begin
  136.           dec(m.StartPoint.y, abs_LineBrkCnt);
  137.           changed := true;
  138.         end;
  139.         if (m.EndPoint.y > p_LinePos)
  140.         and not EndMoved then begin
  141.           dec(m.EndPoint.y, abs_LineBrkCnt);
  142.           changed := true;
  143.         end;
  144.  
  145.         Matches.StartPoint[i] := m.StartPoint;
  146.         Matches.EndPoint[i] := m.EndPoint;
  147.       end;
  148.  
  149.       if changed then begin
  150.         if m.StartPoint.y < min then min := m.StartPoint.y;
  151.         if m.StartPoint.y > max then max := m.StartPoint.y;
  152.         if m.EndPoint.y < min then min := m.EndPoint.y;
  153.         if m.EndPoint.y > max then max := m.EndPoint.y;
  154.       end;
  155.       dec(i);
  156.     end;
  157.   end else begin
  158.     // ggf. Marks verschieben
  159.     for i := 0 to Matches.Count - 1 do begin
  160.       m := Matches[i];
  161.       changed := false;
  162.       StartMoved := false;
  163.       EndMoved := false;
  164.  
  165.       if (m.StartPoint.y = p_LinePos)
  166.       and (m.StartPoint.x >= p_BytePos) then begin
  167.         // Änderung ist in der Zeile vor der Markierung
  168.         // -> Startposition anpassen
  169.         inc(m.StartPoint.x, p_Count);
  170.         if p_LineBrkCnt > 0 then begin
  171.           inc(m.StartPoint.y, p_LineBrkCnt);
  172.           StartMoved := true;
  173.           dec(m.StartPoint.x, p_BytePos - 1);
  174.         end;
  175.         if m.EndPoint.y = p_LinePos then begin
  176.           // Ende der Markierung ist in der gleichen Zeile
  177.           // -> also auch Endposition anpassen
  178.           inc(m.EndPoint.x, p_Count);
  179.           if p_LineBrkCnt > 0 then begin
  180.             inc(m.EndPoint.y, p_LineBrkCnt);
  181.             EndMoved := true;
  182.             dec(m.EndPoint.x, p_BytePos - 1);
  183.           end;
  184.         end;
  185.         changed := true;
  186.       end;
  187.  
  188.       if (m.EndPoint.y = p_LinePos)
  189.       and (m.EndPoint.x > p_BytePos)
  190.       and (
  191.         (m.StartPoint.y <> p_LinePos)
  192.         or (m.StartPoint.x < p_BytePos)
  193.       )then begin
  194.         // Änderung ist in der Markierung
  195.         // -> also Endposition anpassen
  196.         inc(m.EndPoint.x, p_Count);
  197.         if p_LineBrkCnt > 0 then begin
  198.           if not EndMoved then begin
  199.             inc(m.EndPoint.y, p_LineBrkCnt);
  200.             EndMoved := true;
  201.           end;
  202.           dec(m.EndPoint.x, p_BytePos - 1);
  203.         end;
  204.         changed := true;
  205.       end;
  206.  
  207.       if p_LineBrkCnt > 0 then begin
  208.         // y anpassen
  209.         if not StartMoved
  210.         and (m.StartPoint.y > p_LinePos) then begin
  211.           inc(m.StartPoint.y, p_LineBrkCnt);
  212.           changed := true;
  213.         end;
  214.         if not EndMoved
  215.         and (m.EndPoint.y > p_LinePos) then begin
  216.           inc(m.EndPoint.y, p_LineBrkCnt);
  217.           changed := true;
  218.         end;
  219.       end;
  220.  
  221.       Matches.StartPoint[i] := m.StartPoint;
  222.       Matches.EndPoint[i] := m.EndPoint;
  223.  
  224.       if changed then begin
  225.         if m.StartPoint.y < min then min := m.StartPoint.y;
  226.         if m.StartPoint.y > max then max := m.StartPoint.y;
  227.         if m.EndPoint.y < min then min := m.EndPoint.y;
  228.         if m.EndPoint.y > max then max := m.EndPoint.y;
  229.       end;
  230.     end;
  231.   end;
  232.   if min <= max then begin
  233.     {$IFDEF primeMarkupDebug}
  234.     DebugLn('### InvalidateSynLines(%d, %d)', [min, max]);
  235.     {$ENDIF}
  236.     InvalidateSynLines(min, max);
  237.   end;
  238. end;
  239.  
  240. procedure TprimeMarkupBase.SetLines(const p_new: TSynEditStrings);
  241. var
  242.   l_old: TSynEditStrings;
  243. begin
  244.   l_old := Lines;
  245.   if (l_old <> p_new)
  246.   and Assigned(l_old) then begin
  247.     // alten ChangeHandler entfernen
  248.     l_old.RemoveEditHandler(@DoLineChange);
  249.   end;
  250.   inherited SetLines(p_new);
  251.   if (l_old <> p_new)
  252.   and Assigned(p_new) then begin
  253.     // neuen ChangeHandler eintragen
  254.     p_new.AddEditHandler(@DoLineChange);
  255.   end;
  256. end;
  257.  
  258. constructor TprimeMarkupBase.Create(p_SynEdit: TSynEditBase);
  259. begin
  260.   inherited Create(p_SynEdit);
  261. end;
  262.  
  263. destructor TprimeMarkupBase.Destroy;
  264. begin
  265.   Lines := nil;
  266.   inherited Destroy;
  267. end;
  268.  
  269. procedure TprimeMarkupBase.AddMark(p_Start, p_Finish: TPoint);
  270. var
  271.   r: TSynMarkupHighAllMatch;
  272. begin
  273.   r.StartPoint := p_Start;
  274.   r.EndPoint := p_Finish;
  275.   Matches[Matches.Count] := r;
  276. end;
  277.  
  278. procedure TprimeMarkupBase.ClearMarks;
  279. begin
  280.   Matches.Count := 0;
  281. end;
  282.  
  283. end.
  284.  

Build a descendant:
Code: Pascal  [Select][+][-]
  1.   TprimeUseMarkup = class(TprimeMarkupBase)
  2.   public
  3.     constructor Create(ASynEdit : TSynEditBase; AColor: TColor; AFontColor: TColor = clWhite);
  4.   end;
  5. ....
  6. implementation.
  7. constructor TprimeUseMarkup.Create(ASynEdit: TSynEditBase; AColor: TColor; AFontColor: TColor = clWhite);
  8. begin
  9.   inherited Create(ASynEdit);
  10.   MarkupInfo.Clear;
  11.   MarkupInfo.Background := AColor;
  12.   MarkupInfo.BackPriority := 49;
  13.   MarkupInfo.FrameColor := clBlack;
  14.   MarkupInfo.FrameEdges := sfeAround;
  15.   MarkupInfo.FrameStyle := slsSolid;
  16.   MarkupInfo.FrameAlpha := 80;
  17.   MarkupInfo.Foreground := AFontColor;
  18.   MarkupInfo.ForePriority := 49;
  19.   Matches.Capacity := 1000;
  20. end;
  21.  

Build your own descendant of TCustomEynEdit or use property MarkupManager directly with a standard TSynEdit:
Code: Pascal  [Select][+][-]
  1.   TprimeBaseEdit = class(TCustomSynEdit)
  2.   private
  3.     fMarkup: TprimeUseMarkup;
  4.   public
  5.     constructor Create(AOwner: TComponent); override;
  6.     procedure AddMark(Start, Finish: TPoint);
  7.   end;
  8.  
  9. Implementation.
  10.  
  11. constructor TprimeBaseEdit.Create(AOwner: TComponent);
  12. begin
  13.   inherited Create(AOwner);
  14.   fMarkup := TprimeUseMarkup.Create(self, clVariableDirectWrite);
  15.   ...
  16.   TSynEditMarkupManager(MarkupMgr).AddMarkUp(fMarkup);
  17.   ...
  18. end;
  19.  
  20. procedure TprimeBaseEdit.AddMark(Start, Finish: TPoint);
  21. begin
  22.   fMarkup.AddMark(Start, Finish);
  23. end;
  24.  

Use it:
Code: Pascal  [Select][+][-]
  1. ChordEdit.AddMarkup(Point(15, 2), Point(16,2));
  2.  
laz trunk x64 - fpc trunk i386 (cross x64) - Windows 10 Pro x64 (21H2)

Pascal

  • Hero Member
  • *****
  • Posts: 932
Re: Can SynEdit Do This??
« Reply #8 on: February 19, 2017, 01:22:03 pm »
Martin, highlighter wouldn't work here as you can not recognize "A", "D" and so on correctly.
Highlighter would also be okay. I didn't regognized [ and ] around the chords.
laz trunk x64 - fpc trunk i386 (cross x64) - Windows 10 Pro x64 (21H2)

Edson

  • Hero Member
  • *****
  • Posts: 1302
Re: Can SynEdit Do This??
« Reply #9 on: February 19, 2017, 05:36:13 pm »
Using SynFacilCompletion (https://github.com/t-edson/SynFacilCompletion) , it's easy to highlight chords and activate the autocompletion, and the folding. A simple XML syntax would be:

Code: XML  [Select][+][-]
  1. <?xml version="1.0"?>
  2. <Language name="Object Pascal" ext="pas" ColorBlock="Block">
  3.   <Completion>
  4.         <OpenOn AfterPattern="Identifier">
  5.       <Include Attribute="Keyword"></Include>
  6.         </OpenOn>
  7.         <OpenOn AfterPattern="Chord">
  8.       [A] [Am] [A#m]
  9.       [B] [Bm] [B#m]
  10.       [C] [Cm] [C#m]
  11.         </OpenOn>
  12.   </Completion>
  13.   <Attribute Name="Keyword" Style="b"> </Attribute>
  14.   <Attribute Name="Chord" Style="b" Forecol = "Red"> </Attribute>
  15.   <Identifiers CharsStart= "A..Za..z_" Content = "A..Za..z0..9_">
  16.     <token attribute="Keyword">
  17.     Intro Chorus Bridge End
  18.     </token>
  19.   </Identifiers>
  20.   <Token Start="[" End="]" Attribute='Chord'></Token>
  21.   <Section Name="Intro" Start="Intro"> </Section>
  22.   <Section Name="Chorus" Start="Chorus" BackCol="#E0FFF0"></Section>
  23.   <Section Name="Bridge" Start="Bridge"> </Section>
  24.   <Section Name="End" Start="End"> </Section>
  25. </Language>
  26.  

Lazarus 2.2.6 - FPC 3.2.2 - x86_64-win64 on Windows 10

pixelink

  • Hero Member
  • *****
  • Posts: 1260
Re: Can SynEdit Do This (Music Chords)??
« Reply #10 on: February 20, 2017, 11:06:15 am »
This is all GREAT stuff...

So, my end game would be that...

1) The "[" (brackets) would be one color
2) The chords would be another color - BUT ONLY highlighted within the brackets
3) All other text (outside the brackets) would be normal color.

Eventually, I will want to print the song showing ONLY the chords, but not the brackets.

FYI, there is actually around 100 different chord combos when you think about all the sharps and flats for each chord (major, min, dim, flats, sharps etc etc)

Thanks to everyone
Can't Type - Forgetful - Had Stroke = Forgive this old man!
LAZ 2.2.0 •  VSSTUDIO(.Net) 2022 • Win10 • 16G RAM • Nvida GForce RTX 2060

Thaddy

  • Hero Member
  • *****
  • Posts: 14373
  • Sensorship about opinions does not belong here.
Re: Can SynEdit Do This (Music Chords)??
« Reply #11 on: February 20, 2017, 11:49:20 am »
I have some code for you that can actually add the - possible - finger settings for the chord. Interested?
Chordfinder 1.0.1 in KOL, but it is pretty generic code. http://thaddy.nl/chordfinder.zip  Windows only and VERY small. Also supports midi. And Piano key equivalents.
« Last Edit: February 20, 2017, 12:00:14 pm by Thaddy »
Object Pascal programmers should get rid of their "component fetish" especially with the non-visuals.

pixelink

  • Hero Member
  • *****
  • Posts: 1260
Re: Can SynEdit Do This (Music Chords)??
« Reply #12 on: February 20, 2017, 03:23:35 pm »
I have some code for you that can actually add the - possible - finger settings for the chord. Interested?
Chordfinder 1.0.1 in KOL, but it is pretty generic code. http://thaddy.nl/chordfinder.zip  Windows only and VERY small. Also supports midi. And Piano key equivalents.

Sweet Thaddy!
Thanks
Can't Type - Forgetful - Had Stroke = Forgive this old man!
LAZ 2.2.0 •  VSSTUDIO(.Net) 2022 • Win10 • 16G RAM • Nvida GForce RTX 2060

Thaddy

  • Hero Member
  • *****
  • Posts: 14373
  • Sensorship about opinions does not belong here.
Re: Can SynEdit Do This (Music Chords)??
« Reply #13 on: February 20, 2017, 05:49:35 pm »
Maybe we can do this together? I can make a piano bar and a guitar bar and make it cross-platform.
I also have a feature that scrolls song texts based on tempo. You do the editor, though  8-)
« Last Edit: February 20, 2017, 05:51:18 pm by Thaddy »
Object Pascal programmers should get rid of their "component fetish" especially with the non-visuals.

 

TinyPortal © 2005-2018