YOUSEFUL / DELPHI

Atlas UI: FAQ, examples and tutorial

Learning center · Component reference

Atlas UI adds four components to the Atlas UI palette page, for three jobs that Atlas VCL does not cover. The Atlas.Kanban unit holds a Kanban board in two parts: TAtlasKanbanModel is a non-visual model of columns and cards, with work-in-progress limits, moves your code can veto and a validated JSON format, and TAtlasKanban is the control that draws a model and lets the user select and move cards. Atlas.Syntax holds TAtlasSyntaxMemo, a Rich Edit that colours Object Pascal source as you type and keeps its own text-only undo history, together with the lexer behind it, AtlasPascalTokens. Atlas.Updater holds TAtlasWebUpdater, which reads an update manifest over HTTPS, compares version numbers, and downloads, verifies and stores the update file without ever running it. Atlas UI requires Atlas VCL: its runtime package needs AtlasVclRuntime, so add both src folders to the search path (Getting started shows how). For grids, scheduling, rich text, a command bar, trees, PDF viewing and spell checking, use the Atlas VCL components (TAtlasGrid, TAtlasSchedule, TAtlasRichEdit, TAtlasCommandBar, TAtlasTree, TAtlasPdfViewer, TAtlasSpellChecker); Atlas UI does not add its own versions of them.

FAQ

What does Atlas UI contain, and what does it not? The four components above and the types they use (the card record, the token record and kinds, the update record, two exception classes). It is not a large collection of visual controls, it has no FireMonkey or web versions, and it is not compatible with the form files or programming interfaces of other component suites. The design package registers the four components on the Atlas UI palette page, but installing it into the IDE has not been tested yet; compiling the units into your program, as every sample here does, is the tested route.

How do the Kanban model and the board fit together? The model holds all data; the board only draws it and keeps the selected card and its scroll position. Assign the model to the board's Model property. Every change to the model (AddColumn, AddCard, RemoveCard, MoveCard, LoadJSON) repaints every board showing it and then fires OnChange, so several boards can show one model. The model needs no window, so board rules can be tested in a console program (Kanban model in a console). Freeing the model detaches its boards safely.

What does a work-in-progress limit do? AddColumn('active', 'In progress', 3) lets the column hold at most three cards; 0, the default, means no limit. Adding a card to a full column, or moving one into it from another column, raises EAtlasKanban with Column work-in-progress limit reached; reordering inside the column is always allowed. LoadJSON applies the same limits, and the board shows them in the header (In progress 1/3).

How do I stop a move, and what happens when a move fails? Assign BeforeMove and set Allow to False; MoveCard then returns False and nothing changes. The handler receives the card id and the source and target column ids and runs after the built-in checks, so it only needs your own rules. It must not change the model: any change inside it raises EAtlasKanban. Invalid moves (an unknown id, a full column, an index out of range) raise EAtlasKanban from MoveCard. Moves made on the board never raise: MoveSelectedTo, which the mouse and the keyboard use, returns False, keeps the reason in LastError and paints it in red at the bottom of the board.

How do I save a board, and how do I change what the model has no method for? ToJSON returns the whole board as one line of JSON and LoadJSON replaces the board with a saved one. Loading is atomic: the document is built and validated in a separate model first, so a bad document raises an exception and leaves the current board as it was. Every field is required, and a missing one raises the RTL's EJSONException rather than EAtlasKanban. The model has no methods to remove, rename or reorder columns or to edit a card. Change a card by removing it, adding it again and moving it back into place with MoveCard and an index; change columns by editing a board document and loading it. {"schema":1,"columns":[]} empties the board.

Which mouse and keyboard actions does the board support? A click selects a card. Dragging a card onto another column moves it there, always to the end of that column; there is no drop position or drag image, so use MoveCard with an index to place a card. The mouse wheel scrolls the columns together and Shift+wheel scrolls sideways; there are no scroll bars. The control also has arrow-key selection and Ctrl+Left and Ctrl+Right moves, but on a form the arrow keys never reach it unless you use a small subclass that asks for them (shown under Examples and in the tutorial). The selection is not scrolled into view.

Can I change how the board looks? Only the font. Columns are 270 pixels wide and card slots 110 pixels high, scaled with the screen's DPI; each card shows its title in bold and its detail text below. The colours are fixed, VCL styles are not applied, and there are no properties or events for custom drawing, card templates, avatars, tags or due dates. Use the card's Detail text for extra information.

