diff --git a/forms/README.md b/forms/README.md index b564f7d..0240bf1 100644 --- a/forms/README.md +++ b/forms/README.md @@ -322,6 +322,10 @@ messages are available, dispatching each command as it arrives. | `Panel` | TPanel | Cosmetic container panel | | `ScrollBar` | TScrollBar | Horizontal or vertical scrollbar| | `MediaPlayer` | TMediaPlayer | MCI media player control | +| `MainMenu` | TMainMenu | Form main menu bar | +| `PopupMenu` | TPopupMenu | Context (right-click) menu | +| `MenuItem` | TMenuItem | Menu item (child of menu) | +| `RadioGroup` | TRadioGroup | Grouped radio buttons | ### Creating Controls @@ -359,12 +363,12 @@ CTRL.SET 1 3 Text="world" Enabled=0 ### Caption -- **Applies to:** Label, Button, CheckBox, GroupBox, RadioButton, Panel +- **Applies to:** Label, Button, CheckBox, GroupBox, RadioButton, Panel, MenuItem, RadioGroup - **Format:** Quoted string - **Example:** `Caption="Submit"` The display text for labels, buttons, check boxes, group boxes, radio -buttons, and panels. +buttons, panels, menu items, and radio groups. ### Text @@ -381,7 +385,7 @@ CTRL.SET 1 5 Text="Line one\nLine two\nLine three" ### Items -- **Applies to:** ListBox, ComboBox +- **Applies to:** ListBox, ComboBox, RadioGroup - **Format:** Quoted string, items separated by `\n` - **Example:** `Items="Red\nGreen\nBlue"` @@ -390,7 +394,7 @@ new items are added. ### Checked -- **Applies to:** CheckBox, RadioButton +- **Applies to:** CheckBox, RadioButton, MenuItem - **Format:** `0` (unchecked) or `1` (checked) - **Example:** `Checked=1` @@ -433,7 +437,7 @@ Maximum number of characters the user can type. ### ItemIndex -- **Applies to:** ListBox, ComboBox +- **Applies to:** ListBox, ComboBox, RadioGroup - **Format:** Integer (-1 = no selection) - **Example:** `ItemIndex=2` @@ -588,6 +592,41 @@ Pseudo-property that triggers a method call instead of setting a value. Valid commands: `Open`, `Play`, `Stop`, `Close`, `Pause`, `Resume`, `Rewind`, `Next`, `Previous`. +### Parent + +- **Applies to:** MenuItem +- **Format:** Integer (ctrlId of parent menu or menu item) +- **Example:** `Parent=1` + +Specifies the parent for a menu item. The parent can be a MainMenu, +PopupMenu, or another MenuItem (for submenus). + +### Columns + +- **Applies to:** RadioGroup +- **Format:** Integer +- **Example:** `Columns=2` + +Number of columns for the radio button layout. + +### ShortCut + +- **Applies to:** MenuItem +- **Format:** Integer (Delphi ShortCut value) +- **Example:** `ShortCut=16467` + +Keyboard accelerator for the menu item. Uses Delphi's ShortCut +encoding (virtual key + modifier flags). + +### PopupMenu + +- **Applies to:** Any visual control +- **Format:** Integer (ctrlId of a PopupMenu) +- **Example:** `PopupMenu=2` + +Associates a PopupMenu with a control. When the user right-clicks +the control, the popup menu is displayed. + ### BasePath `BasePath` is a property on `TFormClient` (not a protocol property). @@ -619,6 +658,8 @@ No `EVENT.BIND` command is needed. | ListBox | Select | ` "selected text"` | | ComboBox | Select | ` "selected text"` | | ComboBox | Change | `"new text"` | +| MenuItem | Click | (none) | +| RadioGroup | Click | `` | ### Opt-in Events @@ -666,6 +707,25 @@ EVENT 1 3 KeyDown 13 (client sends back when Enter pressed) EVENT.UNBIND 1 3 KeyDown (server disconnects) ``` +### Menu Hierarchy Example + +Menus are built using flat CTRL.CREATE commands with `Parent` properties +to establish the hierarchy: + +``` +CTRL.CREATE 1 1 MainMenu 0 0 0 0 +CTRL.CREATE 1 2 MenuItem 0 0 0 0 Caption="&File" Parent=1 +CTRL.CREATE 1 3 MenuItem 0 0 0 0 Caption="&Open" Parent=2 ShortCut=16463 +CTRL.CREATE 1 4 MenuItem 0 0 0 0 Caption="&Save" Parent=2 ShortCut=16467 +CTRL.CREATE 1 5 MenuItem 0 0 0 0 Caption="E&xit" Parent=2 +CTRL.CREATE 1 6 MenuItem 0 0 0 0 Caption="&Help" Parent=1 +CTRL.CREATE 1 7 MenuItem 0 0 0 0 Caption="&About" Parent=6 +``` + +This creates a menu bar with File (Open, Save, Exit) and Help (About). +MenuItem ctrl 2 is a child of MainMenu ctrl 1; ctrl 3-5 are children of +the File item (ctrl 2); ctrl 7 is a child of the Help item (ctrl 6). + ## String Encoding All strings in the protocol are double-quoted. The following escape @@ -697,9 +757,8 @@ any protocol or application code. ## Limitations - **RadioButton grouping:** All RadioButtons on a form belong to one - group. There is no support for multiple independent radio groups - per form (the protocol has a flat parent model with no child - containment). + group. Use RadioGroup for multiple independent radio groups per + form. - **Image format:** Only BMP files are supported for the Picture property. - **dfm2form and Picture:** The converter cannot extract image diff --git a/forms/dfm2form.c b/forms/dfm2form.c index f249a00..d2570c5 100644 --- a/forms/dfm2form.c +++ b/forms/dfm2form.c @@ -32,7 +32,11 @@ typedef enum { ctRadioButton, ctPanel, ctScrollBar, - ctMediaPlayer + ctMediaPlayer, + ctMainMenu, + ctPopupMenu, + ctMenuItem, + ctRadioGroup } CtrlTypeE; typedef struct { @@ -103,6 +107,11 @@ typedef struct { bool hasAutoOpen; bool hasFileName; bool hasDeviceType; + int32_t parentCtrlIdx; + int32_t columns; + int32_t shortCut; + bool hasColumns; + bool hasShortCut; } DfmCtrlT; typedef struct { @@ -143,7 +152,7 @@ static void escapeStr(const char *src, char *dst, int32_t dstSize); static void initCtrl(DfmCtrlT *ctrl); static void initForm(DfmFormT *form); static CtrlTypeE mapClassName(const char *className); -static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot); +static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot, int32_t parentIdx); static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, DfmCtrlT *ctrl, bool isForm); static int32_t readByte(const uint8_t *data, int32_t size, int32_t *pos); static int32_t readInt16LE(const uint8_t *data, int32_t size, int32_t *pos); @@ -360,6 +369,18 @@ static CtrlTypeE mapClassName(const char *className) { if (stricmp_local(className, "TMediaPlayer") == 0) { return ctMediaPlayer; } + if (stricmp_local(className, "TMainMenu") == 0) { + return ctMainMenu; + } + if (stricmp_local(className, "TPopupMenu") == 0) { + return ctPopupMenu; + } + if (stricmp_local(className, "TMenuItem") == 0) { + return ctMenuItem; + } + if (stricmp_local(className, "TRadioGroup") == 0) { + return ctRadioGroup; + } return ctUnknown; } @@ -370,13 +391,14 @@ static CtrlTypeE mapClassName(const char *className) { static void initCtrl(DfmCtrlT *ctrl) { memset(ctrl, 0, sizeof(DfmCtrlT)); - ctrl->enabled = 1; - ctrl->visible = 1; - ctrl->itemIndex = -1; - ctrl->tabOrder = -1; - ctrl->bevelOuter = 1; // bvRaised - ctrl->largeChange = 1; - ctrl->smallChange = 1; + ctrl->enabled = 1; + ctrl->visible = 1; + ctrl->itemIndex = -1; + ctrl->tabOrder = -1; + ctrl->bevelOuter = 1; // bvRaised + ctrl->largeChange = 1; + ctrl->smallChange = 1; + ctrl->parentCtrlIdx = -1; } @@ -790,6 +812,12 @@ static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, Dfm ctrl->autoOpen = readIntValue(data, size, pos, tag); } ctrl->hasAutoOpen = true; + } else if (!isForm && stricmp_local(propName, "Columns") == 0) { + ctrl->columns = readIntValue(data, size, pos, tag); + ctrl->hasColumns = true; + } else if (!isForm && stricmp_local(propName, "ShortCut") == 0) { + ctrl->shortCut = readIntValue(data, size, pos, tag); + ctrl->hasShortCut = true; } else { skipValue(data, size, pos, tag); } @@ -801,7 +829,7 @@ static void parseProperties(const uint8_t *data, int32_t size, int32_t *pos, Dfm // Parse a component (form or child control) recursively // --------------------------------------------------------------------------- -static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot) { +static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmFormT *form, bool isRoot, int32_t parentIdx) { // Check for flags byte (Delphi 1.0 rarely uses this but handle it) uint8_t peek = data[*pos]; if ((peek & 0xF0) == 0xF0) { @@ -833,7 +861,7 @@ static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmF (*pos)++; // consume terminator break; } - parseComponent(data, size, pos, form, false); + parseComponent(data, size, pos, form, false, -1); } } else { // Child control @@ -851,7 +879,7 @@ static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmF (*pos)++; break; } - parseComponent(data, size, pos, form, false); + parseComponent(data, size, pos, form, false, -1); } return true; } @@ -861,22 +889,24 @@ static bool parseComponent(const uint8_t *data, int32_t size, int32_t *pos, DfmF exit(1); } - DfmCtrlT *ctrl = &form->ctrls[form->ctrlCount]; + int32_t myIdx = form->ctrlCount; + DfmCtrlT *ctrl = &form->ctrls[myIdx]; initCtrl(ctrl); - ctrl->type = type; + ctrl->type = type; + ctrl->parentCtrlIdx = parentIdx; snprintf(ctrl->name, sizeof(ctrl->name), "%s", instName); parseProperties(data, size, pos, form, ctrl, false); form->ctrlCount++; - // Skip nested children (controls within controls, e.g., panels) + // Recurse into children (menu items, panels, etc.) while (*pos < size) { peek = data[*pos]; if (peek == 0x00) { (*pos)++; break; } - parseComponent(data, size, pos, form, false); + parseComponent(data, size, pos, form, false, myIdx); } } @@ -931,13 +961,16 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl) "Unknown", "Label", "Edit", "Button", "CheckBox", "ListBox", "ComboBox", "Memo", "Image", "GroupBox", "RadioButton", "Panel", - "ScrollBar", "MediaPlayer" + "ScrollBar", "MediaPlayer", + "MainMenu", "PopupMenu", "MenuItem", "RadioGroup" }; char escaped[8192]; - fprintf(out, "CTRL.CREATE %d %d %s %d %d %d %d", - formId, ctrlId, typeNames[ctrl->type], - ctrl->left, ctrl->top, ctrl->width, ctrl->height); + if (ctrl->type == ctMainMenu || ctrl->type == ctPopupMenu || ctrl->type == ctMenuItem) { + fprintf(out, "CTRL.CREATE %d %d %s 0 0 0 0", formId, ctrlId, typeNames[ctrl->type]); + } else { + fprintf(out, "CTRL.CREATE %d %d %s %d %d %d %d", formId, ctrlId, typeNames[ctrl->type], ctrl->left, ctrl->top, ctrl->width, ctrl->height); + } // Inline properties if (ctrl->hasCaption) { @@ -1023,6 +1056,15 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl) if (ctrl->hasAutoOpen && ctrl->autoOpen) { fprintf(out, " AutoOpen=1"); } + if (ctrl->type == ctMenuItem && ctrl->parentCtrlIdx >= 0) { + fprintf(out, " Parent=%d", ctrl->parentCtrlIdx + 1); + } + if (ctrl->hasColumns && ctrl->columns != 0) { + fprintf(out, " Columns=%d", ctrl->columns); + } + if (ctrl->hasShortCut && ctrl->shortCut != 0) { + fprintf(out, " ShortCut=%d", ctrl->shortCut); + } fprintf(out, "\n"); @@ -1030,7 +1072,7 @@ static void emitCtrl(FILE *out, int32_t formId, int32_t ctrlId, DfmCtrlT *ctrl) // Auto-wired: Button/CheckBox→Click, Edit→Change, ListBox→Select, // ComboBox→Select+Change, Memo→Change - bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox || ctrl->type == ctRadioButton); + bool autoClick = (ctrl->type == ctButton || ctrl->type == ctCheckBox || ctrl->type == ctRadioButton || ctrl->type == ctMenuItem || ctrl->type == ctRadioGroup); bool autoChange = (ctrl->type == ctEdit || ctrl->type == ctComboBox || ctrl->type == ctMemo || ctrl->type == ctScrollBar); bool autoSelect = (ctrl->type == ctListBox || ctrl->type == ctComboBox); @@ -1160,7 +1202,7 @@ int main(int argc, char *argv[]) { DfmFormT form; initForm(&form); int32_t pos = 4; // skip TPF0 - parseComponent(data, (int32_t)fileSize, &pos, &form, true); + parseComponent(data, (int32_t)fileSize, &pos, &form, true, -1); // Default caption if empty if (form.caption[0] == '\0') { diff --git a/forms/formcli.pas b/forms/formcli.pas index af13c8a..5289703 100644 --- a/forms/formcli.pas +++ b/forms/formcli.pas @@ -13,7 +13,7 @@ unit FormCli; interface uses - SysUtils, Classes, Controls, Forms, StdCtrls, ExtCtrls, MPlayer, WinTypes, WinProcs; + SysUtils, Classes, Controls, Forms, StdCtrls, ExtCtrls, Menus, MPlayer, WinTypes, WinProcs; const MaxMsgLen = 4096; @@ -29,7 +29,8 @@ type TCtrlTypeE = (ctUnknown, ctLabel, ctEdit, ctButton, ctCheckBox, ctListBox, ctComboBox, ctMemo, ctImage, ctGroupBox, ctRadioButton, ctPanel, - ctScrollBar, ctMediaPlayer); + ctScrollBar, ctMediaPlayer, + ctMainMenu, ctPopupMenu, ctMenuItem, ctRadioGroup); { Bound event flags } TBoundEvent = (beDblClick, beKeyDown, beKeyUp, @@ -42,7 +43,7 @@ type TFormCtrlRec = record CtrlId: Integer; CtrlType: TCtrlTypeE; - Control: TControl; + Control: TComponent; Bound: TBoundEvents; end; @@ -81,8 +82,8 @@ type procedure FreeCtrlRec(CR: PFormCtrlRec); { Property application } - procedure ApplyProp(CR: PFormCtrlRec; Key, Value: PChar); - procedure ApplyInlineProps(CR: PFormCtrlRec; P: PChar); + procedure ApplyProp(FR: PFormRec; CR: PFormCtrlRec; Key, Value: PChar); + procedure ApplyInlineProps(FR: PFormRec; CR: PFormCtrlRec; P: PChar); { Event wiring } procedure WireAutoEvents(CR: PFormCtrlRec); @@ -109,7 +110,9 @@ type procedure HandleClick(Sender: TObject); procedure HandleRadioButtonClick(Sender: TObject); procedure HandleScrollBarChange(Sender: TObject); + procedure HandleMenuItemClick(Sender: TObject); procedure HandleMediaPlayerNotify(Sender: TObject); + procedure HandleRadioGroupClick(Sender: TObject); { Outgoing event helpers } procedure SendEvent(FormId, CtrlId: Integer; const EventName: string; const Data: string); @@ -333,8 +336,12 @@ end; procedure TFormClient.FreeCtrlRec(CR: PFormCtrlRec); begin + { Menu items are owned by their parent menu and freed automatically } if CR^.Control <> nil then - CR^.Control.Free; + begin + if not (CR^.Control is TMenuItem) then + CR^.Control.Free; + end; Dispose(CR); end; @@ -403,7 +410,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'Click', ''); @@ -416,7 +423,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'Click', ''); @@ -431,7 +438,7 @@ var Escaped: array[0..4095] of Char; Txt: string; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; Txt := (Sender as TEdit).Text; @@ -449,7 +456,7 @@ var Escaped: array[0..4095] of Char; Txt: string; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; Txt := (Sender as TMemo).Text; @@ -467,7 +474,7 @@ var Escaped: array[0..4095] of Char; Txt: string; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; Txt := (Sender as TComboBox).Text; @@ -486,7 +493,7 @@ var Escaped: array[0..4095] of Char; Txt: string; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; Idx := (Sender as TListBox).ItemIndex; @@ -510,7 +517,7 @@ var Escaped: array[0..4095] of Char; Txt: string; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; Idx := (Sender as TComboBox).ItemIndex; @@ -531,7 +538,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'DblClick', ''); @@ -544,7 +551,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'Enter', ''); @@ -557,7 +564,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'Exit', ''); @@ -571,7 +578,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'KeyDown', IntToStr(Key)); @@ -585,7 +592,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'KeyUp', IntToStr(Key)); @@ -600,7 +607,7 @@ var CtrlId: Integer; Btn: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; Btn := Ord(Button); @@ -617,7 +624,7 @@ var CtrlId: Integer; Btn: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; Btn := Ord(Button); @@ -633,7 +640,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'MouseMove', @@ -647,7 +654,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'Click', ''); @@ -660,7 +667,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'Click', ''); @@ -673,7 +680,7 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'Change', IntToStr((Sender as TScrollBar).Position)); @@ -686,13 +693,39 @@ var FormId: Integer; CtrlId: Integer; begin - Tag := (Sender as TControl).Tag; + Tag := (Sender as TComponent).Tag; FormId := Tag shr 16; CtrlId := Tag and $FFFF; SendEvent(FormId, CtrlId, 'Notify', ''); end; +procedure TFormClient.HandleMenuItemClick(Sender: TObject); +var + Tag: Longint; + FormId: Integer; + CtrlId: Integer; +begin + Tag := (Sender as TComponent).Tag; + FormId := Tag shr 16; + CtrlId := Tag and $FFFF; + SendEvent(FormId, CtrlId, 'Click', ''); +end; + + +procedure TFormClient.HandleRadioGroupClick(Sender: TObject); +var + Tag: Longint; + FormId: Integer; + CtrlId: Integer; +begin + Tag := (Sender as TComponent).Tag; + FormId := Tag shr 16; + CtrlId := Tag and $FFFF; + SendEvent(FormId, CtrlId, 'Click', IntToStr((Sender as TRadioGroup).ItemIndex)); +end; + + procedure TFormClient.HandleFormClose(Sender: TObject; var Action: TCloseAction); var @@ -730,6 +763,10 @@ begin (CR^.Control as TRadioButton).OnClick := HandleRadioButtonClick; ctScrollBar: (CR^.Control as TScrollBar).OnChange := HandleScrollBarChange; + ctMenuItem: + (CR^.Control as TMenuItem).OnClick := HandleMenuItemClick; + ctRadioGroup: + (CR^.Control as TRadioGroup).OnClick := HandleRadioGroupClick; end; end; @@ -738,7 +775,8 @@ procedure TFormClient.WireOptEvent(CR: PFormCtrlRec; const EventName: string); begin if EventName = 'DblClick' then begin - (CR^.Control as TControl).OnDblClick := HandleDblClick; + if CR^.Control is TControl then + (CR^.Control as TControl).OnDblClick := HandleDblClick; CR^.Bound := CR^.Bound + [beDblClick]; end else if EventName = 'Enter' then @@ -767,17 +805,20 @@ begin end else if EventName = 'MouseDown' then begin - (CR^.Control as TControl).OnMouseDown := HandleMouseDown; + if CR^.Control is TControl then + (CR^.Control as TControl).OnMouseDown := HandleMouseDown; CR^.Bound := CR^.Bound + [beMouseDown]; end else if EventName = 'MouseUp' then begin - (CR^.Control as TControl).OnMouseUp := HandleMouseUp; + if CR^.Control is TControl then + (CR^.Control as TControl).OnMouseUp := HandleMouseUp; CR^.Bound := CR^.Bound + [beMouseUp]; end else if EventName = 'MouseMove' then begin - (CR^.Control as TControl).OnMouseMove := HandleMouseMove; + if CR^.Control is TControl then + (CR^.Control as TControl).OnMouseMove := HandleMouseMove; CR^.Bound := CR^.Bound + [beMouseMove]; end else if EventName = 'Click' then @@ -803,7 +844,8 @@ procedure TFormClient.UnwireOptEvent(CR: PFormCtrlRec; const EventName: string); begin if EventName = 'DblClick' then begin - (CR^.Control as TControl).OnDblClick := nil; + if CR^.Control is TControl then + (CR^.Control as TControl).OnDblClick := nil; CR^.Bound := CR^.Bound - [beDblClick]; end else if EventName = 'Enter' then @@ -832,17 +874,20 @@ begin end else if EventName = 'MouseDown' then begin - (CR^.Control as TControl).OnMouseDown := nil; + if CR^.Control is TControl then + (CR^.Control as TControl).OnMouseDown := nil; CR^.Bound := CR^.Bound - [beMouseDown]; end else if EventName = 'MouseUp' then begin - (CR^.Control as TControl).OnMouseUp := nil; + if CR^.Control is TControl then + (CR^.Control as TControl).OnMouseUp := nil; CR^.Bound := CR^.Bound - [beMouseUp]; end else if EventName = 'MouseMove' then begin - (CR^.Control as TControl).OnMouseMove := nil; + if CR^.Control is TControl then + (CR^.Control as TControl).OnMouseMove := nil; CR^.Bound := CR^.Bound - [beMouseMove]; end else if EventName = 'Click' then @@ -866,7 +911,7 @@ end; { ----- Property application ----------------------------------------------- } -procedure TFormClient.ApplyProp(CR: PFormCtrlRec; Key, Value: PChar); +procedure TFormClient.ApplyProp(FR: PFormRec; CR: PFormCtrlRec; Key, Value: PChar); var S: string; Unesc: array[0..4095] of Char; @@ -874,6 +919,7 @@ var Lines: TStringList; P: PChar; Start: PChar; + PCR: PFormCtrlRec; begin S := StrPas(Key); @@ -887,6 +933,8 @@ begin ctGroupBox: (CR^.Control as TGroupBox).Caption := StrPas(Unesc); ctRadioButton: (CR^.Control as TRadioButton).Caption := StrPas(Unesc); ctPanel: (CR^.Control as TPanel).Caption := StrPas(Unesc); + ctMenuItem: (CR^.Control as TMenuItem).Caption := StrPas(Unesc); + ctRadioGroup: (CR^.Control as TRadioGroup).Caption := StrPas(Unesc); end; end else if S = 'Text' then @@ -975,6 +1023,26 @@ begin if Start^ <> #0 then (CR^.Control as TComboBox).Items.Add(StrPas(Start)); end; + ctRadioGroup: + begin + (CR^.Control as TRadioGroup).Items.Clear; + P := Unesc; + Start := P; + while P^ <> #0 do + begin + if P^ = #10 then + begin + P^ := #0; + (CR^.Control as TRadioGroup).Items.Add(StrPas(Start)); + Inc(P); + Start := P; + end + else + Inc(P); + end; + if Start^ <> #0 then + (CR^.Control as TRadioGroup).Items.Add(StrPas(Start)); + end; end; end else if S = 'Checked' then @@ -983,17 +1051,25 @@ begin if CR^.CtrlType = ctCheckBox then (CR^.Control as TCheckBox).Checked := (N <> 0) else if CR^.CtrlType = ctRadioButton then - (CR^.Control as TRadioButton).Checked := (N <> 0); + (CR^.Control as TRadioButton).Checked := (N <> 0) + else if CR^.CtrlType = ctMenuItem then + (CR^.Control as TMenuItem).Checked := (N <> 0); end else if S = 'Enabled' then begin N := StrToIntDef(StrPas(Value), 1); - CR^.Control.Enabled := (N <> 0); + if CR^.Control is TControl then + (CR^.Control as TControl).Enabled := (N <> 0) + else if CR^.Control is TMenuItem then + (CR^.Control as TMenuItem).Enabled := (N <> 0); end else if S = 'Visible' then begin N := StrToIntDef(StrPas(Value), 1); - CR^.Control.Visible := (N <> 0); + if CR^.Control is TControl then + (CR^.Control as TControl).Visible := (N <> 0) + else if CR^.Control is TMenuItem then + (CR^.Control as TMenuItem).Visible := (N <> 0); end else if S = 'MaxLength' then begin @@ -1025,8 +1101,9 @@ begin begin N := StrToIntDef(StrPas(Value), -1); case CR^.CtrlType of - ctListBox: (CR^.Control as TListBox).ItemIndex := N; - ctComboBox: (CR^.Control as TComboBox).ItemIndex := N; + ctListBox: (CR^.Control as TListBox).ItemIndex := N; + ctComboBox: (CR^.Control as TComboBox).ItemIndex := N; + ctRadioGroup: (CR^.Control as TRadioGroup).ItemIndex := N; end; end else if S = 'Stretch' then @@ -1196,11 +1273,45 @@ begin except end; end; + end + else if S = 'Parent' then + begin + if CR^.CtrlType = ctMenuItem then + begin + N := StrToIntDef(StrPas(Value), 0); + PCR := FindCtrl(FR, N); + if PCR <> nil then + begin + if PCR^.Control is TMenu then + (PCR^.Control as TMenu).Items.Add(CR^.Control as TMenuItem) + else if PCR^.Control is TMenuItem then + (PCR^.Control as TMenuItem).Add(CR^.Control as TMenuItem); + end; + end; + end + else if S = 'Columns' then + begin + N := StrToIntDef(StrPas(Value), 1); + if CR^.CtrlType = ctRadioGroup then + (CR^.Control as TRadioGroup).Columns := N; + end + else if S = 'ShortCut' then + begin + N := StrToIntDef(StrPas(Value), 0); + if CR^.CtrlType = ctMenuItem then + (CR^.Control as TMenuItem).ShortCut := N; + end + else if S = 'PopupMenu' then + begin + N := StrToIntDef(StrPas(Value), 0); + PCR := FindCtrl(FR, N); + if (PCR <> nil) and (PCR^.Control is TPopupMenu) and (CR^.Control is TControl) then + (CR^.Control as TControl).PopupMenu := PCR^.Control as TPopupMenu; end; end; -procedure TFormClient.ApplyInlineProps(CR: PFormCtrlRec; P: PChar); +procedure TFormClient.ApplyInlineProps(FR: PFormRec; CR: PFormCtrlRec; P: PChar); var Token: array[0..4095] of Char; Key: array[0..63] of Char; @@ -1248,7 +1359,7 @@ begin StrCopy(Value, Eq); end; - ApplyProp(CR, Key, Value); + ApplyProp(FR, CR, Key, Value); end; end; @@ -1286,6 +1397,14 @@ begin Result := ctScrollBar else if S = 'MediaPlayer' then Result := ctMediaPlayer + else if S = 'MainMenu' then + Result := ctMainMenu + else if S = 'PopupMenu' then + Result := ctPopupMenu + else if S = 'MenuItem' then + Result := ctMenuItem + else if S = 'RadioGroup' then + Result := ctRadioGroup else Result := ctUnknown; end; @@ -1378,7 +1497,7 @@ var FR: PFormRec; CR: PFormCtrlRec; CType: TCtrlTypeE; - Ctrl: TControl; + Comp: TComponent; begin FormId := ParseInt(P); CtrlId := ParseInt(P); @@ -1398,62 +1517,82 @@ begin Exit; { Create the control } - Ctrl := nil; + Comp := nil; case CType of ctLabel: begin - Ctrl := TLabel.Create(FR^.Form); - (Ctrl as TLabel).AutoSize := False; + Comp := TLabel.Create(FR^.Form); + (Comp as TLabel).AutoSize := False; end; ctEdit: - Ctrl := TEdit.Create(FR^.Form); + Comp := TEdit.Create(FR^.Form); ctButton: - Ctrl := TButton.Create(FR^.Form); + Comp := TButton.Create(FR^.Form); ctCheckBox: - Ctrl := TCheckBox.Create(FR^.Form); + Comp := TCheckBox.Create(FR^.Form); ctListBox: - Ctrl := TListBox.Create(FR^.Form); + Comp := TListBox.Create(FR^.Form); ctComboBox: - Ctrl := TComboBox.Create(FR^.Form); + Comp := TComboBox.Create(FR^.Form); ctMemo: - Ctrl := TMemo.Create(FR^.Form); + Comp := TMemo.Create(FR^.Form); ctImage: begin - Ctrl := TImage.Create(FR^.Form); - (Ctrl as TImage).AutoSize := False; + Comp := TImage.Create(FR^.Form); + (Comp as TImage).AutoSize := False; end; ctGroupBox: - Ctrl := TGroupBox.Create(FR^.Form); + Comp := TGroupBox.Create(FR^.Form); ctRadioButton: - Ctrl := TRadioButton.Create(FR^.Form); + Comp := TRadioButton.Create(FR^.Form); ctPanel: - Ctrl := TPanel.Create(FR^.Form); + Comp := TPanel.Create(FR^.Form); ctScrollBar: - Ctrl := TScrollBar.Create(FR^.Form); + Comp := TScrollBar.Create(FR^.Form); ctMediaPlayer: - Ctrl := TMediaPlayer.Create(FR^.Form); + Comp := TMediaPlayer.Create(FR^.Form); + ctMainMenu: + begin + Comp := TMainMenu.Create(FR^.Form); + FR^.Form.Menu := Comp as TMainMenu; + end; + ctPopupMenu: + Comp := TPopupMenu.Create(FR^.Form); + ctMenuItem: + Comp := TMenuItem.Create(FR^.Form); + ctRadioGroup: + Comp := TRadioGroup.Create(FR^.Form); end; - if Ctrl = nil then + if Comp = nil then Exit; - { Set parent and geometry } - if Ctrl is TWinControl then - (Ctrl as TWinControl).Parent := FR^.Form - else - Ctrl.Parent := FR^.Form; + { Set parent and geometry for visual controls } + if Comp is TWinControl then + begin + (Comp as TWinControl).Parent := FR^.Form; + (Comp as TControl).Left := Left; + (Comp as TControl).Top := Top; + (Comp as TControl).Width := Width; + (Comp as TControl).Height := Height; + end + else if Comp is TControl then + begin + (Comp as TControl).Parent := FR^.Form; + (Comp as TControl).Left := Left; + (Comp as TControl).Top := Top; + (Comp as TControl).Width := Width; + (Comp as TControl).Height := Height; + end; + { else: non-visual (menus, menu items) — no parent or geometry } - Ctrl.Left := Left; - Ctrl.Top := Top; - Ctrl.Width := Width; - Ctrl.Height := Height; - Ctrl.Tag := (Longint(FormId) shl 16) or CtrlId; + Comp.Tag := (Longint(FormId) shl 16) or CtrlId; { Create control record } New(CR); CR^.CtrlId := CtrlId; CR^.CtrlType := CType; - CR^.Control := Ctrl; + CR^.Control := Comp; CR^.Bound := []; FR^.Ctrls.Add(CR); @@ -1462,7 +1601,7 @@ begin WireAutoEvents(CR); { Apply inline properties } - ApplyInlineProps(CR, P); + ApplyInlineProps(FR, CR, P); end; @@ -1484,7 +1623,7 @@ begin if CR = nil then Exit; - ApplyInlineProps(CR, P); + ApplyInlineProps(FR, CR, P); end; diff --git a/forms/protocol.md b/forms/protocol.md index e95c9b7..dcec27d 100644 --- a/forms/protocol.md +++ b/forms/protocol.md @@ -70,7 +70,7 @@ Event data varies by event type: | Event | Data | |-----------|-------------------------------| -| Click | (none) | +| Click | (none), or `` (RadioGroup) | | DblClick | (none) | | Change | `"new text"` or `` (ScrollBar) | | Select | ` "selected text"` | @@ -101,28 +101,38 @@ Event data varies by event type: | Panel | TPanel | (none) | | ScrollBar | TScrollBar | Change | | MediaPlayer | TMediaPlayer | (none) | +| MainMenu | TMainMenu | (none) | +| PopupMenu | TPopupMenu | (none) | +| MenuItem | TMenuItem | Click | +| RadioGroup | TRadioGroup | Click | + +MainMenu and PopupMenu use `0 0 0 0` geometry (non-visual). MenuItem +uses `0 0 0 0` geometry and requires a `Parent` property to specify its +parent menu or menu item. One MainMenu per form (auto-attached). +PopupMenu is associated with any control via the `PopupMenu` property. GroupBox and Panel are cosmetic containers (flat parent model — no child containment in the protocol). RadioButtons are all in one group per form. Opt-in events (require EVENT.BIND): Click (Image, GroupBox, Panel), Notify (MediaPlayer), DblClick, KeyDown, KeyUp, Enter, Exit, -MouseDown, MouseUp, MouseMove. +MouseDown, MouseUp, MouseMove. Menu and RadioGroup components do +not support opt-in events. ## Properties | Property | Applies To | Value Format | |-------------|---------------------------------------------|-------------------------------------------| -| Caption | Label, Button, CheckBox, GroupBox, RadioButton, Panel | Quoted string | +| Caption | Label, Button, CheckBox, GroupBox, RadioButton, Panel, MenuItem, RadioGroup | Quoted string | | Text | Edit, ComboBox, Memo | Quoted string (`\n` for line breaks) | -| Items | ListBox, ComboBox | Quoted string (`\n`-delimited) | -| Checked | CheckBox, RadioButton | 0 or 1 | +| Items | ListBox, ComboBox, RadioGroup | Quoted string (`\n`-delimited) | +| Checked | CheckBox, RadioButton, MenuItem | 0 or 1 | | Enabled | All | 0 or 1 | | Visible | All | 0 or 1 | | MaxLength | Edit | Integer | | ReadOnly | Edit, Memo | 0 or 1 | | ScrollBars | Memo | 0-3 (ssNone..ssBoth) | -| ItemIndex | ListBox, ComboBox | Integer (-1 = none) | +| ItemIndex | ListBox, ComboBox, RadioGroup | Integer (-1 = none) | | TabOrder | All windowed controls | Integer | | Stretch | Image | 0 or 1 | | Center | Image | 0 or 1 | @@ -141,6 +151,10 @@ MouseDown, MouseUp, MouseMove. | DeviceType | MediaPlayer | Quoted string (e.g., `dtWaveAudio`) | | AutoOpen | MediaPlayer | 0 or 1 | | Command | MediaPlayer | Quoted string (pseudo-property, see below)| +| Parent | MenuItem | Integer (ctrlId of parent menu/item) | +| Columns | RadioGroup | Integer (number of columns) | +| ShortCut | MenuItem | Integer (Delphi ShortCut value) | +| PopupMenu | Any TControl | Integer (ctrlId of PopupMenu) | File path properties (Picture, FileName) are resolved relative to the client's `BasePath` setting. Subdirectories are allowed (e.g.,