Case Study 1: Building a Text Editor
Overview
A text editor is the quintessential desktop application — it exercises every concept in this chapter: menus with shortcuts, file dialogs, a toolbar, a status bar, an action list, and proper application lifecycle management. We build "PascalPad," a simple but functional text editor in under 300 lines.
Problem Statement
Build a text editor with the following features:
- A TMemo filling the window for text editing.
- File menu: New, Open, Save, Save As, Exit — with standard keyboard shortcuts.
- Edit menu: Undo, Cut, Copy, Paste, Select All.
- Format menu: Word Wrap toggle, Font selection.
- A toolbar with buttons for New, Open, Save, Cut, Copy, Paste.
- A status bar showing line count, character count, and modified status.
- Unsaved changes detection with prompt on close.
- Window title shows filename and modification indicator.
Architecture: TActionList-Centered Design
We put TActionList at the center of our architecture. Every command is an action. Menu items and toolbar buttons are views of those actions.
unit PascalPadMain;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, Forms, Controls, Graphics, Dialogs, Menus, ComCtrls,
ActnList, StdActns, StdCtrls;
type
{ TfrmPascalPad }
TfrmPascalPad = class(TForm)
ActionList1: TActionList;
MainMenu1: TMainMenu;
ToolBar1: TToolBar;
StatusBar1: TStatusBar;
Memo1: TMemo;
OpenDialog1: TOpenDialog;
SaveDialog1: TSaveDialog;
FontDialog1: TFontDialog;
ImageList1: TImageList;
{ Actions }
actNew: TAction;
actOpen: TAction;
actSave: TAction;
actSaveAs: TAction;
actExit: TAction;
actUndo: TAction;
actCut: TAction;
actCopy: TAction;
actPaste: TAction;
actSelectAll: TAction;
actWordWrap: TAction;
actFont: TAction;
{ Menu items — all linked to actions }
mnuFile: TMenuItem;
mnuEdit: TMenuItem;
mnuFormat: TMenuItem;
miNew: TMenuItem;
miOpen: TMenuItem;
miSave: TMenuItem;
miSaveAs: TMenuItem;
miExit: TMenuItem;
miUndo: TMenuItem;
miCut: TMenuItem;
miCopy: TMenuItem;
miPaste: TMenuItem;
miSelectAll: TMenuItem;
miWordWrap: TMenuItem;
miFont: TMenuItem;
procedure actNewExecute(Sender: TObject);
procedure actOpenExecute(Sender: TObject);
procedure actSaveExecute(Sender: TObject);
procedure actSaveAsExecute(Sender: TObject);
procedure actExitExecute(Sender: TObject);
procedure actUndoExecute(Sender: TObject);
procedure actCutExecute(Sender: TObject);
procedure actCopyExecute(Sender: TObject);
procedure actPasteExecute(Sender: TObject);
procedure actSelectAllExecute(Sender: TObject);
procedure actWordWrapExecute(Sender: TObject);
procedure actFontExecute(Sender: TObject);
procedure actSaveUpdate(Sender: TObject);
procedure actCopyUpdate(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
procedure FormCreate(Sender: TObject);
procedure Memo1Change(Sender: TObject);
private
FCurrentFile: string;
FModified: Boolean;
procedure UpdateTitle;
procedure UpdateStatus;
function ConfirmSave: Boolean;
procedure NewFile;
public
end;
implementation
{$R *.lfm}
procedure TfrmPascalPad.FormCreate(Sender: TObject);
begin
{ Action setup }
actNew.ShortCut := ShortCut(Ord('N'), [ssCtrl]);
actOpen.ShortCut := ShortCut(Ord('O'), [ssCtrl]);
actSave.ShortCut := ShortCut(Ord('S'), [ssCtrl]);
actSaveAs.ShortCut := ShortCut(Ord('S'), [ssCtrl, ssShift]);
actUndo.ShortCut := ShortCut(Ord('Z'), [ssCtrl]);
actCut.ShortCut := ShortCut(Ord('X'), [ssCtrl]);
actCopy.ShortCut := ShortCut(Ord('C'), [ssCtrl]);
actPaste.ShortCut := ShortCut(Ord('V'), [ssCtrl]);
actSelectAll.ShortCut := ShortCut(Ord('A'), [ssCtrl]);
{ Dialog setup }
OpenDialog1.Filter := 'Text Files (*.txt)|*.txt|Pascal Files (*.pas)|*.pas|All Files (*.*)|*.*';
SaveDialog1.Filter := OpenDialog1.Filter;
SaveDialog1.DefaultExt := 'txt';
{ Memo setup }
Memo1.Align := alClient;
Memo1.ScrollBars := ssBoth;
Memo1.Font.Name := 'Consolas';
Memo1.Font.Size := 11;
{ Status bar }
StatusBar1.Panels.Clear;
with StatusBar1.Panels.Add do begin Width := 150; Text := 'Lines: 1'; end;
with StatusBar1.Panels.Add do begin Width := 150; Text := 'Characters: 0'; end;
with StatusBar1.Panels.Add do begin Width := 100; Text := 'Ready'; end;
NewFile;
end;
procedure TfrmPascalPad.NewFile;
begin
Memo1.Lines.Clear;
FCurrentFile := '';
FModified := False;
UpdateTitle;
UpdateStatus;
end;
procedure TfrmPascalPad.actNewExecute(Sender: TObject);
begin
if FModified and not ConfirmSave then Exit;
NewFile;
end;
procedure TfrmPascalPad.actOpenExecute(Sender: TObject);
begin
if FModified and not ConfirmSave then Exit;
if OpenDialog1.Execute then
begin
Memo1.Lines.LoadFromFile(OpenDialog1.FileName);
FCurrentFile := OpenDialog1.FileName;
FModified := False;
UpdateTitle;
UpdateStatus;
end;
end;
procedure TfrmPascalPad.actSaveExecute(Sender: TObject);
begin
if FCurrentFile = '' then
actSaveAsExecute(Sender)
else
begin
Memo1.Lines.SaveToFile(FCurrentFile);
FModified := False;
UpdateTitle;
StatusBar1.Panels[2].Text := 'Saved';
end;
end;
procedure TfrmPascalPad.actSaveAsExecute(Sender: TObject);
begin
SaveDialog1.FileName := FCurrentFile;
if SaveDialog1.Execute then
begin
Memo1.Lines.SaveToFile(SaveDialog1.FileName);
FCurrentFile := SaveDialog1.FileName;
FModified := False;
UpdateTitle;
StatusBar1.Panels[2].Text := 'Saved';
end;
end;
procedure TfrmPascalPad.actExitExecute(Sender: TObject);
begin
Close;
end;
procedure TfrmPascalPad.actUndoExecute(Sender: TObject);
begin
Memo1.Undo;
end;
procedure TfrmPascalPad.actCutExecute(Sender: TObject);
begin
Memo1.CutToClipboard;
end;
procedure TfrmPascalPad.actCopyExecute(Sender: TObject);
begin
Memo1.CopyToClipboard;
end;
procedure TfrmPascalPad.actPasteExecute(Sender: TObject);
begin
Memo1.PasteFromClipboard;
end;
procedure TfrmPascalPad.actSelectAllExecute(Sender: TObject);
begin
Memo1.SelectAll;
end;
procedure TfrmPascalPad.actWordWrapExecute(Sender: TObject);
begin
Memo1.WordWrap := not Memo1.WordWrap;
actWordWrap.Checked := Memo1.WordWrap;
if Memo1.WordWrap then
Memo1.ScrollBars := ssVertical
else
Memo1.ScrollBars := ssBoth;
end;
procedure TfrmPascalPad.actFontExecute(Sender: TObject);
begin
FontDialog1.Font := Memo1.Font;
if FontDialog1.Execute then
Memo1.Font := FontDialog1.Font;
end;
{ OnUpdate handlers — called automatically }
procedure TfrmPascalPad.actSaveUpdate(Sender: TObject);
begin
actSave.Enabled := FModified;
end;
procedure TfrmPascalPad.actCopyUpdate(Sender: TObject);
begin
actCopy.Enabled := Memo1.SelLength > 0;
actCut.Enabled := Memo1.SelLength > 0;
end;
procedure TfrmPascalPad.Memo1Change(Sender: TObject);
begin
FModified := True;
UpdateTitle;
UpdateStatus;
end;
procedure TfrmPascalPad.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
if FModified then
CanClose := ConfirmSave
else
CanClose := True;
end;
function TfrmPascalPad.ConfirmSave: Boolean;
begin
case MessageDlg('Unsaved Changes',
'Save changes before continuing?',
mtConfirmation, [mbYes, mbNo, mbCancel], 0) of
mrYes:
begin
actSaveExecute(nil);
Result := not FModified; { True if save succeeded }
end;
mrNo: Result := True;
else Result := False; { Cancel }
end;
end;
procedure TfrmPascalPad.UpdateTitle;
var
FileName: string;
begin
if FCurrentFile <> '' then
FileName := ExtractFileName(FCurrentFile)
else
FileName := 'Untitled';
if FModified then
Caption := '* ' + FileName + ' — PascalPad'
else
Caption := FileName + ' — PascalPad';
end;
procedure TfrmPascalPad.UpdateStatus;
begin
StatusBar1.Panels[0].Text := Format('Lines: %d', [Memo1.Lines.Count]);
StatusBar1.Panels[1].Text := Format('Characters: %d', [Length(Memo1.Text)]);
end;
end.
Key Design Observations
-
Every command is an action. There are no orphan event handlers — every operation flows through the action list. Adding a toolbar button for any action requires only setting its
Actionproperty. -
OnUpdate keeps UI synchronized. The Save action is grayed out when there are no changes. Cut and Copy are grayed out when nothing is selected. This happens automatically.
-
ConfirmSave is reusable. The same confirmation logic serves New, Open, Close, and Exit — four different workflows that all need to check for unsaved changes.
-
The editor is functional in ~200 lines. This is the power of RAD: the framework handles window management, clipboard operations, file dialogs, and widget rendering. You write only the business logic.
Extensions to Try
- Find and Replace: Add a modeless Find/Replace dialog (Edit > Find, Ctrl+F).
- Line numbers: Replace TMemo with TSynEdit (a syntax-highlighting code editor component).
- Recent files: Track the last 5 opened files in a submenu.
- Print: Add File > Print using the Printers/PrintersDlgs units.
- Multiple documents: Use TPageControl with one tab per document (MDI-style).