What does the syntax editor colour, and what is it not? Object Pascal keywords (in bold), strings, numbers (decimal, real and $ hexadecimal) and the three comment forms, recomputed for the whole text after every change. Directives such as override, type names, #13 character codes and multi-line string literals are not recognized, and compiler directives are coloured as comments. It is a lexer, not a parser or a language service: there is no code completion, error marking, folding, line numbers, bracket matching or support for other languages. The other TRichEdit properties and events are there as usual, but any formatting you apply yourself is replaced at the next change. Colouring needs a window, so give the editor its Parent before its Text, or call Highlight afterwards.

How does undo work in the syntax editor? The editor keeps a copy of the whole text after every change, up to 128 copies, and UndoText and RedoText step through them. Ctrl+Z, Ctrl+Y, Ctrl+Shift+Z and the inherited Undo all use this history, so recolouring never becomes an undo step. Undo restores the text only; the caret moves to the end. The history starts with the empty text of a new editor and cannot be cleared, so undoing past a loaded file empties the editor. Because every change stores a full copy and recolours everything, the editor suits small and moderate files; it has not been measured with large ones.

What does TAtlasWebUpdater check, and what is left to my application? It accepts only schema 1 manifests with a numeric three- or four-part version, an HTTPS url without credentials, the exact byte size and the sha256 digest. It refuses redirects and any HTTP status other than 200, lets Windows check the server certificate, downloads the file, checks its size and digest in memory, and only then writes it to the cache folder you name, through a temporary .part file that is renamed when complete, so no partial or unverified file ever carries the final name. It never runs, opens or installs anything, and it does not check publisher signatures: the digest proves the file matches the manifest, not who wrote the manifest. Installation, rollback and cleaning up old cache files are up to your application.

Why should the updater run on a worker thread? FetchManifest and StageUpdate block until the transfer ends or fails, with a 10-second connection timeout and a 15-second response timeout, and there is no progress event or cancellation. Call them from a TTask or TThread and report back with TThread.Queue; the component reference has an example. The whole download is held in memory. A Win32 program stops a response that grows past the manifest's size; a Win64 program receives all of it first and only then refuses it, so a misbehaving server can make it use more memory than the manifest declares.

How can I test update code without an HTTPS server? ParseManifest checks manifest text with no network at all, and CompareVersions needs nothing but two strings; the Manifest check sample exercises both. For a download test, run a local HTTP server bound to 127.0.0.1 and set AllowLoopbackHTTP; plain HTTP is then accepted for exactly that address, not for localhost or any other host.

Examples

Build a board in code without any window. MoveCard with an index reorders cards inside a column.

pascal
uses System.SysUtils, Atlas.Kanban;

procedure BuildBoard;
var
  Model: TAtlasKanbanModel;
  C, I: Integer;
  Line: string;
begin
  Model := TAtlasKanbanModel.Create(nil);
  try
    Model.AddColumn('todo', 'To do');
    Model.AddColumn('active', 'In progress', 2);
    Model.AddColumn('done', 'Done');
    Model.AddCard('todo', 'order-42', 'Prepare customer order', 'Pick, pack and label');
    Model.AddCard('todo', 'invoice-7', 'Send invoice 7');
    Model.AddCard('todo', 'audit', 'Stock audit');
    Model.MoveCard('order-42', 'active');
    Model.MoveCard('audit', 'active');
    Model.MoveCard('audit', 'active', 0);   // reorder: audit goes first
    for C := 0 to Model.ColumnCount - 1 do
    begin
      Line := Model.ColumnTitle(C) + ':';
      for I := 0 to Model.CardCount(C) - 1 do
        Line := Line + ' ' + Model.Card(C, I).ID;
      Writeln(Line);
    end;
  finally
    Model.Free;
  end;
end;

It prints To do: invoice-7, In progress: audit order-42 and Done:.

Enforce a workflow rule with BeforeMove, and see how a veto differs from a refused move: a veto makes MoveCard return False, a full column raises EAtlasKanban.

pascal
uses System.SysUtils, Atlas.Kanban;

type
  TDoneRule = class
    procedure BeforeMove(Sender: TObject; const CardID, FromColumn, ToColumn: string; var Allow: Boolean);
  end;

// A card may enter "done" only from "review".
procedure TDoneRule.BeforeMove(Sender: TObject; const CardID, FromColumn, ToColumn: string; var Allow: Boolean);
begin
  Allow := (ToColumn <> 'done') or (FromColumn = 'review');
end;

procedure ShowMoveRules;
var
  Model: TAtlasKanbanModel;
  Rule: TDoneRule;

  procedure Attempt(const CardID, ColumnID: string);
  var
    Outcome: string;
  begin
    try
      Outcome := BoolToStr(Model.MoveCard(CardID, ColumnID), True);
    except
      on E: EAtlasKanban do
        Outcome := E.Message;
    end;
    Writeln(CardID, ' -> ', ColumnID, ': ', Outcome);
  end;

begin
  Model := TAtlasKanbanModel.Create(nil);
  Rule := TDoneRule.Create;
  try
    Model.AddColumn('todo', 'To do');
    Model.AddColumn('review', 'Review', 1);
    Model.AddColumn('done', 'Done');
    Model.AddCard('todo', 'a', 'First');
    Model.AddCard('todo', 'b', 'Second');
    Model.BeforeMove := Rule.BeforeMove;
    Attempt('a', 'done');     // vetoed by the rule
    Attempt('a', 'review');
    Attempt('b', 'review');   // Review holds one card at most
    Attempt('a', 'done');     // allowed from Review
  finally
    Model.Free;
    Rule.Free;
  end;
end;

It prints a -> done: False, a -> review: True, b -> review: Column work-in-progress limit reached and a -> done: True.

Save a board as JSON, load it into another model, and see that a bad document leaves the board untouched.

pascal
uses System.SysUtils, Atlas.Kanban;

procedure RoundTrip;
var
  Original, Restored: TAtlasKanbanModel;
  Saved: string;
begin
  Original := TAtlasKanbanModel.Create(nil);
  Restored := TAtlasKanbanModel.Create(nil);
  try
    Original.AddColumn('todo', 'To do', 5);
    Original.AddCard('todo', 'order-42', 'Prepare customer order', 'Pick, pack and label');
    Saved := Original.ToJSON;
    Writeln(Saved);
    Restored.LoadJSON(Saved);
    Writeln('Same board: ', Restored.ToJSON = Saved);
    try
      Restored.LoadJSON('{"schema":1,"columns":[{"id":"x","title":"X","limit":0,"cards":[]},' +
        '{"id":"x","title":"Again","limit":0,"cards":[]}]}');
    except
      on E: EAtlasKanban do
        Writeln('Refused: ', E.Message, '; still ', Restored.ColumnCount, ' column(s)');
    end;
  finally
    Restored.Free;
    Original.Free;
  end;
end;

It prints {"schema":1,"columns":[{"id":"todo","title":"To do","limit":5,"cards":[{"id":"order-42","title":"Prepare customer order","detail":"Pick, pack and label"}]}]}, then Same board: TRUE and Refused: Invalid or duplicate column; still 1 column(s).

Use the syntax lexer on its own, here to list the comments in some source. Start is 0-based.

pascal
uses System.SysUtils, Atlas.Syntax;

procedure ListComments(const Source: string);
var
  Token: TAtlasToken;
begin
  for Token in AtlasPascalTokens(Source) do
    if Token.Kind = atkComment then
      Writeln(Token.Start, ': ', Copy(Source, Token.Start + 1, Token.Count));
end;

ListComments('X := 1; // one' + #10 + '{ two } Y := ''// not a comment''; (* three *)') prints 8: // one, 15: { two } and 48: (* three *); the text inside the string is not a comment.

Read an update manifest and compare its version with the running one, without any network access.

pascal
uses System.SysUtils, Atlas.Updater;

procedure CheckManifest;
const
  Manifest = '{"schema":1,"version":"2.1.0","url":"https://updates.example.com/myapp-2.1.0.bin",' +
    '"size":1048576,"sha256":"9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"}';
var
  Updater: TAtlasWebUpdater;
  Info: TAtlasUpdateInfo;
begin
  Updater := TAtlasWebUpdater.Create(nil);
  try
    Info := Updater.ParseManifest(Manifest);
    Writeln(Info.Version, ', ', Info.Size, ' bytes, sha256 ', Info.SHA256);
    Writeln('Newer than 2.0.9: ', TAtlasWebUpdater.CompareVersions(Info.Version, '2.0.9') > 0);
    Writeln('Newer than 2.1.0.0: ', TAtlasWebUpdater.CompareVersions(Info.Version, '2.1.0.0') > 0);
  finally
    Updater.Free;
  end;
end;

It prints 2.1.0, 1048576 bytes, sha256 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 (the digest in lower case), Newer than 2.0.9: TRUE and Newer than 2.1.0.0: FALSE.

Give the board the arrow keys. Create TKeyboardKanban wherever you would create TAtlasKanban.

pascal
uses Winapi.Windows, Winapi.Messages, Atlas.Kanban;

type
  TKeyboardKanban = class(TAtlasKanban)
  protected
    procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
  end;

procedure TKeyboardKanban.WMGetDlgCode(var Message: TWMGetDlgCode);
begin
  inherited;
  Message.Result := Message.Result or DLGC_WANTARROWS;
end;

With it, Up and Down select the previous or next card, Left and Right select a card in the neighbouring column, and Ctrl+Left and Ctrl+Right move the selected card to the end of the neighbouring column, even when buttons or other controls share the form. Without it, the form uses those keys to move the focus.

Tutorial

Sample project: Kanban board.

Goal: build a window with a Kanban board whose cards move from code, from buttons, with the mouse and with the keyboard, under a rule that keeps cards from skipping review.

  1. Create the project. In Delphi choose File > New > VCL Forms Application, then Project > Remove from Project to remove Unit1: this form is built in code. Save the project as KanbanBoard. Open Project > Options > Building > Delphi Compiler > Search path and add the Atlas UI src folder and the Atlas VCL src folder. Open the program source with Project > View Source; the steps below replace everything in it except the {$R *.res} line the IDE wrote, which you keep under the first line.

  2. Start the program with the units it needs. Atlas.Kanban holds both the model and the board.

    pascal
    program KanbanBoard;
    
    uses
      System.SysUtils, System.Classes, Winapi.Windows, Winapi.Messages, Vcl.Forms, Vcl.Controls,
      Vcl.StdCtrls, Vcl.ExtCtrls, Atlas.Kanban;
  3. Declare two classes. TKeyboardKanban is the board with one addition: it asks Windows for the arrow keys, which TAtlasKanban does not, so the form stops using them to move the focus. TBoardForm is the window.

    pascal
    type
      TKeyboardKanban = class(TAtlasKanban)
      protected
        procedure WMGetDlgCode(var Message: TWMGetDlgCode); message WM_GETDLGCODE;
      end;
    
      TBoardForm = class(TForm)
      private
        FModel: TAtlasKanbanModel;
        FBoard: TAtlasKanban;
        FStatus: TLabel;
        FJson: TMemo;
        procedure BeforeMove(Sender: TObject; const CardID, FromColumn, ToColumn: string; var Allow: Boolean);
        procedure ModelChanged(Sender: TObject);
        procedure MoveLeftClick(Sender: TObject);
        procedure MoveRightClick(Sender: TObject);
        procedure ShowJsonClick(Sender: TObject);
        function ColumnOfCard(const CardID: string): Integer;
        procedure MoveSelected(Delta: Integer);
      public
        constructor Create(AOwner: TComponent); override;
      end;
    
    procedure TKeyboardKanban.WMGetDlgCode(var Message: TWMGetDlgCode);
    begin
      inherited;
      Message.Result := Message.Result or DLGC_WANTARROWS;
    end;
  4. Start the constructor and fill the model. It is created first because everything else refers to it. The second column has a work-in-progress limit of 2.

    pascal
    constructor TBoardForm.Create(AOwner: TComponent);
    var
      Bar: TPanel;
      Button: TButton;
    begin
      inherited CreateNew(AOwner);
      Caption := 'Atlas UI: Kanban board';
      Width := 1160;
      Height := 700;
      Position := poScreenCenter;
      Font.Name := 'Segoe UI';
      Font.Size := 10;
    
      FModel := TAtlasKanbanModel.Create(Self);
      FModel.AddColumn('todo', 'To do');
      FModel.AddColumn('active', 'In progress', 2);
      FModel.AddColumn('review', 'Review');
      FModel.AddColumn('done', 'Done');
      FModel.AddCard('todo', 'order-42', 'Prepare customer order', 'Pick, pack and label the parcel');
      FModel.AddCard('todo', 'invoice-7', 'Send invoice 7', 'Net 30, attach the delivery note');
      FModel.AddCard('todo', 'audit', 'Stock audit', 'Count aisle 3 before Friday');
      FModel.AddCard('review', 'catalog', 'Update the catalog', 'New prices for autumn');
      FModel.AddCard('done', 'backup', 'Nightly backup', 'Verified restore on the test server');
  5. Add a toolbar with three buttons, a status line and a memo for the JSON export, still inside the constructor.

    pascal
      Bar := TPanel.Create(Self);
      Bar.Parent := Self;
      Bar.Align := alTop;
      Bar.Height := 44;
      Bar.BevelOuter := bvNone;
      Button := TButton.Create(Self);
      Button.Parent := Bar;
      Button.SetBounds(8, 8, 150, 28);
      Button.Caption := 'Move left (Ctrl+Left)';
      Button.OnClick := MoveLeftClick;
      Button := TButton.Create(Self);
      Button.Parent := Bar;
      Button.SetBounds(166, 8, 160, 28);
      Button.Caption := 'Move right (Ctrl+Right)';
      Button.OnClick := MoveRightClick;
      Button := TButton.Create(Self);
      Button.Parent := Bar;
      Button.SetBounds(334, 8, 120, 28);
      Button.Caption := 'Show JSON';
      Button.OnClick := ShowJsonClick;
      FJson := TMemo.Create(Self);
      FJson.Parent := Self;
      FJson.Align := alBottom;
      FJson.Height := 110;
      FJson.ReadOnly := True;
      FJson.ScrollBars := ssVertical;
      FJson.Font.Name := 'Consolas';
      FStatus := TLabel.Create(Self);
      FStatus.Parent := Self;
      FStatus.Align := alBottom;
      FStatus.AlignWithMargins := True;
  6. Create the board, give it the model, attach the rule and the change handler, and move one card in code. The constructor ends here.

    pascal
      FBoard := TKeyboardKanban.Create(Self);
      FBoard.Parent := Self;
      FBoard.Align := alClient;
      FBoard.Model := FModel;
    
      FModel.BeforeMove := BeforeMove;
      FModel.OnChange := ModelChanged;
      FModel.MoveCard('order-42', 'active');
      FBoard.SelectCard('order-42');
      ActiveControl := FBoard;
      ModelChanged(FModel);
    end;
  7. Write the rule and the status line. BeforeMove only decides; it must not change the model. ModelChanged runs after every change and counts the cards per column.

    pascal
    procedure TBoardForm.BeforeMove(Sender: TObject; const CardID, FromColumn, ToColumn: string; var Allow: Boolean);
    begin
      Allow := (ToColumn <> 'done') or (FromColumn = 'review');
    end;
    
    procedure TBoardForm.ModelChanged(Sender: TObject);
    var
      Text: string;
      C: Integer;
    begin
      Text := '';
      for C := 0 to FModel.ColumnCount - 1 do
        Text := Text + Format('%s: %d   ', [FModel.ColumnTitle(C), FModel.CardCount(C)]);
      if FBoard.LastError <> '' then
        Text := Text + '   last error: ' + FBoard.LastError;
      FStatus.Caption := Text;
    end;
  8. Write the button handlers. The model addresses columns by position, so ColumnOfCard finds the selected card's column; MoveSelectedTo does the move and reports a refusal in LastError instead of raising.

    pascal
    function TBoardForm.ColumnOfCard(const CardID: string): Integer;
    var
      C, I: Integer;
    begin
      for C := 0 to FModel.ColumnCount - 1 do
        for I := 0 to FModel.CardCount(C) - 1 do
          if FModel.Card(C, I).ID = CardID then
            Exit(C);
      Result := -1;
    end;
    
    procedure TBoardForm.MoveSelected(Delta: Integer);
    var
      Target: Integer;
    begin
      Target := ColumnOfCard(FBoard.SelectedCard);
      if Target < 0 then
      begin
        FStatus.Caption := 'Click a card first.';
        Exit;
      end;
      Target := Target + Delta;
      if (Target < 0) or (Target >= FModel.ColumnCount) then
        Exit;
      if not FBoard.MoveSelectedTo(FModel.ColumnID(Target)) then
        FStatus.Caption := 'Not moved: ' + FBoard.LastError;
      FBoard.SetFocus;
    end;
    
    procedure TBoardForm.MoveLeftClick(Sender: TObject);
    begin
      MoveSelected(-1);
    end;
    
    procedure TBoardForm.MoveRightClick(Sender: TObject);
    begin
      MoveSelected(1);
    end;
    
    procedure TBoardForm.ShowJsonClick(Sender: TObject);
    begin
      FJson.Text := FModel.ToJSON;
    end;
  9. End the program with the usual main block.

    pascal
    var
      Form: TBoardForm;
    
    begin
      Application.Initialize;
      Application.MainFormOnTaskbar := True;
      Application.CreateForm(TBoardForm, Form);
      Application.Run;
    end.
  10. Run it (F9). The window shows four columns. The headers read To do 2, In progress 1/2, Review 1 and Done 1; Prepare customer order is in In progress with a blue outline, because the constructor moved and selected it; the status line reads To do: 2 In progress: 1 Review: 1 Done: 1.

  11. Try the moves. Press Left: Send invoice 7 is selected. Press Ctrl+Right: it moves to the end of In progress, whose header now reads 2/2. Press Left (Stock audit is selected) and Ctrl+Right: nothing moves and the board shows Column work-in-progress limit reached in red at its bottom left; Move right gives the same refusal and writes Not moved: Column work-in-progress limit reached in the status line. Drag Update the catalog onto Done: it moves, because it comes from Review. Drag Prepare customer order onto Done: the rule declines it and the board shows Move declined. Show JSON puts the whole board, as one line of JSON, in the memo.

To adapt it, save FModel.ToJSON to a file in the form's OnClose and call FModel.LoadJSON with it in the constructor instead of the AddColumn and AddCard calls, so the board survives a restart. The sample project is the same program with one addition: a -image switch that saves the window as a PNG file instead of opening it.

Troubleshooting

The arrow keys move the focus to another control instead of selecting cards. TAtlasKanban does not answer WM_GETDLGCODE, so the form treats the arrow keys as dialog keys. Use the TKeyboardKanban subclass from Examples; nothing else is needed.

The syntax editor shows the text in black. The text was assigned before the editor had a Parent, and colouring needs a window. Assign Parent first, or call Highlight once after assigning it.

EAtlasKanban: Cannot mutate the board during a move callback. Your BeforeMove handler, or something it calls, changed the model. The handler may only read the model and set Allow. Make follow-up changes after MoveCard returns True, or queue them with TThread.ForceQueue.

LoadJSON raises EJSONException with Value 'detail' not found (or 'limit', 'cards', ...). The document was written by hand or by another program and leaves out a field. LoadJSON needs every field that ToJSON writes, including an empty detail and a limit of 0. Catch EJSONException as well as EAtlasKanban when you load files; the board is unchanged either way.

A card dropped with the mouse lands at the bottom of the column. The board has no drop position: a drop appends to the end of the target column, and a drop on the card's own column moves it to the end. Use MoveCard(ID, ColumnID, Index) from code to put a card at a position.

Updates require HTTPS in a test against a local server. Plain HTTP is accepted only with AllowLoopbackHTTP set to True and only for the host 127.0.0.1. Use http://127.0.0.1:<port>/..., not localhost.

HTTP status 301 or HTTP status 302. The server redirected, and the updater does not follow redirects on purpose. Put the final HTTPS address in the manifest's url and in the FetchManifest call.

Existing cache file has different content. The cache folder already holds a file with the staged name (atlas-<version>-<sha256>.bin) but other bytes, for example one changed by hand. The updater never overwrites it. Delete the file, or stage into another folder.

The window stops responding while it checks for updates. FetchManifest and StageUpdate are synchronous and can take up to the 10-second connection timeout plus the 15-second response timeout, or much longer for a large file. Run them on a worker thread, as in the TAtlasWebUpdater reference example.

Windows asks for administrator rights before a small test program starts. Windows treats a 32-bit program whose file name contains words such as "update", "setup", "install" or "patch", and which has no application manifest, as an installer. Programs built in the IDE have a manifest by default; a program built with the command-line compiler and no manifest resource does not. Rename the program or add a manifest that sets requestedExecutionLevel to asInvoker.

Undo empties the syntax editor after a file was loaded. The text history starts with the empty text the editor had when it was created, and loading a file is one more change. There is no method to clear the history, and a new editor starts with the same empty entry, so this cannot be avoided from outside the component; it is a known limitation.