diff --git a/BUGS-TO-REPORT.md b/BUGS-TO-REPORT.md index 584230c04..e1d20e41d 100644 --- a/BUGS-TO-REPORT.md +++ b/BUGS-TO-REPORT.md @@ -3926,3 +3926,492 @@ Severity: low. The compiler accepts code that the class's author meant to forbid + +--- + +## Horizontal and Vertical Spacing move the controls with no undo step and without marking the form changed + +**Describe the bug** +In the form designer, the commands of Format > Horizontal Spacing and Format > Vertical Spacing (Make Equal, Increase, Decrease, Remove) move the selected controls on the surface, but the move is not recorded: the tab is not marked changed, Ctrl+Z does nothing, and the designer's own copy of the form keeps the old positions. Closing the designer asks nothing, and the form opens again with the controls where they were. If another change follows before the form is saved, the moved positions are saved with it. It was observed in an IDE started with an `APPDATA` of its own, reading the designer's controls and its form state, and the messages it sends to the IDE. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form with three controls side by side: Command1 at Left 10, Check1 at Left 140 and Command2 at Left 250, each 100 wide. +2. Select the three and choose Format > Horizontal Spacing > Remove. Check1 moves to Left 110 and Command2 to Left 210. +3. The tab shows no change mark, and Ctrl+Z leaves the controls where they are. +4. Close the designer: nothing is asked. Open the form again: Check1 is at 140 and Command2 at 250. +5. The same with Format > Vertical Spacing > Remove (Check1 to Top 80, Command2 to Top 104), Make Equal and Increase. + +**Expected behavior** +Each spacing command an undo step that marks the form changed, as every other Format command is: Align > Left marks the tab, Ctrl+Z undoes it, and it is saved. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +The designer sends no `onChanged` or `onSignificantChange` for a spacing command. `formDesginer_RemoveSpacingHorizontal`, `formDesginer_RemoveSpacingVertical` and `formDesigner_AdjustSpacingOfSelectedControls_Horizontal` / `_Vertical` (`ide/main.js`) never call `undoPoint`, which `alignControlsLeft` calls at its end. A later change, such as an arrow key on another control, writes the whole surface into the form, the spaced positions included. + +Severity: medium. The arrangement the reader sees is lost on close with nothing to warn of it, or is saved later as part of an unrelated change that cannot be undone separately. + + + +--- + +## Make Same Size marks the form changed, but Save keeps the old sizes unless another change follows + +**Describe the bug** +Format > Make Same Size resizes the selected controls on the surface and marks the tab changed, and Ctrl+Z undoes it, but the designer's own copy of the form keeps the old sizes. Ctrl+S then saves the old sizes: closing and opening the form again shows them. If another change follows before the save --- an arrow key on another control, or a click on the form's background --- the new sizes are saved with it. It was observed in an IDE started with an `APPDATA` of its own, reading the designer's controls and its form state. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form with Command1 and Command2 30 high and Check1 24 high. +2. Select the three and choose Format > Make Same Size > Height (Shortest). All three are 24 high, and the tab is marked changed. +3. Press Ctrl+S at once. The tab's change mark goes. +4. Close the designer and open the form again. Command1 and Command2 are 30 high. + +**Expected behavior** +The new sizes saved, as an Align command's new positions are. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +After step 2 the designer's form state (`state.item.form`, what Save writes) still holds `Height: 30`. With an arrow key on another control between steps 2 and 3, the form opens again with the 24s. A click on the form's background between them saves them too. + +Severity: medium. The reader sees the sizes changed and saved, and the form is not. + + + +--- + +## Arrow keys in the designer's zoom box also move the selected controls + +**Describe the bug** +The form designer's toolbar has a zoom box, a drop-down of 25 % to 400 %. With the focus in it, Up Arrow and Down Arrow change the zoom, as a drop-down does, and also move the controls selected in the designer by one device pixel, as the same key in the designer does. Each press is a change to the form: the tab is marked changed and an undo step is made. Nothing on screen points to the move, since the zoom changes at the same time. It was observed in an IDE started with an `APPDATA` of its own, reading the control's position and the messages the designer sends to the IDE. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form and click Command1, at Top 50. +2. Put the focus in the zoom box, which reads 100 % (a person has it there after picking a value from it). +3. Press Up Arrow twice. The zoom goes to 75 % and then 50 %, and Command1 moves up a device pixel at each press: Top 48.67 at 150 % display scaling. The tab is marked changed. + +**Expected behavior** +Keys in the zoom box changing only the zoom. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +Each press sends `onChanged` and `onSignificantChange`, as an arrow key in the designer does. At 100 % display scaling a press moves a control 1 pixel. With two controls selected, Up twice and Down twice make four undo steps. + +Severity: medium. A control's position changes with nothing to show it, and the change is saved with the form. + + + +--- + +## Lock Controls does not mark the form changed, so a lock made on its own is lost when the designer is closed + +**Describe the bug** +Format > Lock Controls locks the selected controls in the designer, which shows a lock icon on them and refuses to move them, but does not mark the form changed. The lock list in the form's `__IDEOptions.lockedControls` is not updated either. Closing the designer asks nothing, and the form opens again with nothing locked. A lock is saved only when another change to the form follows it. It was observed in an IDE started with an `APPDATA` of its own, reading the designer's lock list and its form state. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form and click Command1. +2. Choose Format > Lock Controls. Command1 shows the lock icon; the tab shows no change mark. +3. Close the designer: nothing is asked. +4. Open the form again: Command1 is not locked. + +**Expected behavior** +The lock marked as a change to the form and saved with it, as the grid options, which are stored in the same `__IDEOptions`, are. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +`lockTheControl` (`ide/main.js`) is called by the lock commands with an argument that skips `IDEOptionsChanged`, and they make no undo point; by the code, the form's context menu's Lock all controls and Unlock all controls are the same, which was not tested. Lock Controls, an arrow key on another control and Ctrl+S keep the lock. + +Severity: medium. The reader locks controls to protect a layout and the lock is silently lost. + + + +--- + +## Align > Center (Horizontal) and Center (Vertical) centre the controls on the selection's left or top edge, not on its centre + +**Describe the bug** +Format > Align > Center (Horizontal) puts the centre of every selected control on the leftmost left edge of the selection, and Center (Vertical) puts each centre on the topmost top edge, so the controls move half their width left of the selection, or half their height above it. VB6's Format > Align > Centers centres the controls on the selection's centre line. It was observed in an IDE started with an `APPDATA` of its own, reading the controls' positions. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form with Command1 at 10,50, Check1 at 140,55 and Command2 at 250,110, each 100 wide; Command1 and Command2 30 high, Check1 24. +2. Select the three and choose Format > Align > Center (Horizontal). All three are at Left -40, centred on 10, partly off the form. +3. From the same start, choose Center (Vertical). Command1 and Command2 are at Top 35 and Check1 at 38: centred on 50. + +**Expected behavior** +The controls centred on the selection's centre: Left 130 (the selection spans 10 to 350), or their centres on 95 (it spans 50 to 140). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +`alignControlsCenterHorizontal` (`ide/main.js`) takes its line from `getTopLeftOfAllControls`, which returns the smallest left and the smallest top, and `alignControlsCenterVertical` from `getTopRightOfAllControls().y`. Align > Left, Right, Top and Bottom are right. The command is an undo step, so Ctrl+Z puts the controls back. + +Severity: low. The result is on screen and can be undone. + + + +--- + +## Center In Container puts a control off centre when the designer is zoomed + +**Describe the bug** +Format > Center In Container (Horizontally) and (Vertically) centre the selected controls in their container at 100 %. At any other zoom they move the controls by the wrong amount: at 50 % a TextBox in a Frame goes partly out of the Frame, and three controls on the form move left instead of right. It was observed in an IDE started with an `APPDATA` of its own, reading the controls' positions. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form with a Frame 200 x 130 holding a TextBox 120 x 24 at Left 20, Top 30. +2. Choose View > Zoom Out twice: the zoom box reads 50 %. +3. Select the TextBox and choose Format > Center In Container (Horizontally). It goes to Left -9.98. +4. Choose Center In Container (Vertically). It goes to Top 20.52. + +**Expected behavior** +The TextBox at Left 40 and Top 53, centred in the Frame, as the same commands give at 100 %. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +`tbFormDesigner_CenterSelectedGroupInContainerHorizontal` (`ide/main.js`) reads the container's size with `getBoundingClientRect()`, which is the zoomed size, and compares it with the controls' unzoomed positions. Three controls on the form spanning 10 to 350, centred horizontally at 50 %, moved by -80 instead of +20. + +Severity: low. The result is on screen and can be undone. + + + +--- + +## On a CustomControls form, PROPERTIES shows a control's position in twips but takes a typed value in pixels + +**Describe the bug** +On a form of the CustomControls package the controls' `Left`, `Top`, `Width` and `Height` are pixels, in the `.tbform` file and on the surface. PROPERTIES shows them multiplied by 15, as twips, but takes a value typed into the same box as pixels. So retyping the value shown moves the control 15 times as far. On a form of the VB package, PROPERTIES shows and takes twips alike. It was observed in an IDE started with an `APPDATA` of its own, reading the PROPERTIES box and the control's position. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a CustomControls form with a control at Left 20, Top 200, 120 x 32 (pixels, in the `.tbform` file). +2. Select the control. PROPERTIES shows Left 300, Top 3000, Width 1800, Height 480. +3. Type 40 in Left and press Enter. The control moves to Left 40 pixels, and the box then shows 600. + +**Expected behavior** +PROPERTIES showing and taking the same unit: pixels on a CustomControls form, whose controls are laid out in pixels. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +`cacheParentScaleModeInfo` (`ide/main.js`) takes a form with no `ScaleMode`, as a CustomControls form is, to be in `vbTwips` and multiplies by 15 for the display. On a VB form in twips, Command1 at 10 pixels shows Left 150, and typing 450 moves it to 30 pixels. + +Severity: low. The control moves on the surface at once, so the mistake is visible. + + + +--- + +## Escape and then a pointer move over the form mark the form changed, with an undo step that changes nothing + +**Describe the bug** +In a form designer with a control selected, Escape selects the form. The next move of the pointer over the form, with no button pressed, or a click on a control, marks the form changed and makes an undo step, though nothing has moved. Closing then asks to save, and Ctrl+Z first steps through the empty undo step before it reaches the last real change. It was observed in an IDE started with an `APPDATA` of its own, reading the tab, the messages the designer sends to the IDE, and the controls' positions. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form that has not been changed. Click a control, Command1. +2. Press Escape. The form is selected. +3. Move the pointer over the form. The tab is marked changed; no control has moved. +4. For the undo step: open the form again, click Command1, press Right Arrow, then do steps 2 and 3. Ctrl+Z leaves Command1 where the arrow put it; a second Ctrl+Z moves it back. + +**Expected behavior** +No change and no undo step until something on the form changes, as a click on one control and then another gives. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +After step 3 the designer sends `onChanged` and `onSignificantChange`. It writes the surface into the form state, so at a display scaling other than 100 % a position the surface rounds to a device pixel changes in what Save writes: at 150 %, Check1's `Top` of 55 became 54.6667. It does not happen every time: in the trials one try in four did not do it, when Escape had first closed an open File menu, and in the reproducer's first four runs two plain tries in seven did not. A click on a control, a Format menu opened and closed, and the grid panel shown and left do not do it. + +Severity: low. Nothing visible moves; the reader is asked to save a form they did not change. + + + +--- + +## Ctrl+A does nothing in a PROPERTIES box + +**Describe the bug** +In the PROPERTIES pane, Ctrl+A in a property's box neither selects the box's text nor does anything else: the caret stays where it was, and the designer's selection is unchanged. Typing then adds to the value. It was observed in an IDE started with an `APPDATA` of its own, reading the box's selection. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form and click Command1. +2. Click in the Caption box of PROPERTIES and press End. The caret is at 8, after `Command1`. +3. Press Ctrl+A. The selection is still 8 to 8, and only Command1 is selected in the designer. +4. Type `Go`: the box holds `Command1Go`. + +**Expected behavior** +Ctrl+A selecting the box's text, as it does in every other text box. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +With the designer focused, Ctrl+A selects every control, as it should. In a PROPERTIES box the key is consumed and does neither. + +Severity: low. Home, Shift+End or the mouse select the text. + + + +--- + +## Alt+arrow in the form designer also opens the IDE's menu bar + +**Describe the bug** +In the form designer, Alt+Left, Alt+Right, Alt+Up and Alt+Down align the selected controls (lefts, rights, tops, bottoms). Each also opens the IDE's menu bar, which then has to be closed with Escape. The IDE opens its menu bar as soon as Alt goes down, so every Alt shortcut opens it. It was observed in an IDE started with an `APPDATA` of its own, sending the keys as a keyboard does. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form and select Command1 and Check1. +2. Press Alt+Left. Check1's left edge moves to Command1's, and the IDE's menu bar opens. + +**Expected behavior** +The alignment only. A menu bar opened by Alt alone, when it is released with no other key pressed, as Windows does. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +Alt pressed and held alone opens the menu bar before it is released. With the arrow's key events carrying the Alt flag but no key event for Alt itself, the menu bar opens on the arrow's key-up. + +Severity: low. The alignment is done; the menu is in the way. + + + +--- + +## Renaming a control to another control's name asks the control-array question twice + +**Describe the bug** +Typing the name of another control into a control's Name box in PROPERTIES asks whether to create a control array. Answered YES, the other control becomes element 0 of an array, but the renamed control keeps its name and the same question is asked again; a second YES renames it as element 1. It was observed in an IDE started with an `APPDATA` of its own, reading the boxes shown and the controls' names. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form with Label1 and Check1, neither in an array. +2. Select Label1, type `Check1` in its Name box and press Enter. The box asks: *A control already exists with the name 'Check1'. Do you want to create a control array?* +3. Click YES. Check1 becomes Check1(0), Label1 is still Label1, and the same box is shown again. +4. Click YES. Label1 becomes Check1(1). + +**Expected behavior** +One question; YES makes both controls the array, Check1(0) and Check1(1). + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +Answering NO to the first question asks nothing more and keeps the old name. Renaming to the name of an existing array (Command1 to Command2, with Command2(0) and Command2(1) on the form) asks nothing and makes Command2(2). + +Severity: low. The second question can be answered, and the result is then right. + + + +--- + +## Add Windows Form accepts a name that is not an identifier + +**Describe the bug** +Add Windows Form (and the other Add commands for a designer) asks for the file's name and makes the class's name from it. A name with a space is turned into a bracketed identifier, `[My Form]`, but a name that starts with a digit is taken as it is: `1bad` gives `Class 1bad`, with no message, and the project then does not compile. It was observed in an IDE started with an `APPDATA` of its own, reading the new `.twin` file and the diagnostics. + +**To Reproduce** +Steps to reproduce the behavior: +1. In Project Explorer, choose Add > Add Windows Form. The name box offers `MyForm.tbform` with `MyForm` selected. +2. Type `1bad` and press Enter. `1bad.tbform` and `1bad.twin` are created and the designer opens; nothing is said. +3. `1bad.twin` holds `Class 1bad`, and DIAGNOSTICS lists `TB5174 Invalid number format` at line 4. + +**Expected behavior** +The name refused with a message, or made into an identifier as `My Form` is. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +`a.b` gives `Class [a.b]`, and `My Form` gives `Class [My Form]` and a form whose `Name` is `[My Form]`. + +Severity: low. The compile error is listed at once. + + + +--- + +## View > Zoom Out stops at 50 %, though the zoom box offers 25 % + +**Describe the bug** +With a form designer focused, View > Zoom Out lowers the zoom one step at a time, 75 %, then 50 %, and then does nothing more, though the toolbar's zoom box offers 25 % and can choose it. View > Zoom In from 50 % steps up to 400 %. It was observed in an IDE started with an `APPDATA` of its own, reading the zoom box after each command. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form and click it, so that the designer has the focus. +2. Choose View > Zoom Out four times. The zoom box reads 75 %, 50 %, 50 %, 50 %. + +**Expected behavior** +A third Zoom Out going to 25 %, the box's smallest value. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +`tbFormDesigner_ZoomDecrease` (`ide/main.js`) steps only `if (zoomSelect.selectedIndex > 1)`, and 25 % is index 0. The same commands scale the zoom by `g_textScaleFactor` the opposite way from the zoom box (`value * (textScale/100)` against `value / (textScale/100)`), which matters only when the Windows text size is above 100 %; that is not reported here, since it could not be tested. + +Severity: low. The box reaches 25 %. + + + +--- + +## The History entry for a change in a designer reads [line: undefined] + +**Describe the bug** +A change in a form designer adds an entry to the HISTORY panel, named after the form. Its tooltip reads the file's path followed by `[line: undefined]`, where an entry for an edit in a code file gives the line. It was observed in an IDE started with an `APPDATA` of its own, holding the pointer on the entry. + +**To Reproduce** +Steps to reproduce the behavior: +1. Open a form, click a control and press Right Arrow. +2. Show the HISTORY panel and hold the pointer on the entry for the form. The tooltip reads `//Sources/frmMain.tbform [line: undefined]` and the date. + +**Expected behavior** +No line in the tooltip of a designer's entry, since a designer has none. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +The designer's `onSignificantChange` handler calls `updateHistory(uri,
)` with no line, and the tooltip is built as ` [line: ]`. An edit inside a procedure of a code file gives an entry whose tooltip reads `[line: 5]`. A double-click on the form's entry opens the designer, as it should. + +Severity: low. Cosmetic. + + + +--- + +## Shift+F7 does not switch from a form's code to its designer when a folder in its path has .twin in its name + +**Describe the bug** +Shift+F7, and the toolbar's Switch Between Form And Code, open a form's designer from its code file. When a folder in the file's path has `.twin` in its name, such as `Sources/v1.twin.forms/frmDeep.twin`, they show the notification *Unable to switch form design mode: this document is not part of a form* and open nothing. From the designer to the code the switch works. It was observed in an IDE started with an `APPDATA` of its own. + +**To Reproduce** +Steps to reproduce the behavior: +1. A project with a form frmDeep in the folder `Sources/v1.twin.forms` (`frmDeep.tbform` and `frmDeep.twin`). +2. Open `frmDeep.twin` and press Shift+F7. The notification above is shown, and the designer does not open. + +**Expected behavior** +The designer of frmDeep opened, as Shift+F7 does for a form in `Sources`. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +`switchToFormPart` (`ide/main.js`) replaces the first `.twin` in the whole path with `.tbform`, giving `Sources/v1.tbform.forms/frmDeep.twin`, which does not exist. From the designer the first `.tbform` in the path is the file's own, so that direction works. + +Severity: low. The designer opens from Project Explorer. + + + +--- + +## With a report open, a floating Toolbox lists every form control + +**Describe the bug** +With a report designer open, the docked Toolbox lists only the tools a report can hold: Pointer, Label, Shape, Line, Image, CheckMark and QRCode. When the Toolbox floats, it lists all 24 tools of a form and More components. A tool a report cannot hold is refused when used, with the notification *Can't add CommandButton to a report. Only windowless controls allowed.* It was observed in an IDE started with an `APPDATA` of its own, reading the tools shown. + +**To Reproduce** +Steps to reproduce the behavior: +1. Add a report with Add Windows Report and Enter. The docked Toolbox lists the seven tools above. +2. Hide the Toolbox and show it again (View > TOOLBOX twice), so that it floats. It lists Pointer, PictureBox, Label, TextBox, Frame, CommandButton and the rest, 24 tools, and More components. + +**Expected behavior** +The same seven tools as the docked Toolbox. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +The rule that hides the other tools for a report (`.reportDesignerMode ...`) applies only inside `#bodyInner`, where the docked Toolbox is; a floating panel is outside it. + +Severity: low. Nothing wrong can be added. + + + +--- + +## A package's read-only designer file opens an empty tab besides the notice that it cannot be opened + +**Describe the bug** +A designer file of a referenced package, such as the VB package's `tbObjectDesigner_PROPERTYPAGE.tbppage`, is read-only. Opening it from Project Explorer shows the notification *Can't open designer for a read-only document.*, and also adds a tab of that name and selects it: an empty tab with no close icon. A middle click closes it. It was observed in an IDE started with an `APPDATA` of its own. + +**To Reproduce** +Steps to reproduce the behavior: +1. In a project that references the VB package, expand Packages > VB > Sources > DESIGNERS in Project Explorer. +2. Double-click `tbObjectDesigner_PROPERTYPAGE.tbppage`. The notification above is shown, and an empty tab `tbObjectDesigner_PROPERTYPAGE.tbppage` is added and selected. + +**Expected behavior** +The notification only, with no tab; or the file shown read-only, as View As JSON can. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +The IDE never reports the file as opened (the open's completion callback is not called), and the page throws a `TypeError` in a websocket request handler. + +Severity: low. Cosmetic; the tab closes with a middle click. + + + +--- + +## A form file with bad JSON opens an empty designer that takes no edit + +**Describe the bug** +A `.tbform` file whose JSON has a syntax error (the project lists `TB5246 bad JSON syntax` for it) opens in a designer tab that shows no control and an empty PROPERTIES pane, with the notification *ERROR: bad JSON encountered when parsing tbform*. The designer looks usable but takes no edit: a control drawn or double-clicked from the Toolbox is not added, the tab is never marked changed, and Ctrl+S leaves the file as it was. Nothing is lost. It was observed in an IDE started with an `APPDATA` of its own, reading the designer and the saved project file. + +**To Reproduce** +Steps to reproduce the behavior: +1. A project with a form whose `.tbform` file has a doubled comma, `"Height": 300,,`. +2. Open the `.tbform`. The notification above is shown, and the designer tab is empty. +3. Double-click the CommandButton tool. Nothing is added, and the tab shows no change mark. +4. Press Ctrl+S. The file is as it was. + +**Expected behavior** +The designer not opened, with the notification, or a message in the designer that the file cannot be read, so that the reader opens it as text to repair it. + +**Desktop:** + - OS: Windows 10 Pro 22H2 (build 19045) + - twinBASIC compiler version: BETA 1005 + +**Additional context** +The page throws `TypeError: Cannot read properties of undefined (reading '0')` in `formDesigner_syncToState` when the designer opens, and `addControl` throws at the double-click. A `.tbform` with no `FormDesignerId` opens normally. + +Severity: low. Nothing is lost; the empty designer is misleading. + + diff --git a/WIP.Harness.md b/WIP.Harness.md index c8f0c8ba6..973c148a5 100644 --- a/WIP.Harness.md +++ b/WIP.Harness.md @@ -1189,6 +1189,14 @@ Eight rules: anything else. When the IDE is still revealing lines 10 s later, `openFile`, `setCursor` and `select` throw, naming the file and the place, rather than go on while the cursor can still move; `afterReveal` itself returns `false`. The IDE's side of it is in BUGS-TO-REPORT.md. +- **A breakpoint set straight after an edit is lost.** The compiler's answer to an edit goes to + `parseDocumentDecorations` too, which redraws every breakpoint glyph from the answer's own + list. An F9 pressed in the 20--60 ms before the answer arrives loses its glyph, though the + compiler keeps the breakpoint. The next F9 in that file sends a list without it, and it is + gone. So a script that edits and then sets a breakpoint calls `waitForEditAnswer` first, as + `shoot_docs.mjs`'s `breakpointOn` and the `debugging` lane do. It waits until the file is no + longer `pendingChangeConfirmation`. A person rarely presses F9 that fast, and the defect is + not queued (owner, 2026-10-12). - **A click checks where its press lands** (the `assert` lane of `ide-test.bat` shows it). The page can change between the call that aims and the press, a few milliseconds to a hundred later on a busy page. In an editor the debugger has just diff --git a/WIP.IDE.md b/WIP.IDE.md index 1d46753e5..b9f12db9d 100644 --- a/WIP.IDE.md +++ b/WIP.IDE.md @@ -204,7 +204,8 @@ From the survey of BETA 1005. Each chapter's fact sheet confirms the items in it Export…), the object view, search, and the empty headings for each Add item. - **Packages:** the Available Packages tab, Import from file, the link and overwrite prompts, and the "package needed" prompts for WinNativeForms and CustomControls. -- **Status bar:** the error, warning, hint and info counters, and the memory figure. +- **Status bar:** the memory figure, Ln/Col, LLVM progress and the Personal edition badge. The + error, warning, hint and info counters are in the DIAGNOSTICS panel's header, not here. - **Themes:** custom `.theme` files in a `themes` folder. - **Webpage:** mobile mode, and back, forward and reload. - **Troubleshooting:** the DevTools command (`tbIdeDevTools_Show`, which has no menu item). diff --git a/WIP.Screenshots.md b/WIP.Screenshots.md index bfe74897a..920c05857 100644 --- a/WIP.Screenshots.md +++ b/WIP.Screenshots.md @@ -183,7 +183,7 @@ Exit codes: 0 every picture written or unchanged, 1 a picture failed, 2 the tool restarted on an empty licence key in the page) and `splash` (the demo, for the splash alone), `programs`, `codelens`, `memory`, `package` and `fusion` (pictures of a running program), `package-server`, `settings-linked` and `package-check`, `import`, `webpage` and `web` (a - Chrome, not an IDE), `featuremap`, `code-chapter`, `debugging`, `customcontrols`, `code` and `sample9` (all described + Chrome, not an IDE), `featuremap`, `code-chapter`, `debugging`, `forms-chapter`, `customcontrols`, `code` and `sample9` (all described below). A setup is `{name, start, prepare}`. Every add-in a setup loads has its `SaveSetting` key snapshotted, emptied and restored (`tbDocsHelp`, `GlobalSearchAddIn`). @@ -294,6 +294,61 @@ plus `Menu_Debug` (paused, the code focused) and `Menu_Run` (WaitAWhile running) - A line for the console is put in its input row whole and entered (`consoleRun`): a space typed while the row's completion list is open accepts the list's first entry. +## The "Designing forms and reports" chapter + +Setup `forms-chapter`: the fixture `test/shots/forms/`, FormsDemo, which refers to VB, the App +global object and WinNativeCommonCtls (for the Toolbox's COMMON CONTROLS group). `frmMain` (380 by +270, multi-coloured grabbers on) has a File menu (Open, a separator, Exit), Text1, Label1, +Command1, Check1, Frame1 holding Text2, and the control array Command2(0) and (1); `MyReport` +has a Label in Report Header and a Line and a Label in Report Detail. Its pictures are under +`docs/IDE/Forms/Images/` and `docs/IDE/Reports/Images/`, plus `Menu_Format_HorizontalSpacing`, +now taken with Command1, Check1 and Command2(0) selected (the page's own example of **Left** 10, +140 and 250), so that Make Equal is live. `Designer_PackageNeeded` is taken in setup `settings`, +whose project does not refer to the VB package. + +- **Each picture starts from nothing** (`formsReset`: no menu or dialog, every tab closed with its + changes discarded, the default layout), then opens its designer (`formsDesigner`) and selects + with real clicks (`selectNamed`: a click, then Shift+clicks; `Command2(1)` names an array + member). +- **The striped ground round a designer is not steady**: it differs by a few dozen pixels from + one capture to the next, so a picture with much of it never gets two equal captures. + `groundUnsteady(c, clip, over)` gives `capture` the editor area under the tab row as + `unsteady`, less the form's boxes, the tabs and `over` (a menu, a list, the panel and, in an + annotated picture, each label and arrow of the overlay from `layerBoxes`; the overlay's own + box is too big for it). The form, its menu bar and its readouts are compared only 2 pixels in + from their edges, a readout only between its rounded ends: the ground shows through their + edges and shadows, and a 2-pixel band there failed Designer_Locked now and then. A cut-out + (`keep`) avoids the ground altogether. +- **The container's red outline is an animation** (`borderBlink`, the outline red half the + time), which the quiet style sheet stops where the outline is transparent. Controls_Container + holds it at its middle with a style of its own (`tbShotBlink`, `animation ... -0.25s paused`), + removed afterwards. +- **The TabIndex circles show while Ctrl is held**: a `rawKeyDown` of Control, settled by the + designer's class `designerShowTabIndex`, and the key released after the capture. The pointer + stays on the form where it clicked, and the shot waits for its readout: moved away, the + readout shows or not by the moment the pointer left, and the picture changed between runs. A circle is + drawn only on a control whose **TabStop** is True (the designer sets `data-tabindex` only for + those), so Label1 and Frame1 have none. +- **The designer reverses a menu's children from the file**: the fixture lists Exit, `-`, Open + so that the drop-down shows Open, the separator, Exit. +- **The zoom box is set by a `change` event on `#zoomSelect`**, never by the arrow keys, which + also move the selected controls. Designer_Zoom shows the box and its list only, four rows of + it: the Light theme's list has a smaller font, and the whole list makes the light picture 100 + pixels shorter than the dark one. The form at 50 % is not in it: its scaled dots differ in + every capture, even with the ground left out. +- **The Toolbox is scrolled with the mouse wheel**: a capture never holds more than the real + window's height, so a taller page does not show more of it. Show Control Names is set from the + panel's `...` menu and put back afterwards (saved nowhere). The search of Toolbox_Search is + ended (the box focused, Escape) after the capture, or every later picture's Toolbox lacks + tools. +- **A report's PAGE WIDTH, LEFT MARGIN and RIGHT MARGIN bars show only under the pointer** + (opacity 0, 1 on hover): Report_Controls rests the pointer on PAGE WIDTH. +- **Preview_Window** is a running program's window, taken once with `captureWindow` from the + IDE's desktop. The File menu is not open in it: a native menu is a window + of its own (`#32768`), which the tool cannot yet capture and lay over the form's window. +- PROPERTIES shows no description line: in BETA 1005 its text is set in a footer of the + designer's own properties box, which is 0 pixels wide, so nothing is drawn. + ## Special IDE states - **`Services_Unavailable`**: the badge in the `no-project` setup. With no project the badge's diff --git a/bugs/add-form-bad-name/repro.json b/bugs/add-form-bad-name/repro.json new file mode 100644 index 000000000..1002f48af --- /dev/null +++ b/bugs/add-form-bad-name/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Add Windows Form accepts the name 1bad, which is not an identifier, and writes Class 1bad, which does not compile" + ], + "steps": "1. In Project Explorer, choose Add > Add Windows Form.\n2. Type 1bad over the name offered and press Enter.\n3. 1bad.tbform and 1bad.twin are created with no message; 1bad.twin holds Class 1bad, and DIAGNOSTICS lists TB5174 Invalid number format at it.\nControl: the name My Form gives Class [My Form]." +} diff --git a/bugs/customcontrols-properties-twips/repro.json b/bugs/customcontrols-properties-twips/repro.json new file mode 100644 index 000000000..09dbbde1e --- /dev/null +++ b/bugs/customcontrols-properties-twips/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "on a CustomControls form, PROPERTIES shows a control's Left in twips but takes a typed value in pixels" + ], + "steps": "1. Open a CustomControls form whose control MyButton1 is at Left 20 (pixels, in the .tbform file).\n2. Select MyButton1. PROPERTIES shows Left 300.\n3. Type 40 in Left and press Enter. The control moves to Left 40 pixels, and the box then shows 600.\nControl: on a form of the VB package, Command1 at 10 pixels shows Left 150, and typing 450 moves it to 30 pixels." +} diff --git a/bugs/form-align-center-edge/repro.json b/bugs/form-align-center-edge/repro.json new file mode 100644 index 000000000..a275f2a12 --- /dev/null +++ b/bugs/form-align-center-edge/repro.json @@ -0,0 +1,9 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Align > Center (Horizontal) centres every selected control on the leftmost left edge", + "Align > Center (Vertical) centres every selected control on the topmost top edge" + ], + "steps": "1. Open a form with Command1 at 10,50, Check1 at 140,55 and Command2 at 250,110, each 100 wide.\n2. Select the three and choose Format > Align > Center (Horizontal). Each is at Left -40: centred on 10, the leftmost left edge, not on 180, the selection's centre.\n3. From the same start, Center (Vertical) centres each on Top 50, the topmost top edge, not on 95.\nControl: Align > Left puts all three at Left 10." +} diff --git a/bugs/form-alt-arrow-menu/repro.json b/bugs/form-alt-arrow-menu/repro.json new file mode 100644 index 000000000..ba47a74c4 --- /dev/null +++ b/bugs/form-alt-arrow-menu/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Alt+Left in the designer also opens the IDE's menu bar" + ], + "steps": "1. Open a form and select two controls.\n2. Press Alt+Left. The controls' left edges are aligned, and the IDE's menu bar opens its first menu.\nControl: the alignment itself." +} diff --git a/bugs/form-array-question-twice/repro.json b/bugs/form-array-question-twice/repro.json new file mode 100644 index 000000000..72d58a1f4 --- /dev/null +++ b/bugs/form-array-question-twice/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "renaming Label1 to Check1 asks the control-array question twice: the first YES makes Check1 an array, the second renames Label1" + ], + "steps": "1. Open a form with Label1 and Check1, neither in an array.\n2. Select Label1, type Check1 in its Name box in PROPERTIES and press Enter. The box asks: A control already exists with the name 'Check1'. Do you want to create a control array?\n3. Click YES. Check1 becomes Check1(0), Label1 keeps its name, and the same question comes again.\n4. Click YES. Label1 becomes Check1(1).\nControl: answering NO to the question asks nothing more and keeps the old name." +} diff --git a/bugs/form-bad-json-empty-designer/repro.json b/bugs/form-bad-json-empty-designer/repro.json new file mode 100644 index 000000000..f4ba2b38f --- /dev/null +++ b/bugs/form-bad-json-empty-designer/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "a form file with bad JSON opens an empty designer that adds no control, and Save leaves the file as it was" + ], + "steps": "1. A project with a form whose .tbform file has a JSON syntax error, such as \"Height\": 300,, (DIAGNOSTICS lists TB5246 bad JSON syntax).\n2. Open the .tbform. The notification reads ERROR: bad JSON encountered when parsing tbform, and the designer tab opens with no control and an empty PROPERTIES.\n3. Double-click the CommandButton tool: nothing is added, and the tab stays unchanged. Ctrl+S leaves the file as it was." +} diff --git a/bugs/form-center-in-container-zoom/repro.json b/bugs/form-center-in-container-zoom/repro.json new file mode 100644 index 000000000..76cc32d3c --- /dev/null +++ b/bugs/form-center-in-container-zoom/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "at 50 %, Center In Container puts a control in a Frame off centre" + ], + "steps": "1. Open a form with a Frame 200 x 130 holding a TextBox 120 x 24 at 20,30.\n2. Choose View > Zoom Out twice, to 50 %.\n3. Select the TextBox and choose Format > Center In Container (Horizontally), then (Vertically). It goes to -9.98,20.52, not to 40,53.\nControl: at 100 % the same commands put it at 40,53." +} diff --git a/bugs/form-escape-marks-changed/repro.json b/bugs/form-escape-marks-changed/repro.json new file mode 100644 index 000000000..cd56092da --- /dev/null +++ b/bugs/form-escape-marks-changed/repro.json @@ -0,0 +1,9 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Escape, which selects the form, and then a pointer move over it mark the form changed with no control moved", + "the change that Escape and a pointer move make is an undo step that changes nothing" + ], + "steps": "1. Open a form that has not been changed and click a control.\n2. Press Escape: the form is selected.\n3. Move the pointer over the form, with no button pressed. The tab is marked changed, and closing asks to save; no control has moved.\n4. After an arrow key on a control and steps 2 and 3, the first Ctrl+Z changes nothing; the next undoes the arrow key.\nControl: a click on one control and then on another leaves the tab unchanged." +} diff --git a/bugs/form-lock-not-saved/repro.json b/bugs/form-lock-not-saved/repro.json new file mode 100644 index 000000000..0ba0e2676 --- /dev/null +++ b/bugs/form-lock-not-saved/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Lock Controls on its own does not mark the form changed: closing asks nothing, and the lock is gone when the form is opened again" + ], + "steps": "1. Open a form and click a control.\n2. Choose Format > Lock Controls. The control shows the lock icon; the tab is not marked changed.\n3. Close the designer: nothing is asked. Open it again: the control is not locked.\nControl: Lock Controls followed by an arrow key on another control and Ctrl+S keeps the lock." +} diff --git a/bugs/form-same-size-not-saved/repro.json b/bugs/form-same-size-not-saved/repro.json new file mode 100644 index 000000000..3d4e2779c --- /dev/null +++ b/bugs/form-same-size-not-saved/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Make Same Size > Height (Shortest) marks the form changed, but the form's state keeps the old heights, and Save writes them" + ], + "steps": "1. Open a form with Command1 and Command2 30 high and Check1 24 high.\n2. Select the three and choose Format > Make Same Size > Height (Shortest). All three are 24 high, and the tab is marked changed.\n3. Press Ctrl+S, close the designer and open it again. Command1 and Command2 are 30 high.\nControl: the same followed by an arrow key on another control, then Ctrl+S, keeps the 24." +} diff --git a/bugs/form-spacing-not-recorded/repro.json b/bugs/form-spacing-not-recorded/repro.json new file mode 100644 index 000000000..2277cf53a --- /dev/null +++ b/bugs/form-spacing-not-recorded/repro.json @@ -0,0 +1,9 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Horizontal Spacing > Remove moves the controls but sends no change, makes no undo step and leaves the form's state and the tab as they were", + "after Vertical Spacing > Remove, closing the designer asks nothing, and the form opens again with the old positions" + ], + "steps": "1. Open a form with three controls side by side, such as Command1 at Left 10, Check1 at 140 and Command2 at 250, each 100 wide.\n2. Select the three and choose Format > Horizontal Spacing > Remove. Check1 moves to 110 and Command2 to 210.\n3. The tab is not marked changed, Ctrl+Z does nothing, and closing the designer asks nothing. Opened again, the controls are at 140 and 250.\nThe same with Vertical Spacing > Remove, Make Equal and Increase.\nControl: Format > Align > Left marks the form changed, is undone by Ctrl+Z and is kept." +} diff --git a/bugs/form-switch-twin-folder/repro.json b/bugs/form-switch-twin-folder/repro.json new file mode 100644 index 000000000..fb2cd0146 --- /dev/null +++ b/bugs/form-switch-twin-folder/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Shift+F7 in the code of a form in the folder v1.twin.forms says the document is not part of a form" + ], + "steps": "1. A project with a form frmDeep in the folder Sources/v1.twin.forms.\n2. Open frmDeep.twin and press Shift+F7. The notification reads Unable to switch form design mode: this document is not part of a form, and the designer does not open.\nControl: Shift+F7 in frmMain.twin, in Sources, opens its designer, and the switch from frmDeep's designer opens frmDeep.twin." +} diff --git a/bugs/form-zoom-box-arrows-move/repro.json b/bugs/form-zoom-box-arrows-move/repro.json new file mode 100644 index 000000000..aed63dee6 --- /dev/null +++ b/bugs/form-zoom-box-arrows-move/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Up Arrow in the zoom box zooms out and also moves the selected control up, as a change to the form" + ], + "steps": "1. Open a form and click a control, such as Command1 at Top 50.\n2. Put the focus in the toolbar's zoom box (100 %) and press Up Arrow twice.\n3. The zoom goes to 75 % and then 50 %, and Command1 moves up one device pixel at each press (Top 48.67 at 150 % display scaling). The tab is marked changed, and each press is an undo step." +} diff --git a/bugs/form-zoom-out-floor/repro.json b/bugs/form-zoom-out-floor/repro.json new file mode 100644 index 000000000..e426290c6 --- /dev/null +++ b/bugs/form-zoom-out-floor/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "View > Zoom Out goes 100 %, 75 %, 50 % and then stays at 50 %, though the zoom box offers 25 %" + ], + "steps": "1. Open a form and click it, so that the designer has the focus.\n2. Choose View > Zoom Out four times. The zoom box reads 75 %, 50 %, 50 %, 50 %; it offers 25 %, which it can choose itself.\nControl: View > Zoom In from 50 % goes to 75 %." +} diff --git a/bugs/history-form-line-undefined/repro.json b/bugs/history-form-line-undefined/repro.json new file mode 100644 index 000000000..03506c4e6 --- /dev/null +++ b/bugs/history-form-line-undefined/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "the History entry for a change in a designer reads [line: undefined] in its tooltip" + ], + "steps": "1. Open a form, click a control and press Right Arrow.\n2. Show the HISTORY panel and hold the pointer over the entry for the form. The tooltip reads //Sources/frmMain.tbform [line: undefined] and the date.\nControl: an edit inside a procedure of a code file gives an entry whose tooltip reads [line: 5]." +} diff --git a/bugs/properties-ctrl-a/repro.json b/bugs/properties-ctrl-a/repro.json new file mode 100644 index 000000000..e7c85ef56 --- /dev/null +++ b/bugs/properties-ctrl-a/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "Ctrl+A in a PROPERTIES box selects nothing" + ], + "steps": "1. Open a form and click Command1.\n2. Click in the Caption box of PROPERTIES and press End, then Ctrl+A.\n3. Nothing is selected in the box (the caret stays at 8), and the designer's selection is unchanged. Typing then adds to the caption.\nControl: Ctrl+A with the designer focused selects every control." +} diff --git a/bugs/readonly-designer-empty-tab/repro.json b/bugs/readonly-designer-empty-tab/repro.json new file mode 100644 index 000000000..1b18e2837 --- /dev/null +++ b/bugs/readonly-designer-empty-tab/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "a package's read-only designer file opens an empty tab of its name besides the notice that it cannot be opened" + ], + "steps": "1. In a project that references the VB package, open Packages/VB/Sources/DESIGNERS/tbObjectDesigner_PROPERTYPAGE.tbppage from Project Explorer.\n2. The notification reads Can't open designer for a read-only document., and a tab of that name is added and selected, empty, with no close icon. A middle click closes it." +} diff --git a/bugs/report-floating-toolbox/repro.json b/bugs/report-floating-toolbox/repro.json new file mode 100644 index 000000000..68e9322bb --- /dev/null +++ b/bugs/report-floating-toolbox/repro.json @@ -0,0 +1,8 @@ +{ + "mode": "lane", + "lane": "ide:forms", + "tests": [ + "with a report open, a floating Toolbox lists every tool of a form" + ], + "steps": "1. Add a report with Add Windows Report. The docked Toolbox lists Pointer, Label, Shape, Line, Image, CheckMark and QRCode.\n2. Hide the Toolbox and show it again, so that it floats. It lists the 24 tools of a form, TextBox, CommandButton and Timer among them, and More components.\nControl: the docked Toolbox's seven." +} diff --git a/builder/page-baseline.json b/builder/page-baseline.json index 24f7e74e2..2c0f3f364 100644 --- a/builder/page-baseline.json +++ b/builder/page-baseline.json @@ -1,5 +1,5 @@ { "src": "docs", - "pages": 1243, - "staticFiles": 933 + "pages": 1248, + "staticFiles": 1009 } diff --git a/docs/IDE/Forms/Arranging.md b/docs/IDE/Forms/Arranging.md new file mode 100644 index 000000000..fc77468d2 --- /dev/null +++ b/docs/IDE/Forms/Arranging.md @@ -0,0 +1,129 @@ +--- +title: Arranging controls +parent: Designing forms +grand_parent: IDE +nav_order: 3 +permalink: /tB/IDE/Forms/Arranging +index: + - align controls + - make same size + - spacing, controls + - center in container + - Format menu +--- + +# Arranging controls + +The **Format** menu lines up the controls you have selected: it aligns their edges, gives them the same width or height, spaces them evenly, centres them in their container and changes their order. This page describes what each command does to the controls. The [Format menu](../Reference/Menu/Format) page lists the commands. + +![The Format menu with two controls selected in the form designer: all of its commands are available rather than greyed out.](../Menu/Images/Menu_Format_1.png){:width="232" height="281"} + +The same commands are in the right-click menu of a selection, and most have a button in the toolbar. They act on the controls selected in the designer, so the whole menu is greyed out until a designer is open with a selection. + +How many controls a command needs: + +| Commands | Controls needed | +|---|---| +| **Align**, **Make Same Size**, **Horizontal Spacing** and **Vertical Spacing** | Two or more. | +| **Make Equal** in the two spacing menus | Three or more. | +| **Center In Container**, **Bring To Front**, **Send To Back**, **Lock Controls** | One or more. | +| **Align to grid** in the right-click menu | One or more. | + +With a single control selected, the **Align** and **Make Same Size** submenus are greyed out and the **Center In Container**, order and lock commands are available. **Align → To Grid** in the menu bar needs two controls, so for one control use **Align to grid** in the right-click menu. + +The align, spacing, centre, **Make Same Size** and **Align to grid** commands skip a locked control and a control whose **Dock** is not none. A locked control still counts when the command measures the selection. A line is handled by both of its ends. + +None of the controls is the reference. The first control you clicked has no special role: the align commands measure the bounding box of the whole selection. + +## Align + +![The Format menu with Align highlighted and its submenu open to the right. Left, Center (Horizontal) and Right sit above Top, Center (Vertical) and Bottom, with To Grid on its own below; the four edge commands have ALT plus arrow-key shortcuts. All are available.](../Menu/Images/Menu_Format_Align.png){:width="436" height="281"} + +**Left**, **Right**, **Top** and **Bottom** move every control to an edge of the selection. They compare positions in form coordinates, so they work across containers. + +| Command | Keys | Result | +|---|---|---| +| **Left** | Alt+Left | Every control gets the leftmost left edge of the selection. | +| **Right** | Alt+Right | Every control gets the rightmost right edge. | +| **Top** | Alt+Up | Every control gets the topmost top edge. | +| **Bottom** | Alt+Down | Every control gets the lowest bottom edge. | + +**Center (Horizontal)** and **Center (Vertical)** have no keys. **To Grid** has none either. It rounds the **Left** and **Top** of each selected control down to the grid, and rounds the size down too: a control 24 pixels high becomes 20 with a 10-pixel grid. It acts at once, even when **Align Controls To Grid** is off. + +> [!NOTE] +> In twinBASIC BETA 1005, **Center (Horizontal)** and **Center (Vertical)** do not centre the controls on the middle of the selection. They centre each control on the left edge, or the top edge, of the selection. Three controls 100 pixels wide at **Left** 10, 140 and 250 all end at **Left** -40, their middles on 10, and may end outside their container. To line up the middles of a group, align the edges and then type the **Left** or **Top** values in [PROPERTIES](Properties). **Center In Container** centres a group in its container correctly at a zoom of 100 %. + +> [!NOTE] +> In twinBASIC BETA 1005, each Alt+arrow also opens the IDE's menu bar when you release the keys. The controls are aligned all the same, and nothing is lost. If the menu bar is in the way, align with the **Format → Align** commands or the toolbar buttons instead. + +## Make the controls the same size + +![The Format menu with Make Same Size highlighted and its submenu open to the right, offering Width (Widest), Width (Narrowest), Height (Tallest) and Height (Shortest), each with a CTRL plus SHIFT plus arrow-key shortcut.](../Menu/Images/Menu_Format_MakeSameSize.png){:width="523" height="281"} + +**Make Same Size** gives every selected control the **Width** or the **Height** of one control of the selection. + +| Command | Keys | Result | +|---|---|---| +| **Width (Widest)** | Ctrl+Shift+Right | Every control gets the width of the widest. | +| **Width (Narrowest)** | Ctrl+Shift+Left | Every control gets the width of the narrowest. | +| **Height (Tallest)** | Ctrl+Shift+Down | Every control gets the height of the tallest. | +| **Height (Shortest)** | Ctrl+Shift+Up | Every control gets the height of the shortest. | + +A command changes nothing when the controls already have the same size. + +> [!WARNING] +> In twinBASIC BETA 1005, **Make Same Size** changes the controls on the surface and marks the form changed, but it does not reach the saved form. If you press Ctrl+S, close the tab and open the form again, the controls have their old sizes. A later change, such as moving a control with an arrow key, saves the new sizes with it. After you use **Make Same Size**, make such a change before you save, or type the **Width** and **Height** values in [PROPERTIES](Properties) instead. + +## Space the controls evenly + +![The Format menu with Horizontal Spacing highlighted and its submenu open to the right. Make Equal, Increase, Decrease and Remove are all available.](../Menu/Images/Menu_Format_HorizontalSpacing.png){:width="345" height="281"} + +**Horizontal Spacing** works on the controls sorted by **Left**. **Vertical Spacing** does the same by **Top**, with the grid height and the bottom edge. In both, the first control stays where it is. + +| Command | Result | +|---|---| +| **Make Equal** | Needs three controls. The first and the last stay. The controls between them are placed so that the gaps are equal. | +| **Increase** | The second control moves by one grid width, the third by two grid widths, and so on. The gaps grow by one grid width each. | +| **Decrease** | The same, in the opposite direction. | +| **Remove** | Each control is placed with its edge on the edge of the one before it, so the gap is 0. | + +The step of **Increase** and **Decrease** is the **Grid Width**, or the **Grid Height** for a vertical command, even when **Align Controls To Grid** is off. + +For example, take three controls 100 pixels wide with **Left** 10, 140 and 250, and a grid of 10: + +| Command | New **Left** of the second and third | +|---|---| +| **Make Equal** | 130 and 250 | +| **Increase** | 150 and 270 | +| **Decrease** | 130 and 230 | +| **Remove** | 110 and 210 | + +The spacing commands compare positions that count from each control's own parent. Controls in different containers therefore give odd results. For example, **Remove** on a text box in a frame and a button on the form places the button at the text box's right edge as counted from the frame's corner, not from the form's. + +> [!WARNING] +> In twinBASIC BETA 1005, **Horizontal Spacing** and **Vertical Spacing** move the controls on the surface, but do not mark the form changed and do not make an undo step. Ctrl+Z does not undo them. If you close the tab, the IDE asks no question, and the form opens again with the old positions. The new positions are saved only when a later real change follows, for example moving a control with an arrow key, because that change saves the whole surface. After you use these commands, make such a change before you save, and check the positions in [PROPERTIES](Properties). To go back, type the old **Left** or **Top** values there. + +## Center in the container + +**Center In Container (Horizontally)** and **Center In Container (Vertically)** move the selection as a group, so that the group's bounding box is centred in the container the controls share. The controls keep their positions relative to one another. They work for one control or for many, and need all of them to have the same parent. If they do not, nothing moves and a notification says so. + +> [!NOTE] +> In twinBASIC BETA 1005, **Center In Container** reads the width of the container as it is drawn, so at a zoom other than 100 % it moves the controls to the wrong place. At 50 %, a text box in a frame ended at **Left** -9.98 instead of 40, and the group on the form moved by -80 instead of 20. Set the zoom to 100 % before you use the command. + +## Bring to front and send to back + +**Bring To Front** and **Send To Back** change the order in which controls are drawn, which decides which one is on top. They work within one parent: each selected control becomes the last, or the first, of its parent's controls. They have no keys. The toolbar has a button for each. + +## Lock controls + +**Lock Controls** locks or unlocks the selected controls. [Lock controls](Designer#lock-controls) explains it. + +## Undo + +Each command makes one undo step, and Ctrl+Z or the **Undo** button undoes it, except the spacing commands, which make none. + +## See Also + +- [Format menu](../Reference/Menu/Format) -- the commands, with their keys +- [Working with controls](Controls) -- selecting and moving controls +- [Designer](Designer) -- the grid and the locks diff --git a/docs/IDE/Forms/Controls.md b/docs/IDE/Forms/Controls.md new file mode 100644 index 000000000..f3a4aac50 --- /dev/null +++ b/docs/IDE/Forms/Controls.md @@ -0,0 +1,203 @@ +--- +title: Working with controls +parent: Designing forms +grand_parent: IDE +nav_order: 2 +permalink: /tB/IDE/Forms/Controls +index: + - draw a control + - select controls + - rubber band + - control array + - container + - in-place editing + - right-click menu, form designer +--- + +# Working with controls + +This page describes how to put controls on a form and work with them: drawing, selecting, moving and sizing, the clipboard, containers, control arrays, editing a caption in place and opening an event handler. [Arranging controls](Arranging) describes the commands that line up a selection. + +Most commands of the designer need the designer to have the focus. When the **PROPERTIES** pane has the focus, a key goes to the property box you are in, so Delete, the arrow keys, Ctrl+A and Ctrl+C do not touch the controls. A click on a menu item or a toolbar button works whichever pane has the focus. + +## Draw a control + +1. Click a tool in the [Toolbox](Toolbox). The pointer over the designer becomes a cross. +2. Click or drag on the form, or on a container. + +A drag gives the control that rectangle. With **Align Controls To Grid** on, the corner snaps to the grid. A plain click, with no movement, gives the control 200 by 50 pixels at the point you clicked. A **Timer** drawn by a click is 27 by 27. A **Line** runs from the point where you press to the point where you release, and snaps to the grid only when **Align Controls To Grid** is on. + +The new control is selected. It takes the form's font and the next free name: `Command1`, `Check1`, `Option1`, `Text1`, `Combo1`, `List1`, `HScroll1`, `VScroll1`, `Dir1`, `File1`, `Drive1`, `Picture1`, `CheckMark1` or `QRCode1`, and for other controls the class name and a number, such as `Label1`, `Frame1`, `Timer1`, `Shape1`, `Line1` or `Image1`. Its **Index** is -1, and its **TabIndex** is the highest on the form plus one. + +The tool returns to the pointer after each control. Esc cancels a tool you selected. A double-click on a tool in the Toolbox places that control without drawing: 150 by 40 pixels, centred from left to right on the form. + +Adding a **QRCode** control shows a message that the control is licensed under the MIT licence, with a link to `https://github.com/wqweto/VbQRCodegen`. + +## Select controls + +**Click** a control to select only it. Shift+click or Ctrl+click adds a control to the selection, or removes it when it is already selected. A click on an empty part of the form selects the form, and the PROPERTIES pane then shows the form's own properties. A right-click on a control that is not selected selects only that control. + +**Drag a band** on the form, or inside a container, with the pointer tool to select several controls at once. + +- The band selects every control it touches. A control inside a container is selected only when the band touches the container too. A line is selected when the band covers either end. +- Alt+drag selects only the controls that lie wholly inside the band. +- A band that selects nothing selects the form. +- Shift pressed on the background while a control is selected starts no band. + +![A form in the designer while a band is dragged on it: a dashed rectangle runs from the empty part of the form at the right across Label1 and into the right half of Text1.](Images/Controls_RubberBand.png){:width="386" height="333"} + +Ctrl+A selects every control on the form, including those inside containers. It does not select the form. The right-click menu has **Select All** too. + +Esc does one of two things. With nothing selected, or the form selected, it returns to the pointer tool. With a control selected, it selects the control's parent, the container or the form. Pressing it again climbs one level more, so a text box in a frame goes to the frame, then to the form. There is no **Select Parent** menu item. A right-click menu that is open makes Esc be ignored once. + +The object drop-down at the top of [PROPERTIES](Properties) lists the form and every control, and choosing one selects it. It reaches a control that is hidden, outside the visible part of the form or covered by another. Menu items that are not visible are selected in the [menu editor](Menu-Editor). + +![Three controls selected on a form: Command1 with blue grabbers, Text1 with green ones and Check1 with orange ones.](Images/Controls_MultiSelect.png){:width="386" height="333"} + +## Move a control + +Drag a control to move it. All selected controls move together. A control inside a selected container moves once, with its container. A locked control does not move, and neither does a control whose **Dock** is not none. With **Align Controls To Grid** on, a position is rounded down to a multiple of the grid size, measured from the container's corner. A line moves by both ends. + +A drag moves a control within its parent only. You cannot drag a control into a container or out of one, because **Left** and **Top** stay relative to the same parent. To move a control to another container, cut it, select the container or the form, and paste. + +The arrow keys move the selection by one screen pixel, or by one grid cell when **Align Controls To Grid** is on. A key held down repeats, and the repeated moves are one undo step. + +## Size a control + +Drag a grabber of a selected control to resize it. With **Align Controls To Grid** on, the new size is rounded down to a multiple of the grid size. A docked control has fewer grabbers: **Dock** left keeps only the right one, top only the bottom, right only the left, bottom only the top, and fill none. A line has two, one for each end. + +Shift+arrow makes the selection wider, narrower, taller or shorter, by the arrow keys' step. + +You can also type **Left**, **Top**, **Width** and **Height** in [PROPERTIES](Properties#units-of-position-and-size), in the unit the form's scale mode names. + +### Size the form + +Three handles resize the form: the middle of the right edge, the middle of the bottom edge and the bottom-right corner. They set the form's **Width** and **Height**, and do not snap to the grid. The handles on the top and left edges and the other corners are drawn dimmer and do nothing. + +On a CustomControls form, a control anchored to the left and top stays where it is when you drag a handle. On a Windows form the form designer does not stretch the controls. Their anchoring is the compiler's. See [Anchoring and docking](../../../Features/GUI-Components/Anchoring-Docking). + +Each drag of a handle is one undo step. + +## Copy, cut, paste and delete + +| Action | Keys | What it does | +|---|---|---| +| Copy | Ctrl+C, Ctrl+Insert | Puts the selected controls on the system clipboard as JSON text. Pasted into a text editor, the clipboard shows that text. | +| Cut | Ctrl+X, Shift+Delete | Copies, then deletes. | +| Paste | Ctrl+V | Adds the controls on the clipboard. | +| Paste (same name, control array) | none | Pastes as a [control array](#control-arrays). Right-click menu only. | +| Duplicate (new name) | none | Copies and pastes in one step. Right-click menu only. | +| Duplicate (same name, control array) | none | Copies and pastes as a control array. Right-click menu only. | +| Delete | Delete | Deletes the selection and the controls inside any container in it. The right-click menu has no **Delete**. The code in the `.twin` file is not touched. | + +The two **same name** items and **Paste** are greyed out in the right-click menu while the clipboard holds no controls. The two **same name** items are missing on a CustomControls form. If the IDE may not read the clipboard, **Paste** shows the notification *ERROR: unable to read clipboard contents*. + +Where a paste goes: + +- With exactly one container selected, a Frame for example, the controls go into it, at its top left corner. +- With nothing selected, so that the form is selected, they go to the form's top left corner. +- Otherwise they go into the same container as the first selected control, below the lowest selected control and 10 pixels from it. + +If no control on the form has any of the pasted names, which is the case when you paste into another form, the controls keep their names and their **Index**. Otherwise every pasted control gets a fresh name, such as `Command3`, an **Index** of -1, and the next free **TabIndex**. Event handlers are not copied, only the controls. Pasting a single menu item goes into the [menu editor](Menu-Editor). + +In a report only windowless controls can be pasted. See [Designing reports](../Reports/). + +## Containers + +A container is a control that holds other controls: a **Frame**, a **PictureBox**, a user control that allows it, and the CustomControls **WaynesFrame**. A control drawn inside a container belongs to it, and its **Left** and **Top** count from the container's corner. + +While a tool is selected, moving the pointer over a container outlines the container in red. The next control goes inside it. Hold Ctrl while you click to draw the control on the form instead. + +![A form in the designer with a tool selected and the pointer inside Frame1: a red outline surrounds the frame.](Images/Controls_Container.png){:width="386" height="333"} + +Moving a container moves its controls, and deleting it deletes them. A paste with a container selected goes inside it, and Esc on a control inside selects the container. **Center In Container** needs all selected controls to have the same parent. Otherwise it moves nothing and shows the notification *Selected controls are inside different containers*. + +## Control arrays + +Controls with the same name and different **Index** values form a control array. They share one set of event handlers, and each handler receives the **Index** as an argument. The designer lists a member as `Name(Index)`. **Index** is -1, shown blank, for a control that is not in an array. The legal range is -1 to 32767, and any other value you type becomes -1. A CustomControls form has no control arrays and no **Index** property. + +There are three ways to make an array: + +- Give a control the name of another control. A box asks *A control already exists with the name 'X'. Do you want to create a control array?* The first **YES** sets only the first control's **Index** to 0, and the same box comes again. The second **YES** renames the control to the next index. **NO** gives the control its old name back. If the name already belongs to an array, the renamed control joins it at once, with the next free index. A control type that has no **Index** property shows an error message instead. +- **Duplicate (same name, control array)** in the right-click menu. +- **Paste (same name, control array)** in the right-click menu. + +The last two give the new controls the highest **Index** plus one, and turn a lone original with **Index** -1 into index 0. Choosing an event of a member in the [EVENTS tab](Properties#events) inserts a handler with an `Index As Integer` argument, such as `Private Sub Command2_GotFocus(Index As Integer)`. + +> [!NOTE] +> In twinBASIC BETA 1005, the box that asks about the control array comes twice when you give a control the name of a single control. The first **YES** sets the other control's **Index** to 0 and leaves the renamed control's name unchanged, and the same box comes again. The second **YES** renames the control to `X(1)`. Answer **YES** both times. **NO** puts the old name back. + +## Names and code + +Renaming a control does not change the code. A handler such as `Text1_Change` stays as it is. Code that uses the old name, such as `Text1.Text = "hi"`, shows a *TB5079 Unrecognized symbol* problem in **DIAGNOSTICS** the next time the project compiles. A change of **Name** makes the compiler compile the project again. Deleting a control leaves its handlers in the code. + +## In-place editing + +With **IDE: Show in-place editing toolbar in the form designer** on, which is the default, a small toolbar shows below a single selected control. It is hidden while you drag. What it holds depends on the control. + +| Control | Tools | +|---|---| +| **Label**, **TextBox** | The pencil, **B**, **I** and **U** (highlighted when on), a font-size box from 1 to 100 points with **+** and **-** buttons, and left, centre and right alignment, which set **Alignment**. | +| **CommandButton**, **Frame**, **CheckBox**, **OptionButton** | The pencil and the font tools. | +| **Image** | The pencil, which opens a file dialog for **Picture**. | +| An ActiveX control | The pencil, which opens the control's Custom property page. | +| A **MultiFrame** | A **FramesCount** box. Lowering it shows *To reduce the frame count, select and delete the individual frames*. | +| Any other control with a **FontSize** | The font tools, without the pencil. | +| A control the compiler failed to draw | The error text. | + +The pencil opens a text box over a Label, CommandButton, TextBox, Frame, CheckBox or OptionButton. It holds the **Caption**, or the **Text** of a TextBox. Enter commits, Shift+Enter adds a line, Esc cancels, and a click elsewhere commits. An empty box sets the **Caption** to an empty string. The form itself has no in-place toolbar. + +The option in [IDE Options](../Customising/IDE-Options#form-designer-and-toolbox) turns the toolbar off for good. The command `tbFormDesigner_ToggleInPlaceEditingToolbar` hides or shows it for the session only. + +## Open an event handler + +Double-click a control, or the form, with the pointer tool. The code editor opens the control's default event handler, or the last event in the control's event list when the compiler names no default. If the handler exists, the editor goes to it. If not, the editor inserts an empty handler with the caret in its body. A menu item in the form's menu bar opens its `Click` handler the same way. A control with no events does nothing. + +For example, a double-click on a **Label** inserts `Label1_Click`, on a **TextBox** goes to `Text1_Change`, and on the form goes to `Form_Load`, when those exist. The [EVENTS tab](Properties#events) and the [combo boxes of the editor](../Code/Editor#the-navigation-combo-boxes) open and insert handlers in the same way. + +## The right-click menu + +A right-click opens a menu on the form, on a control, or on a menu item in the [menu editor](Menu-Editor). Its entries show the keys of the commands. + +![The right-click menu of two selected command buttons, open over the form. It lists Cut, Copy, the greyed Paste items and the two Duplicate items; then Align, Make Same Size, Horizontal Spacing and Vertical Spacing; the two Center In Container items; Bring To Front and Send To Back; Align to grid; and Lock Controls. The Align submenu is open to the right with Left, Center (Horizontal), Right, Top, Center (Vertical), Bottom and To Grid, and the keys of four of them.](Images/Controls_ContextMenu.png){:width="522" height="563"} + +**On the form**, the menu has **View Code**, **Paste**, **Paste (same name, control array)** (not on a CustomControls form), **Select All**, and **Lock all controls** and **Unlock all controls**. Only the one of the two lock items that applies shows: with every control locked, **Unlock all controls**, and with some locked, **Lock all controls**. + +**On one or more controls**, the menu has these groups: + +- **Cut**, **Copy**, **Paste**, **Paste (same name, control array)**, **Duplicate (new name)** and **Duplicate (same name, control array)**. The two **same name** items are not on a CustomControls form. +- **Align**, **Make Same Size**, **Horizontal Spacing** and **Vertical Spacing**, each a submenu. They are greyed out unless more than one control is selected. See [Arranging controls](Arranging). +- **Center In Container (Horizontally)** and **Center In Container (Vertically)**. +- **Bring To Front** and **Send To Back**. +- **Align to grid**, which snaps even one control to the grid. +- **Lock Controls** and **Unlock controls**. +- **ActiveX Properties...**, when a selected control is an ActiveX control, and, when exactly one selected control offers property pages, one item for each page. The label **ActiveX Properties...** is in English whatever the IDE language. The page names come from the control. + +## Keys + +| Key | What it does | +|---|---| +| Ctrl+Z, Ctrl+Y | Undo and redo. | +| Ctrl+X, Shift+Delete | Cut. | +| Ctrl+C, Ctrl+Insert | Copy. | +| Ctrl+V | Paste. | +| Delete | Delete the selected controls. | +| Ctrl+A | Select all the controls. | +| Esc | Return to the pointer tool, or select the parent. | +| Left, Right, Up, Down | Move the selection by one screen pixel, or one grid cell. | +| Shift+arrow | Resize the selection by the same step. | +| Alt+arrow | Align the selection to the left, right, top or bottom. | +| Ctrl+Shift+arrow | Make the selection the same width or height. | +| Ctrl held | Show the tab-index badges. | +| Shift+F7 | Open the designer from the code. | +| F6 | [Preview](Previewing) the form. | + +Ctrl+arrow does nothing in the form designer. With the **TOOLBOX** focused, Left and Up select the previous tool and Right and Down the next. The commands for the Center items, Bring To Front, Send To Back, Lock, Duplicate, Zoom In and Zoom Out, and for searching and focusing the Toolbox and PROPERTIES, have no default key. [Keyboard shortcuts](../Customising/Keyboard-Shortcuts) gives them one. + +## See Also + +- [Designer](Designer) -- opening a form, the grid, zoom, locks and the tab order +- [Arranging controls](Arranging) -- the **Format** menu +- [Toolbox](Toolbox) -- the controls to draw +- [Properties](Properties) -- changing what you have drawn +- [Menu editor](Menu-Editor) -- the menus of a form diff --git a/docs/IDE/Forms/Designer.md b/docs/IDE/Forms/Designer.md new file mode 100644 index 000000000..ed064ee85 --- /dev/null +++ b/docs/IDE/Forms/Designer.md @@ -0,0 +1,231 @@ +--- +title: Designer +parent: Designing forms +grand_parent: IDE +nav_order: 1 +permalink: /tB/IDE/Forms/Designer +index: + - add a form + - form, adding + - grid, form designer + - zoom, form designer + - lock controls + - tab order + - View As JSON + - Object Designer +--- + +# Designer + +A form is two files, a designer file and a code file. This page describes how to create a form, how to open its designer, how to move between design and code, and the parts of the designer window. It then describes the settings of the surface: grid, outlines, zoom, locks and tab order. [Working with controls](Controls) describes what you do on the surface. + +## Add a form + +The **Add** menu creates a form. It is in the right-click menu of the [Project Explorer](../Project/Explorer), and in the **Project** menu. The Windows items and the CustomControls item are always shown, whether the project refers to their package or not. + +| Item | Creates | Package it needs | +|---|---|---| +| **Add Windows Form** | `MyForm.tbform` and `MyForm.twin` | WinNativeForms | +| **Add Windows MDI Form** | `MyForm.tbform` and `MyForm.twin`, for an MDI form | WinNativeForms | +| **Add Windows UserControl** | `MyUserControl.tbcontrol` and `MyUserControl.twin` | WinNativeForms | +| **Add Windows PropertyPage** | `MyPropertyPage.tbppage` and `MyPropertyPage.twin` | WinNativeForms | +| **Add Windows Report** | `MyReport.tbreport` and `MyReport.twin`. See [Designing reports](../Reports/). | WinNativeForms | +| **Add CustomControls Form** | `MyForm.tbform` and `MyForm.twin` | CustomControls | +| **Add** *name* **Form** | A form of a kind that a package supplies, one item for each kind. | That package | + +The new files go into the folder that the Project Explorer has selected. The Project Explorer switches to its file view, and a name box opens in the tree. It holds the default name, `MyForm.tbform` for a form, with the name part selected. If the name is taken, the box offers `MyForm2`, `MyForm3` and so on. + +![The Project Explorer in its file view with the Sources folder expanded. The first row under Sources is a name box holding MyForm.tbform, with MyForm selected; the project's own files follow it.](Images/Designer_AddName.png){:width="267" height="304"} + +Enter creates both files at once and opens the designer in a normal tab, not a preview tab. Esc, or an empty name, creates nothing. A name with a space is written in square brackets, such as `Class [My Form]`, both in the class and in the form's **Name** property. A Windows form, an MDI form and a report are bracketed for a punctuation mark too, such as the dot in `a.b`. A user control, a property page and a CustomControls form are bracketed for a space only. + +> [!NOTE] +> In twinBASIC BETA 1005, the name box accepts a name that is not an identifier, such as `1bad`, and creates the files without a message. The class is then `Class 1bad`. + +What the new files hold depends on the kind: + +| Kind | Designer file | Code file | +|---|---|---| +| Windows form | A form 300 pixels wide and 300 high, with **StartUpPosition** `vbStartUpScreen`. | The attributes `[Description("")]`, `[FormDesignerId("...")]` and `[PredeclaredId]`, and `Class MyForm` with an empty `Sub New()`. | +| MDI form | As a form, without the start-up position. | As a form. | +| CustomControls form | A form 300 pixels wide and 300 high. | As a Windows form. | +| User control | A control 300 pixels wide and 300 high. | As a form, and also `[ClassId]`, `[InterfaceId]`, `[EventInterfaceId]` with new GUIDs, and `[COMControl]`. | +| Property page | As a user control. | As a user control, without `[COMControl]`. | +| Report | Five sections. | `Class MyReport` with `Sub New()` and an empty `Sub Report_Load()`. | + +The designer file holds only these few properties at first. When you save, the file also holds every property the compiler reports for the form and its controls. + +### The package prompts + +If the project does not refer to the package an item needs, the item shows a box titled **twinBASIC** instead of creating a form. It reads *Package needed* and the package's name, and says that you must first add a reference to the package, and that the compiler restarts after you click the button. + +![A twinBASIC message box headed Package needed: CustomControls, saying that a reference to the CustomControls package must be added to the project first and that the compiler will be restarted once it is, above an Add CustomControls package button and a Cancel button.](../Images/RightClick-Add-CustomControlsForm-Popup.png){:width="539" height="194"} + +![A twinBASIC message box headed Package needed: WinNativeForms, saying that a reference to the WinNativeForms package must be added to the project first and that the compiler will be restarted once it is, above an Add WinNativeForms package button and a Cancel button.](Images/Designer_PackageNeeded.png){:width="544" height="194"} + +The button, **Add WinNativeForms package** or **Add CustomControls package**, adds the reference and restarts the compiler. It creates no form. Choose the **Add** item again to create one. **Cancel** creates nothing. + +## Open a designer + +A designer opens for any of its four kinds of file when you open the file: + +- Double-click it in the Project Explorer, or single-click when **File Explorer: Single-Click Preview** is on. See [IDE Options](../Customising/IDE-Options#project-explorer-tabs-and-start-up). +- Open it from **OPEN EDITORS**, or double-click a row in **DIAGNOSTICS** that names it. +- Create it with an **Add** item. + +Opening a designer ends a form program that waits for input. See [Running](../Debugging/Running#what-ends-a-run). + +A file that the designer cannot use opens in one of three ways: + +- A form from a package, such as the designers in the VB package, is read-only. The IDE shows the notification *Can't open designer for a read-only document.* +- A file with a JSON error shows the notification *ERROR: bad JSON encountered when parsing tbform*. +- A file without a `FormDesignerId` opens normally. The designer makes a new id for the session and does not write it to the file. + +> [!NOTE] +> In twinBASIC BETA 1005, a read-only designer file still gets a tab, which is empty and selected. Close it. + +> [!NOTE] +> In twinBASIC BETA 1005, a form file with a JSON error opens an empty designer with no controls, and PROPERTIES is empty. An edit there is refused, and Ctrl+S leaves the file as it was, so nothing is lost. Correct the JSON in a text editor outside the IDE, and open the form again. + +### The two files of a form + +A form's designer file and its `.twin` file belong together. Renaming one in the Project Explorer asks *Rename* the partner *to* the new name *also?*, and **YES** renames both. **Delete Permanently** on one of the pair offers **Delete Both**, **Delete** the file's name, and **Cancel**, when the IDE asks before it deletes files. See [IDE Options](../Customising/IDE-Options#project-explorer-tabs-and-start-up) for **File Explorer: Ask Before Delete Files**. + +### View a form as JSON + +**View As JSON** in the Project Explorer's right-click menu shows the designer file as text. It is in the menu for `.tbform`, `.tbcontrol`, `.tbppage` and `.tbreport` files and for the files in a package, and it is hidden when more than one item is selected. The text opens in a read-only tab titled `JSON: 'frmMain.tbform':`. A comment at its top gives the `URI:` of the file and the `SNAPSHOT:` time. The tab is a snapshot and does not follow later changes, but it includes the changes in the designer that you have not saved yet. + +## Switch between design and code + +Three commands open the code file from the designer: + +- The toolbar button **Switch Between Form And Code**. +- **View → Code Editor**. +- **View Code** in the designer's [right-click menu](Controls#the-right-click-menu). + +None of them has a key. The code file opens in a normal tab, or in a preview tab when **File Explorer: Single-Click Preview** is on. + +From the code file, **View → Object Designer** and Shift+F7 open the designer. The command looks for a `.tbform`, then a `.tbcontrol`, a `.tbppage` and a `.tbreport` with the same name. The toolbar button works from either side. F7 and Esc do nothing in a designer. + +Both switches find the partner file by replacing `.twin` or `.tbform` in the path. If the file has no partner, a notification says *Unable to switch form design mode: this document is not part of a form*. + +> [!NOTE] +> In twinBASIC BETA 1005, a folder whose name contains `.twin`, such as `v1.twin.forms`, breaks the switch from the code to the designer, and the notification above appears. The switch from the designer to the code still works. Open the designer from the Project Explorer, or rename the folder. + +## The parts of the window + +The designer uses the IDE's toolbar for its buttons. A button is greyed out unless the context of its command holds. The [Toolbar](../Reference/Toolbar) page names them. A few need explaining: + +- The **grid** button opens a small panel of options when the pointer enters it. +- The **zoom** box sets the size of the surface. See [Zoom](#zoom). +- **PREVIEW**, titled *Launch the form in isolation to test its functionality*, runs the form on its own. See [Previewing a form](Previewing). + +![The form designer open on a tab named MyForm.tbform in Editor 1. An empty form occupies the top left corner as a white dotted grid, with a plus menu button above it, sizing handles on its right and bottom edges, and a 300 x 300 size readout at its lower corner.](../Images/tbForm.png){:width="1319" height="698"} + +| Part | What it is | +|---|---| +| **Form Menu** bar | The strip above a form where its menus are designed. It has a **+ menu** button. See [Menu editor](Menu-Editor). | +| The surface | The form's client area, drawn from the compiler's picture of it. | +| Size handles | Three handles on the form's right edge, bottom edge and bottom-right corner resize the form. The five others, on the top and left edges and the other corners, are drawn dimmer and do nothing. See [Size the form](Controls#size-the-form). | +| Readouts | While you are not drawing, dragging or sizing a control, the pointer position (`x:` and `y:`) shows at the lower left of the surface and the form's size at the lower right, whatever is selected. While you draw, drag or size a control, its position and size show instead, and they disappear when you release the button. The readouts are always in pixels. | +| Grabbers | Eight small circles round a selected control. A docked control has fewer. A locked control shows a lock icon. | +| Tab-index badges | Yellow circles that show while you hold Ctrl. See [Tab order](#tab-order). | +| In-place toolbar | The small bar below a selected control. See [In-place editing](Controls#in-place-editing). | + +![A form in the designer at 100 %, with a File menu and the + menu button in the bar above it. The form holds two text boxes, a label, two command buttons, a check box, a frame round one of the text boxes, and a control array of two buttons. The label is selected: eight round grabbers surround it, and a bar of editing buttons sits below it. Under the form, a box at the left shows the pointer position and one at the right the form's size, 380 x 270. Labels with arrows name the grabbers, the bar and the two boxes.](Images/Designer_Selected.png){:width="548" height="387"} + +While the designer loads, an overlay lists the steps of the connection to the compiler, with a progress bar. A failure reads *error 802; connection closed*, *803 connection errored* or *804 connection abandoned*. **Ignore and continue** hides the overlay. The overlay shows for a fraction of a second on a normal start. + +A touch screen works too. A one-finger drag with a tool selected draws a control, and a pinch with two fingers zooms. + +## Grid and outlines + +The grid options are in the panel that opens when the pointer enters the grid button. + +**Show Grid** +: Default on. Draws the grid as dots on the surface. + +**Align Controls To Grid** +: Default off. Snaps a control to the grid when you draw, drag or size it, and moves it by one grid cell for each arrow key press. + +**Show Outlines** +: Default off. Draws a dashed outline round every control, useful for a control with no visible border. + +**Multi-coloured Grabbers** +: On for a new form. A form file that lacks the setting opens with it off. With it on and several controls selected, the grabbers of each control have a different colour, so the controls can be told apart. + +**Grid Width** and **Grid Height** +: Default 10 pixels each. The smallest value is 1. + +![The grid options panel open below the grid button of the toolbar. Show Grid and Multi-coloured Grabbers are ticked, Align Controls To Grid and Show Outlines are not, and Grid Width and Grid Height each hold 10.](Images/Designer_GridOptions.png){:width="227" height="191"} + +These options belong to the form. They are saved in the form file, so another form can have another grid. Changing one is an undo step, marks the form changed and refreshes the designer from the compiler. + +With **Align Controls To Grid** on, a position is rounded down to a multiple of the grid size, measured from the container's top left corner. **Format → Align → To Grid** snaps the selected controls to the grid at once, whatever the setting. The step of **Increase** and **Decrease** in the spacing commands is the grid width or height even when snapping is off. See [Arranging controls](Arranging). + +Each toggle is also a command with no menu item and no key: `tbFormDesigner_ToggleGridIndicators` (the grid button), `ToggleAlignControlsToGrid`, `ToggleControlOutlines`, `ToggleMultiColouredGrabbers`, `ToggleLockAllControls`, `ToggleShowTabIndex` and `ToggleInPlaceEditingToolbar`, each with the `tbFormDesigner_` prefix. The grid indicators, the alignment to the grid, the outlines, the in-place toolbar and the tab-index overlay also have `TurnOn` and `TurnOff` forms, such as `TurnGridIndicatorsOn`. [Keyboard shortcuts](../Customising/Keyboard-Shortcuts) describes how to give any of them a key. + +## Zoom + +The zoom box in the toolbar offers 25 % to 400 % in steps of 25 %. The surface is scaled, and clicks and drags still act in form pixels: at 50 %, a drag of 30 by 12 screen pixels moves a control by 60 by 24 form pixels, and the readouts show form pixels. The zoom belongs to the designer and is not saved. + +With a designer focused, the **View** menu shows **Zoom In** and **Zoom Out** in place of **Increase Font Size** and **Decrease Font Size**. They have no keys. Zoom has no mouse-wheel shortcut and no key of its own. + +![The zoom box of the toolbar showing 50 %, with its list open below it and 50 % highlighted between 25 % and 75 %; the list scrolls on to further values.](Images/Designer_Zoom.png){:width="240" height="119"} + + + + +> [!NOTE] +> In twinBASIC BETA 1005, **Zoom Out** stops at 50 %, although the box offers 25 %. Choose 25 % in the box. + +> [!WARNING] +> In twinBASIC BETA 1005, the arrow keys also move the selected controls while the zoom box has the focus. With a control selected, Up pressed twice there changes the zoom by two steps and moves the control up by two steps. The form is marked changed, each press is an undo step, and a save keeps the new position. Nothing on the screen says the control moved. Check the positions of the controls before you save. + +## Lock controls + +**Format → Lock Controls** locks each selected control that is unlocked and unlocks each one that is locked, so a mixed selection swaps. The right-click menu offers **Lock Controls** and **Unlock controls** for a selection, and **Lock all controls** and **Unlock all controls** on the form itself. Each of the two commands for all controls then selects every control. + +A locked control is drawn grey, with a lock icon. You cannot move or size it with the mouse or the keys, and the commands that arrange controls skip it. You can still select, cut, delete and edit it in PROPERTIES. A locked control still counts in the bounding box that the align commands use. + +The lock is stored under the control's name, so it follows a rename, and it covers every member of a control array. + +![A form with Show Outlines on, so that a dashed outline surrounds every control. The command button Command1 is selected and locked: a grey lock icon shows on it and its grabbers are grey. The other buttons are unlocked.](Images/Designer_Locked.png){:width="386" height="333"} + +> [!WARNING] +> In twinBASIC BETA 1005, locking or unlocking controls does not mark the form changed. If you lock a control and close the tab, the IDE asks no question, and the lock is gone when you open the form again. The lock is saved only with the next real change. After you lock a control, make another change to the form, such as moving a control, before you save. + +## Tab order + +Holding Ctrl while the designer has the focus shows a yellow circle at the top right of every control whose **TabStop** is **True**, holding the control's **TabIndex**. A control that takes no focus, such as a **Label** or a **Frame**, has no circle, and neither has a control on a CustomControls form. The circles disappear when you release the key. Ctrl is also the key that adds a control to the selection, so a Ctrl+click makes them flash. + +There is no tab-order dialog and no tool that sets the order by clicking. You set **TabIndex** as a property in [PROPERTIES](Properties). A new control gets the highest **TabIndex** plus one. + +![A form while Ctrl is held. Yellow circles at the top right of the text boxes, the check box and the command buttons hold their TabIndex values, 0, 2, 3, 5, 6 and 7.](Images/Designer_TabIndex.png){:width="386" height="333"} + +## Save, undo and history + +A designer tab shows the dot for unsaved changes as soon as the form changes. Ctrl+S writes the file, and closing the tab asks the usual question. + +Ctrl+Z and Ctrl+Y, and the **Undo** and **Redo** buttons of the toolbar, step through the changes. Ctrl+Shift+Z is not bound. The designer keeps a copy of the whole form for every step, with no limit, until you close the tab. + +- A drag, an arrow key press, a Shift+arrow press and each [Format](Arranging) command except the spacing commands are one step each. A key held down, repeating, is one step, however long you hold it. +- Undo does not cover the code in the `.twin` file. +- After a save, the steps stay, and Ctrl+Z still undoes. After you undo to the state you saved, the tab stays marked changed. + +Every real change also writes an entry to the **HISTORY** panel, named for the form. Double-click it to open the form's designer. See [History](../Code/Navigating#history). + +> [!NOTE] +> In twinBASIC BETA 1005, the tooltip of a form's entry in **HISTORY** ends with `[line: undefined]`, because a form change has no line. Nothing is lost. + +> [!NOTE] +> In twinBASIC BETA 1005, a designer can be marked changed with no edit. Click a control, press Esc to select the form, and then click another control or only move the pointer over the form: the tab gets the dot, and closing asks to save. It does not happen every time. Every control is where it was, so saving or discarding gives the same form. Ctrl+Z then steps through two empty steps before it reaches your last real change. + +## See Also + +- [Working with controls](Controls) -- drawing, selecting, moving and sizing +- [Arranging controls](Arranging) -- the **Format** menu +- [Toolbox](Toolbox) -- the controls you can draw +- [Properties](Properties) -- the PROPERTIES pane +- [Previewing a form](Previewing) -- Preview, F6 and RESYNC +- [Toolbar](../Reference/Toolbar) -- the buttons of the toolbar diff --git a/docs/IDE/Forms/Images/Controls_Container.light.png b/docs/IDE/Forms/Images/Controls_Container.light.png new file mode 100644 index 000000000..743d29ffd Binary files /dev/null and b/docs/IDE/Forms/Images/Controls_Container.light.png differ diff --git a/docs/IDE/Forms/Images/Controls_Container.light.svg b/docs/IDE/Forms/Images/Controls_Container.light.svg new file mode 100644 index 000000000..56b8cec50 --- /dev/null +++ b/docs/IDE/Forms/Images/Controls_Container.light.svg @@ -0,0 +1 @@ +File+ menux:120 y:220380 x 270 diff --git a/docs/IDE/Forms/Images/Controls_Container.png b/docs/IDE/Forms/Images/Controls_Container.png new file mode 100644 index 000000000..3ca724141 Binary files /dev/null and b/docs/IDE/Forms/Images/Controls_Container.png differ diff --git a/docs/IDE/Forms/Images/Controls_Container.svg b/docs/IDE/Forms/Images/Controls_Container.svg new file mode 100644 index 000000000..21d189cd4 --- /dev/null +++ b/docs/IDE/Forms/Images/Controls_Container.svg @@ -0,0 +1 @@ +File+ menux:120 y:220380 x 270 diff --git a/docs/IDE/Forms/Images/Controls_ContextMenu.light.png b/docs/IDE/Forms/Images/Controls_ContextMenu.light.png new file mode 100644 index 000000000..bccab8341 Binary files /dev/null and b/docs/IDE/Forms/Images/Controls_ContextMenu.light.png differ diff --git a/docs/IDE/Forms/Images/Controls_ContextMenu.light.svg b/docs/IDE/Forms/Images/Controls_ContextMenu.light.svg new file mode 100644 index 000000000..6030a3a38 --- /dev/null +++ b/docs/IDE/Forms/Images/Controls_ContextMenu.light.svg @@ -0,0 +1 @@ +frmMain.tbformFile+ menu380 x 270CutCTRL+X / SHIFT+DELETECopyCTRL+C / CTRL+INSERTPasteCTRL+VPaste (same name, control array)Duplicate (new name)Duplicate (same name, control array)AlignMake Same SizeHorizontal SpacingVertical SpacingCenter In Container (Horizontally)Center In Container (Vertically)Bring To FrontSend To BackAlign to gridLock ControlsLeftALT+ARROWLEFTCenter (Horizontal)RightALT+ARROWRIGHTTopALT+ARROWUPCenter (Vertical)BottomALT+ARROWDOWNTo Grid diff --git a/docs/IDE/Forms/Images/Controls_ContextMenu.png b/docs/IDE/Forms/Images/Controls_ContextMenu.png new file mode 100644 index 000000000..9cc14efec Binary files /dev/null and b/docs/IDE/Forms/Images/Controls_ContextMenu.png differ diff --git a/docs/IDE/Forms/Images/Controls_ContextMenu.svg b/docs/IDE/Forms/Images/Controls_ContextMenu.svg new file mode 100644 index 000000000..822fb7ec5 --- /dev/null +++ b/docs/IDE/Forms/Images/Controls_ContextMenu.svg @@ -0,0 +1 @@ +frmMain.tbformFile+ menu380 x 270CutCTRL+X / SHIFT+DELETECopyCTRL+C / CTRL+INSERTPasteCTRL+VPaste (same name, control array)Duplicate (new name)Duplicate (same name, control array)AlignMake Same SizeHorizontal SpacingVertical SpacingCenter In Container (Horizontally)Center In Container (Vertically)Bring To FrontSend To BackAlign to gridLock ControlsLeftALT+ARROWLEFTCenter (Horizontal)RightALT+ARROWRIGHTTopALT+ARROWUPCenter (Vertical)BottomALT+ARROWDOWNTo Grid diff --git a/docs/IDE/Forms/Images/Controls_MultiSelect.light.png b/docs/IDE/Forms/Images/Controls_MultiSelect.light.png new file mode 100644 index 000000000..779aa795f Binary files /dev/null and b/docs/IDE/Forms/Images/Controls_MultiSelect.light.png differ diff --git a/docs/IDE/Forms/Images/Controls_MultiSelect.light.svg b/docs/IDE/Forms/Images/Controls_MultiSelect.light.svg new file mode 100644 index 000000000..341d5cd47 --- /dev/null +++ b/docs/IDE/Forms/Images/Controls_MultiSelect.light.svg @@ -0,0 +1 @@ +File+ menux:190 y:86380 x 270 diff --git a/docs/IDE/Forms/Images/Controls_MultiSelect.png b/docs/IDE/Forms/Images/Controls_MultiSelect.png new file mode 100644 index 000000000..cd28eecb9 Binary files /dev/null and b/docs/IDE/Forms/Images/Controls_MultiSelect.png differ diff --git a/docs/IDE/Forms/Images/Controls_MultiSelect.svg b/docs/IDE/Forms/Images/Controls_MultiSelect.svg new file mode 100644 index 000000000..746e91599 --- /dev/null +++ b/docs/IDE/Forms/Images/Controls_MultiSelect.svg @@ -0,0 +1 @@ +File+ menux:190 y:86380 x 270 diff --git a/docs/IDE/Forms/Images/Controls_RubberBand.light.png b/docs/IDE/Forms/Images/Controls_RubberBand.light.png new file mode 100644 index 000000000..819eefd96 Binary files /dev/null and b/docs/IDE/Forms/Images/Controls_RubberBand.light.png differ diff --git a/docs/IDE/Forms/Images/Controls_RubberBand.light.svg b/docs/IDE/Forms/Images/Controls_RubberBand.light.svg new file mode 100644 index 000000000..6ffae0bfc --- /dev/null +++ b/docs/IDE/Forms/Images/Controls_RubberBand.light.svg @@ -0,0 +1 @@ +File+ menu380 x 270 diff --git a/docs/IDE/Forms/Images/Controls_RubberBand.png b/docs/IDE/Forms/Images/Controls_RubberBand.png new file mode 100644 index 000000000..0b5abf954 Binary files /dev/null and b/docs/IDE/Forms/Images/Controls_RubberBand.png differ diff --git a/docs/IDE/Forms/Images/Controls_RubberBand.svg b/docs/IDE/Forms/Images/Controls_RubberBand.svg new file mode 100644 index 000000000..61dad030d --- /dev/null +++ b/docs/IDE/Forms/Images/Controls_RubberBand.svg @@ -0,0 +1 @@ +File+ menu380 x 270 diff --git a/docs/IDE/Forms/Images/Designer_AddName.light.png b/docs/IDE/Forms/Images/Designer_AddName.light.png new file mode 100644 index 000000000..ba8a1d939 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_AddName.light.png differ diff --git a/docs/IDE/Forms/Images/Designer_AddName.png b/docs/IDE/Forms/Images/Designer_AddName.png new file mode 100644 index 000000000..a692c1671 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_AddName.png differ diff --git a/docs/IDE/Forms/Images/Designer_GridOptions.light.png b/docs/IDE/Forms/Images/Designer_GridOptions.light.png new file mode 100644 index 000000000..cce0b9906 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_GridOptions.light.png differ diff --git a/docs/IDE/Forms/Images/Designer_GridOptions.light.svg b/docs/IDE/Forms/Images/Designer_GridOptions.light.svg new file mode 100644 index 000000000..43fc77870 --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_GridOptions.light.svg @@ -0,0 +1 @@ +100%Show GridAlign Controls To GridShow OutlinesMulti-coloured GrabbersGrid WidthGrid Height1010 diff --git a/docs/IDE/Forms/Images/Designer_GridOptions.png b/docs/IDE/Forms/Images/Designer_GridOptions.png new file mode 100644 index 000000000..42b7e5abd Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_GridOptions.png differ diff --git a/docs/IDE/Forms/Images/Designer_GridOptions.svg b/docs/IDE/Forms/Images/Designer_GridOptions.svg new file mode 100644 index 000000000..21cdadae7 --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_GridOptions.svg @@ -0,0 +1 @@ +100%Show GridAlign Controls To GridShow OutlinesMulti-coloured GrabbersGrid WidthGrid Height1010 diff --git a/docs/IDE/Forms/Images/Designer_Locked.light.png b/docs/IDE/Forms/Images/Designer_Locked.light.png new file mode 100644 index 000000000..7a868ec5a Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_Locked.light.png differ diff --git a/docs/IDE/Forms/Images/Designer_Locked.png b/docs/IDE/Forms/Images/Designer_Locked.png new file mode 100644 index 000000000..557975db5 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_Locked.png differ diff --git a/docs/IDE/Forms/Images/Designer_PackageNeeded.light.png b/docs/IDE/Forms/Images/Designer_PackageNeeded.light.png new file mode 100644 index 000000000..8299dc607 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_PackageNeeded.light.png differ diff --git a/docs/IDE/Forms/Images/Designer_PackageNeeded.light.svg b/docs/IDE/Forms/Images/Designer_PackageNeeded.light.svg new file mode 100644 index 000000000..25b0354ea --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_PackageNeeded.light.svg @@ -0,0 +1 @@ +twinBASICAdd WinNativeForms packageCancelPackage needed: WinNativeFormsYou must first add a reference to the WinNativeForms package in this project.After clicking `Add WinNativeForms package`, the compiler will be restarted,ready for you to add forms to this project. diff --git a/docs/IDE/Forms/Images/Designer_PackageNeeded.png b/docs/IDE/Forms/Images/Designer_PackageNeeded.png new file mode 100644 index 000000000..53af2fff5 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_PackageNeeded.png differ diff --git a/docs/IDE/Forms/Images/Designer_PackageNeeded.svg b/docs/IDE/Forms/Images/Designer_PackageNeeded.svg new file mode 100644 index 000000000..39553d38c --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_PackageNeeded.svg @@ -0,0 +1 @@ +twinBASICAdd WinNativeForms packageCancelPackage needed: WinNativeFormsYou must first add a reference to the WinNativeForms package in this project.After clicking `Add WinNativeForms package`, the compiler will be restarted,ready for you to add forms to this project. diff --git a/docs/IDE/Forms/Images/Designer_Selected.light.png b/docs/IDE/Forms/Images/Designer_Selected.light.png new file mode 100644 index 000000000..9570c9f4a Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_Selected.light.png differ diff --git a/docs/IDE/Forms/Images/Designer_Selected.light.svg b/docs/IDE/Forms/Images/Designer_Selected.light.svg new file mode 100644 index 000000000..65b116662 --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_Selected.light.svg @@ -0,0 +1 @@ +File+ menux:300 y:245380 x 270BIU8pt+-GrabbersIn-place toolbarReadouts diff --git a/docs/IDE/Forms/Images/Designer_Selected.png b/docs/IDE/Forms/Images/Designer_Selected.png new file mode 100644 index 000000000..c0ecb3df7 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_Selected.png differ diff --git a/docs/IDE/Forms/Images/Designer_Selected.svg b/docs/IDE/Forms/Images/Designer_Selected.svg new file mode 100644 index 000000000..77524df3d --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_Selected.svg @@ -0,0 +1 @@ +File+ menux:300 y:245380 x 270BIU8pt+-GrabbersIn-place toolbarReadouts diff --git a/docs/IDE/Forms/Images/Designer_TabIndex.light.png b/docs/IDE/Forms/Images/Designer_TabIndex.light.png new file mode 100644 index 000000000..a9fb4122c Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_TabIndex.light.png differ diff --git a/docs/IDE/Forms/Images/Designer_TabIndex.light.svg b/docs/IDE/Forms/Images/Designer_TabIndex.light.svg new file mode 100644 index 000000000..cd1e3670e --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_TabIndex.light.svg @@ -0,0 +1 @@ +File+ menu023567x:300 y:245380 x 270 diff --git a/docs/IDE/Forms/Images/Designer_TabIndex.png b/docs/IDE/Forms/Images/Designer_TabIndex.png new file mode 100644 index 000000000..d76b375af Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_TabIndex.png differ diff --git a/docs/IDE/Forms/Images/Designer_TabIndex.svg b/docs/IDE/Forms/Images/Designer_TabIndex.svg new file mode 100644 index 000000000..24a485eeb --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_TabIndex.svg @@ -0,0 +1 @@ +File+ menu023567x:300 y:245380 x 270 diff --git a/docs/IDE/Forms/Images/Designer_Zoom.light.png b/docs/IDE/Forms/Images/Designer_Zoom.light.png new file mode 100644 index 000000000..16196dde9 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_Zoom.light.png differ diff --git a/docs/IDE/Forms/Images/Designer_Zoom.light.svg b/docs/IDE/Forms/Images/Designer_Zoom.light.svg new file mode 100644 index 000000000..d4c6619c0 --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_Zoom.light.svg @@ -0,0 +1 @@ +50%25%50%75%100% diff --git a/docs/IDE/Forms/Images/Designer_Zoom.png b/docs/IDE/Forms/Images/Designer_Zoom.png new file mode 100644 index 000000000..2855f96b9 Binary files /dev/null and b/docs/IDE/Forms/Images/Designer_Zoom.png differ diff --git a/docs/IDE/Forms/Images/Designer_Zoom.svg b/docs/IDE/Forms/Images/Designer_Zoom.svg new file mode 100644 index 000000000..f3a54f15d --- /dev/null +++ b/docs/IDE/Forms/Images/Designer_Zoom.svg @@ -0,0 +1 @@ +50%25%50%75%100% diff --git a/docs/IDE/Forms/Images/MenuEditor_Bar.light.png b/docs/IDE/Forms/Images/MenuEditor_Bar.light.png new file mode 100644 index 000000000..0d485ce6c Binary files /dev/null and b/docs/IDE/Forms/Images/MenuEditor_Bar.light.png differ diff --git a/docs/IDE/Forms/Images/MenuEditor_Bar.light.svg b/docs/IDE/Forms/Images/MenuEditor_Bar.light.svg new file mode 100644 index 000000000..e46844244 --- /dev/null +++ b/docs/IDE/Forms/Images/MenuEditor_Bar.light.svg @@ -0,0 +1 @@ +File+ menu&Edit diff --git a/docs/IDE/Forms/Images/MenuEditor_Bar.png b/docs/IDE/Forms/Images/MenuEditor_Bar.png new file mode 100644 index 000000000..fb1b2669b Binary files /dev/null and b/docs/IDE/Forms/Images/MenuEditor_Bar.png differ diff --git a/docs/IDE/Forms/Images/MenuEditor_Bar.svg b/docs/IDE/Forms/Images/MenuEditor_Bar.svg new file mode 100644 index 000000000..083092ce8 --- /dev/null +++ b/docs/IDE/Forms/Images/MenuEditor_Bar.svg @@ -0,0 +1 @@ +File+ menu&Edit diff --git a/docs/IDE/Forms/Images/MenuEditor_Open.light.png b/docs/IDE/Forms/Images/MenuEditor_Open.light.png new file mode 100644 index 000000000..36f7ff98c Binary files /dev/null and b/docs/IDE/Forms/Images/MenuEditor_Open.light.png differ diff --git a/docs/IDE/Forms/Images/MenuEditor_Open.light.svg b/docs/IDE/Forms/Images/MenuEditor_Open.light.svg new file mode 100644 index 000000000..fb694de66 --- /dev/null +++ b/docs/IDE/Forms/Images/MenuEditor_Open.light.svg @@ -0,0 +1 @@ +frmMain.tbformFile+ menu380 x 270OpenExit++RenameF2CutCopyPaste BeforePaste AfterPaste IntoMove UpCTRL+ARROWUPMove DownCTRL+ARROWDOWNToggle CheckedSPACEToggle EnabledToggle VisibleInsert AboveInsert BelowInsert Seperator AboveInsert Seperator BelowDeleteDELETE diff --git a/docs/IDE/Forms/Images/MenuEditor_Open.png b/docs/IDE/Forms/Images/MenuEditor_Open.png new file mode 100644 index 000000000..06651d193 Binary files /dev/null and b/docs/IDE/Forms/Images/MenuEditor_Open.png differ diff --git a/docs/IDE/Forms/Images/MenuEditor_Open.svg b/docs/IDE/Forms/Images/MenuEditor_Open.svg new file mode 100644 index 000000000..bdc2c08ae --- /dev/null +++ b/docs/IDE/Forms/Images/MenuEditor_Open.svg @@ -0,0 +1 @@ +frmMain.tbformFile+ menu380 x 270OpenExit++RenameF2CutCopyPaste BeforePaste AfterPaste IntoMove UpCTRL+ARROWUPMove DownCTRL+ARROWDOWNToggle CheckedSPACEToggle EnabledToggle VisibleInsert AboveInsert BelowInsert Seperator AboveInsert Seperator BelowDeleteDELETE diff --git a/docs/IDE/Forms/Images/Preview_Window.png b/docs/IDE/Forms/Images/Preview_Window.png new file mode 100644 index 000000000..451b4c2a0 Binary files /dev/null and b/docs/IDE/Forms/Images/Preview_Window.png differ diff --git a/docs/IDE/Forms/Images/Properties_ColourPicker.light.png b/docs/IDE/Forms/Images/Properties_ColourPicker.light.png new file mode 100644 index 000000000..fb9f2f77b Binary files /dev/null and b/docs/IDE/Forms/Images/Properties_ColourPicker.light.png differ diff --git a/docs/IDE/Forms/Images/Properties_ColourPicker.light.svg b/docs/IDE/Forms/Images/Properties_ColourPicker.light.svg new file mode 100644 index 000000000..0981f3a10 --- /dev/null +++ b/docs/IDE/Forms/Images/Properties_ColourPicker.light.svg @@ -0,0 +1 @@ +PROPERTIESDESIGNAPPEARANCEDRAWINGBEHAVIOURNameCommand1IndexCaption...Command1ToolTipText...VisualStylesTrueAppearance1 - vbAppear3dBackColor&H(BGR)8000000FForeColor&H(BGR)80000012Style0 - vbButtonStandardFontNameSegoe UIFontSize8FontBoldFalseFontItalicFalseFontStrikethruFalseFontUnderlineFalsePicture{0 bytes}...PictureDpiScalingFalseDownPicture{0 bytes}...DisabledPicture{0 bytes}...PictureAlignment1 - vbAlignTopMaskColor&H(BGR)C0C0C0UseMaskColorFalseEnabledTrueCommand1    CommandButtonCancelChoose diff --git a/docs/IDE/Forms/Images/Properties_ColourPicker.png b/docs/IDE/Forms/Images/Properties_ColourPicker.png new file mode 100644 index 000000000..72654e276 Binary files /dev/null and b/docs/IDE/Forms/Images/Properties_ColourPicker.png differ diff --git a/docs/IDE/Forms/Images/Properties_ColourPicker.svg b/docs/IDE/Forms/Images/Properties_ColourPicker.svg new file mode 100644 index 000000000..7dd6af26c --- /dev/null +++ b/docs/IDE/Forms/Images/Properties_ColourPicker.svg @@ -0,0 +1 @@ +PROPERTIESDESIGNAPPEARANCEDRAWINGBEHAVIOURNameCommand1IndexCaption...Command1ToolTipText...VisualStylesTrueAppearance1 - vbAppear3dBackColor&H(BGR)8000000FForeColor&H(BGR)80000012Style0 - vbButtonStandardFontNameSegoe UIFontSize8FontBoldFalseFontItalicFalseFontStrikethruFalseFontUnderlineFalsePicture{0 bytes}...PictureDpiScalingFalseDownPicture{0 bytes}...DisabledPicture{0 bytes}...PictureAlignment1 - vbAlignTopMaskColor&H(BGR)C0C0C0UseMaskColorFalseEnabledTrueCommand1    CommandButtonCancelChoose diff --git a/docs/IDE/Forms/Images/Properties_Control.light.png b/docs/IDE/Forms/Images/Properties_Control.light.png new file mode 100644 index 000000000..d8b6071ff Binary files /dev/null and b/docs/IDE/Forms/Images/Properties_Control.light.png differ diff --git a/docs/IDE/Forms/Images/Properties_Control.light.svg b/docs/IDE/Forms/Images/Properties_Control.light.svg new file mode 100644 index 000000000..856806a21 --- /dev/null +++ b/docs/IDE/Forms/Images/Properties_Control.light.svg @@ -0,0 +1 @@ +ImportedTypeLibrariesMiscellaneousPackagesReferencesResourcesSourcesfrmMain.tbformfrmMain.twinMyReport.tbreportMyReport.twinStartup.twinPROPERTIESPROPERTIESEVENTSDESIGNAPPEARANCEDRAWINGNameCommand1IndexCaption...Command1ToolTipText...VisualStylesTrueAppearance1 - vbAppear3dBackColor&H(BGR)8000000FForeColor&H(BGR)80000012Style0 - vbButtonStandardFontNameSegoe UIFontSize8FontBoldFalseFontItalicFalseFontStrikethruFalseFontUnderlineFalsePicture{0 bytes}...PictureDpiScalingFalseDownPicture{0 bytes}...DisabledPicture{0 bytes}...PictureAlignment1 - vbAlignTopCommand1    CommandButtonTabsObject drop-downCategory diff --git a/docs/IDE/Forms/Images/Properties_Control.png b/docs/IDE/Forms/Images/Properties_Control.png new file mode 100644 index 000000000..2b9c66189 Binary files /dev/null and b/docs/IDE/Forms/Images/Properties_Control.png differ diff --git a/docs/IDE/Forms/Images/Properties_Control.svg b/docs/IDE/Forms/Images/Properties_Control.svg new file mode 100644 index 000000000..37325589b --- /dev/null +++ b/docs/IDE/Forms/Images/Properties_Control.svg @@ -0,0 +1 @@ +ImportedTypeLibrariesMiscellaneousPackagesReferencesResourcesSourcesfrmMain.tbformfrmMain.twinMyReport.tbreportMyReport.twinStartup.twinPROPERTIESPROPERTIESEVENTSDESIGNAPPEARANCEDRAWINGNameCommand1IndexCaption...Command1ToolTipText...VisualStylesTrueAppearance1 - vbAppear3dBackColor&H(BGR)8000000FForeColor&H(BGR)80000012Style0 - vbButtonStandardFontNameSegoe UIFontSize8FontBoldFalseFontItalicFalseFontStrikethruFalseFontUnderlineFalsePicture{0 bytes}...PictureDpiScalingFalseDownPicture{0 bytes}...DisabledPicture{0 bytes}...Command1    CommandButtonTabsObject drop-downCategory diff --git a/docs/IDE/Forms/Images/Properties_Events.light.png b/docs/IDE/Forms/Images/Properties_Events.light.png new file mode 100644 index 000000000..4d186b982 Binary files /dev/null and b/docs/IDE/Forms/Images/Properties_Events.light.png differ diff --git a/docs/IDE/Forms/Images/Properties_Events.light.svg b/docs/IDE/Forms/Images/Properties_Events.light.svg new file mode 100644 index 000000000..0b9c61e27 --- /dev/null +++ b/docs/IDE/Forms/Images/Properties_Events.light.svg @@ -0,0 +1 @@ +380 x 270+-PROPERTIESPROPERTIESEVENTSGENERALClick...Command1_Click() ...DragDrop...DragOver...GotFocus...Initialize...KeyDown...KeyPress...KeyUp...LostFocus...MouseDown...MouseMove...MouseUp...OLECompleteDrag...OLEDragDrop...OLEDragOver...OLEGiveFeedback...OLESetData...OLEStartDrag...Command1    CommandButton diff --git a/docs/IDE/Forms/Images/Properties_Events.png b/docs/IDE/Forms/Images/Properties_Events.png new file mode 100644 index 000000000..af7dca245 Binary files /dev/null and b/docs/IDE/Forms/Images/Properties_Events.png differ diff --git a/docs/IDE/Forms/Images/Properties_Events.svg b/docs/IDE/Forms/Images/Properties_Events.svg new file mode 100644 index 000000000..bbd0f4f49 --- /dev/null +++ b/docs/IDE/Forms/Images/Properties_Events.svg @@ -0,0 +1 @@ +380 x 270-PROPERTIESPROPERTIESEVENTSGENERALClick...Command1_Click() ...DragDrop...DragOver...GotFocus...Initialize...KeyDown...KeyPress...KeyUp...LostFocus...MouseDown...MouseMove...MouseUp...OLECompleteDrag...OLEDragDrop...OLEDragOver...OLEGiveFeedback...OLESetData...OLEStartDrag...Command1    CommandButton diff --git a/docs/IDE/Forms/Images/Properties_SystemColours.light.png b/docs/IDE/Forms/Images/Properties_SystemColours.light.png new file mode 100644 index 000000000..cc1c545aa Binary files /dev/null and b/docs/IDE/Forms/Images/Properties_SystemColours.light.png differ diff --git a/docs/IDE/Forms/Images/Properties_SystemColours.png b/docs/IDE/Forms/Images/Properties_SystemColours.png new file mode 100644 index 000000000..280c9c5c3 Binary files /dev/null and b/docs/IDE/Forms/Images/Properties_SystemColours.png differ diff --git a/docs/IDE/Forms/Images/Properties_SystemColours.svg b/docs/IDE/Forms/Images/Properties_SystemColours.svg new file mode 100644 index 000000000..cd7f057a0 --- /dev/null +++ b/docs/IDE/Forms/Images/Properties_SystemColours.svg @@ -0,0 +1 @@ +PROPERTIESDESIGNAPPEARANCEDRAWINGBEHAVIOURMOUSEWINDOWNameCommand1IndexCaption...Command1ToolTipText...VisualStylesTrueAppearance1 - vbAppear3dBackColor&H(BGR)8000000FForeColor&H(BGR)80000012Style0 - vbButtonStandardFontNameSegoe UIFontSize8FontBoldFalseFontItalicFalseFontStrikethruFalseFontUnderlineFalsePicture{0 bytes}...PictureDpiScalingFalseDownPicture{0 bytes}...DisabledPicture{0 bytes}...PictureAlignment1 - vbAlignTopMaskColor&H(BGR)C0C0C0UseMaskColorFalseEnabledTrueRightToLeftFalseCausesValidationTrueCancelFalseDefaultFalseMouseIcon{0 bytes}...MousePointer0 - vbDefaultDragMode0 - vbManualDragIcon{0 bytes}...OLEDropMode0 - vbOLEDropNoneCommand1    CommandButtonCopyPasteCopy '-2147483633' To...SYSTEM COLORSvbScrollBarsvbDesktopvbActiveTitleBarvbInactiveTitleBarvbMenuBarvbWindowBackgroundvbWindowFramevbMenuTextvbWindowTextvbActiveTitleBarTextvbActiveBordervbInactiveBordervbApplicationWorkspacevbHighlightvbHighlightTextvbButtonFacevbButtonShadowvbGrayTextvbButtonTextvbInactiveTitleBarTextvb3DHighlightvb3DDKShadowvb3DLightvbInfoTextvbInfoBackgroundvbHotTrackTextvbActiveTitleBarGradientvbInactiveTitleBarGradientvbMenuHighlightvbMenuBarFlat diff --git a/docs/IDE/Forms/Images/Toolbox_Form.light.png b/docs/IDE/Forms/Images/Toolbox_Form.light.png new file mode 100644 index 000000000..ada28347f Binary files /dev/null and b/docs/IDE/Forms/Images/Toolbox_Form.light.png differ diff --git a/docs/IDE/Forms/Images/Toolbox_Form.light.svg b/docs/IDE/Forms/Images/Toolbox_Form.light.svg new file mode 100644 index 000000000..6c5199ad3 --- /dev/null +++ b/docs/IDE/Forms/Images/Toolbox_Form.light.svg @@ -0,0 +1 @@ +TOOLBOXOptionButtonComboBoxListBoxHScrollBarVScrollBarTimerDriveListBoxDirListBoxFileListBoxShapeLineImageCheckMarkQRCodeDataOLEMultiFrameCOMMON CONTROLSDTPickerImageListListViewMonthViewProgressBarSliderTreeViewUpDownMore components... diff --git a/docs/IDE/Forms/Images/Toolbox_Form.png b/docs/IDE/Forms/Images/Toolbox_Form.png new file mode 100644 index 000000000..b16a9d19d Binary files /dev/null and b/docs/IDE/Forms/Images/Toolbox_Form.png differ diff --git a/docs/IDE/Forms/Images/Toolbox_Form.svg b/docs/IDE/Forms/Images/Toolbox_Form.svg new file mode 100644 index 000000000..3931e58b0 --- /dev/null +++ b/docs/IDE/Forms/Images/Toolbox_Form.svg @@ -0,0 +1 @@ +TOOLBOXOptionButtonComboBoxListBoxHScrollBarVScrollBarTimerDriveListBoxDirListBoxFileListBoxShapeLineImageCheckMarkQRCodeDataOLEMultiFrameCOMMON CONTROLSDTPickerImageListListViewMonthViewProgressBarSliderTreeViewUpDownMore components... diff --git a/docs/IDE/Forms/Images/Toolbox_Search.light.png b/docs/IDE/Forms/Images/Toolbox_Search.light.png new file mode 100644 index 000000000..80afec338 Binary files /dev/null and b/docs/IDE/Forms/Images/Toolbox_Search.light.png differ diff --git a/docs/IDE/Forms/Images/Toolbox_Search.light.svg b/docs/IDE/Forms/Images/Toolbox_Search.light.svg new file mode 100644 index 000000000..135da050f --- /dev/null +++ b/docs/IDE/Forms/Images/Toolbox_Search.light.svg @@ -0,0 +1 @@ +TOOLBOXMore components...Command, TextEDITOR 1frmMain.tbformFile+ menuShow Control Names diff --git a/docs/IDE/Forms/Images/Toolbox_Search.png b/docs/IDE/Forms/Images/Toolbox_Search.png new file mode 100644 index 000000000..c69c8a68d Binary files /dev/null and b/docs/IDE/Forms/Images/Toolbox_Search.png differ diff --git a/docs/IDE/Forms/Images/Toolbox_Search.svg b/docs/IDE/Forms/Images/Toolbox_Search.svg new file mode 100644 index 000000000..99f39225b --- /dev/null +++ b/docs/IDE/Forms/Images/Toolbox_Search.svg @@ -0,0 +1 @@ +TOOLBOXMore components...Command, TextEDITOR 1frmMain.tbformFile+ menuShow Control Names diff --git a/docs/IDE/Forms/Menu-Editor.md b/docs/IDE/Forms/Menu-Editor.md new file mode 100644 index 000000000..73e79f879 --- /dev/null +++ b/docs/IDE/Forms/Menu-Editor.md @@ -0,0 +1,75 @@ +--- +title: Menu editor +parent: Designing forms +grand_parent: IDE +nav_order: 4 +permalink: /tB/IDE/Forms/Menu-Editor +index: + - menu editor + - form menu + - Form Menu bar +--- + +# Menu editor + +The menu editor designs the menus of a form on the form itself: you add a menu and its items, type their captions, and move them with the mouse and the keys. The menu editor is not a window of its own. It is the **Form Menu** bar above the form in the designer. + +The bar is on a Windows form, an MDI form and a user control. A CustomControls form and a report do not have it. No menu item or key opens the menu editor. The menus are [**Menu**](../../Packages/VB/Menu/) controls, and they belong to the form. The [Toolbox](Toolbox) does not list **Menu**. + +![The Form Menu bar above a form: the menu File, then a text box holding &Edit for a new menu, then the + menu button.](Images/MenuEditor_Bar.png){:width="380" height="80"} + +## Add a menu + +1. Click **+ menu** in the bar. A text box opens in the bar, holding the name of the new control, such as `Menu1`, with all of it selected. +2. Type the caption. Enter commits it, and Esc restores the old text. Clicking elsewhere commits it. + +A new top-level menu goes to the end of the bar. + +The control's **Name** follows the caption while you type it. The name is the caption with every character removed except the letters A to Z in either case, the digits 0 to 9 and the characters above U+00FF. So `Edit Stuff` gives `EditStuff`, and `&File` gives `File`. The accented letters of Latin-1 are dropped, so `&Édition` gives `dition`. An empty result gives `Menu`, and a name that is taken gets the next free number. The name follows the caption only while it still equals the name that the old caption suggested. After you edit the name in PROPERTIES, it stops following. + +## Add and arrange items + +Click a menu in the bar to open its drop-down, which lists the items. The **+** row at the end of the list adds an item. Click an item to select it. If it has items of its own, its list opens to its right. + +- A caption of `-` makes a separator, drawn as a line. +- An `&` in a caption marks the accelerator letter. The editor shows the caption without the `&`, and `&&` shows a literal `&`. The underline is not drawn. +- The shortcut text of an item shows at the right of its row, taken from its **ShortcutId**. +- A tick shows an item whose **Checked** is True, and an arrow an item that has items of its own. A caption is grey with a light shadow when **Enabled** is False, and faded when **Visible** is False. + +![The File menu of the Form Menu bar open below it, listing Open, a separator line and Exit, with a + button at the end. The right-click menu of Open is open over it: Rename; Cut, Copy and the greyed Paste Before, Paste After and Paste Into; Move Up and Move Down; Toggle Checked, Toggle Enabled and Toggle Visible; Insert Above, Insert Below, Insert Seperator Above and Insert Seperator Below; and Delete, with the keys of five of them.](Images/MenuEditor_Open.png){:width="392" height="473"} + +### The right-click menu of an item + +Right-click a menu or an item to open its menu. The labels are in English whatever the IDE language. + +| Item | Key | Works on | +|---|---|---| +| **Rename** | F2 | A menu and an item. | +| **Cut**, **Copy** | | A menu and an item. | +| **Paste Before**, **Paste After**, **Paste Into** | | A menu and an item, live only while the clipboard holds one menu item. | +| **Move Left**, **Move Right** | Ctrl+Left, Ctrl+Right | A menu in the bar. | +| **Move Up**, **Move Down** | Ctrl+Up, Ctrl+Down | An item in a list. | +| **Toggle Checked** | Space | An item in a list. | +| **Toggle Enabled**, **Toggle Visible** | | A menu and an item. | +| **Insert Left**, **Insert Right** | | A menu in the bar. | +| **Insert Above**, **Insert Below** | | An item in a list. | +| **Insert Seperator Above**, **Insert Seperator Below** | | An item in a list. The menu spells them that way. | +| **Delete** | Delete | A menu and an item. | + +The keys work while a menu item is selected and the designer has the focus. **Rename** opens the text box in place, with the caption selected, and the **Name** follows what you type. **Cut** removes the item and keeps it on the clipboard. **Paste Into** puts the cut item under the selected menu or item. **Copy** puts one item on the system clipboard as JSON, as a control is copied. + +## Set other properties + +Everything else about a menu item is a property in [PROPERTIES](Properties): **Name**, **Index**, **Caption**, **Picture**, **Enabled**, **Visible**, **HelpContextID**, **Checked**, **IconSizeX**, **IconSizeY**, **NegotiatePosition**, **ShortcutId**, **Tag** and **WindowList**. **ShortcutId** is a drop-down of 76 values, from `0 - vbShortcutNone` and `1 - vbShortcutCtrlA` on. The menus and the items are in the object drop-down of PROPERTIES as `File Menu`. A rubber band does not select them. + +A double-click on a menu item opens its `Click` handler, and inserts it if it is missing. A double-click on **File**, for example, inserts `Private Sub File_Click()`. + +## Menus in a preview + +A menu is part of the form. The window of a [preview](Previewing) shows the menus that you designed. + +## See Also + +- [Designer](Designer) -- the Form Menu bar in the designer window +- [Menu](../../Packages/VB/Menu/) -- the class that a menu item is +- [Properties](Properties) -- the properties of a menu item diff --git a/docs/IDE/Forms/Previewing.md b/docs/IDE/Forms/Previewing.md new file mode 100644 index 000000000..4bf29b22a --- /dev/null +++ b/docs/IDE/Forms/Previewing.md @@ -0,0 +1,58 @@ +--- +title: Previewing a form +parent: Designing forms +grand_parent: IDE +nav_order: 7 +permalink: /tB/IDE/Forms/Previewing +index: + - preview, form + - RESYNC + - F6 +--- + +# Previewing a form + +Preview runs one form on its own, so that you can try it without starting the whole project. This page describes Preview and the **RESYNC** button, which refreshes a designer from the compiler. + +## Preview + +Click **PREVIEW** in the toolbar, titled *Launch the form in isolation to test its functionality*, or press F6 while the designer is active. The form opens in a window of its own. + +- Preview shows the design as it is on the screen, whether you saved it or not. +- A menu that you designed in the [menu editor](Menu-Editor) shows in the window. +- Preview works for a form and for a report. For a user control or a property page, the button is greyed out and F6 does nothing. +- F6 in the form's code also previews the form. + +![The previewed form as a window of its own, titled frmMain, with its File menu in the menu bar and the controls laid out as in the designer.](Images/Preview_Window.png){:width="381" height="320"} + + + + +A preview is a debug session. `Form_Load` runs, a breakpoint in it stops the run, a click on a button runs its `Click` handler, and closing the window runs `Form_Unload` and `Form_Terminate` and writes `[DEBUGGER] done.` to the **DEBUG CONSOLE**. [Run Or Preview](../Debugging/Running#run-or-preview) describes what F6 does in each place, and [Breakpoints and stepping](../Debugging/Breakpoints-and-Stepping) what you can do while it is stopped. + +The preview of a report opens the report window, titled with the project's name. [Designing reports](../Reports/) describes it. + +Preview is the only way to run one form. **Start** (F5) and **Run → Start** do not use the open designer. They run the program from its startup object, which [Project Settings](../Project-Settings#startup-object) names. + +## RESYNC + +The designer keeps its own picture of the form, and gets the controls' pictures from the compiler. A **RESYNC** button covers the designer when that picture may be out of date. Pressing it reads the whole form again from the compiler, and keeps the selection. + +The button appears in these cases: + +- The connection to the compiler closes or fails, for example when the compiler restarts. +- The display scaling of Windows changes. The IDE checks it every 5 seconds. +- The compiler sends a new [Toolbox](Toolbox) list while the designer's tab is hidden. +- A build starts, and the form contains a user control that belongs to the project. + +In the last case the IDE presses the button itself at once. It also presses the button when you return to a designer tab that was waiting for a refresh. So you rarely see the button: the form you come back to is already up to date. That includes a form that uses a custom control whose class you changed in its code while the form's tab was open behind it. See [Property sheet and object serialization](../../../Tutorials/CustomControls/Properties). + +In Safe Mode, the button says *Can't resync the form designer in Safe Mode*, with the title *Form Designer Resync Failed*. + +Changing the grid or the outlines of the form makes the designer read the live form from the compiler again. + +## See Also + +- [Run Or Preview](../Debugging/Running#run-or-preview) -- what F6 does in a procedure, in a form's code and in a designer +- [Designer](Designer) -- the designer window +- [Menu editor](Menu-Editor) -- the menus that show in a preview diff --git a/docs/IDE/Forms/Properties.md b/docs/IDE/Forms/Properties.md new file mode 100644 index 000000000..0f4e0eb59 --- /dev/null +++ b/docs/IDE/Forms/Properties.md @@ -0,0 +1,137 @@ +--- +title: Properties +parent: Designing forms +grand_parent: IDE +nav_order: 6 +permalink: /tB/IDE/Forms/Properties +redirect_from: + - /tB/IDE/Project/Properties +index: + - PROPERTIES pane + - property editor + - colour picker + - EVENTS tab + - twips +--- + +# Properties + +The **PROPERTIES** pane shows the properties of the form or the control that is selected in the designer, and lets you change them. It has a second tab, **EVENTS**, that opens the event handlers of a control in the code. This page describes the pane, the editors it uses for each kind of property, and the units of positions and sizes. + +![The Properties pane, empty apart from its title bar, with no property rows listed.](../Images/Properties.png){:width="400" height="376"} + +Each open designer has a property sheet of its own, and the pane shows the one of the active designer. The pane is empty, as in the picture, while a code tab is active or no project is open. With a designer active and nothing selected, the pane shows the form's own properties. + +![The PROPERTIES pane with Command1 selected. Under its title bar are the PROPERTIES and EVENTS tabs, then the object drop-down showing Command1 and CommandButton, then the list of properties under the category headers DESIGN, APPEARANCE and DRAWING. Labels with arrows on the left name the tabs, the drop-down and a category header.](Images/Properties_Control.png){:width="573" height="520"} + +From the top, the pane holds the **PROPERTIES** and **EVENTS** tabs, the object drop-down and the list of properties. The magnifier in the title bar searches the list. + +## Choose what to edit + +The object drop-down lists the form, every control, and in a report every section. A control shows as its **Name**, or `Name(Index)` in a control array, then its class. After the first two entries the list is sorted by text. Choosing an entry selects it in the designer. The list reaches a control that is hidden, outside the visible part of the form or covered by another control. + +Several selected controls show **[MULTI-SELECT]** in the drop-down. The list still holds every control and the form. + +### Several controls at once + +With several controls selected, the list merges their properties. A property that one of the controls lacks is still listed. A number whose value differs shows 0, and a **Boolean** whose value differs shows an in-between state. Typing a value sets it on every selected control. For example, selecting a **CommandButton** and a **TextBox** and typing a **Width** sets both. **Caption** is listed, because the button has it, and so is **Text**. The **EVENTS** tab is empty. + +## Find a property + +The list is grouped by category, not alphabetically, and there is no alphabetical view. The groups are **DESIGN**, **BINDINGS**, **DATA**, **APPEARANCE**, **DRAWING**, **BEHAVIOUR**, **MOUSE**, **WINDOW**, **HELP**, **KEYBOARD**, **SCALE**, **LAYOUT**, **GENERAL** and **DDE**. A property the IDE does not know goes into **GENERAL**. Within a group the order is not strictly alphabetical: **Name** comes first under **DESIGN**, and **Caption** or **Text** comes before **ToolTipText**. The **Index** property is not listed for a form, a user control, a property page or a report. + +The magnifier opens a search box. The text is a list of parts of property names, separated by commas, and a property matches when its name contains any of them, in either case. The search looks at names, not at values. + +A property that is not implemented yet shows an information icon, and its tooltip says *THIS PROPERTY IS NOT YET IMPLEMENTED. THE VALUE WILL BE IGNORED.* + +When the compiler cannot draw a control, a banner with a warning sign and the error text shows at the top of the list. If the CustomControls package is missing, the **DEBUG CONSOLE** says *twinBASIC error: it looks like you haven't added the CustomControls to this project.* + +## The editors + +A property has the editor that suits its type. A value is committed as the table says. + +| Type of property | Editor | Committed | +|---|---|---| +| Text | A text box. Double-click the name to put the caret in the box. | When you press Enter or leave the box. | +| **Boolean** | A check box and the word `True` or `False`. Double-click the name or the value to switch it. | At once. | +| Numbers | A number box. An **Index** of -1 shows empty. | When you press Enter or leave the box. | +| Date | A text box that shows the date as text. The text you type is sent as you typed it. | When you press Enter or leave the box. | +| Enumeration | A drop-down. Double-click the name to step to the next value, going back to the first after the last. | At once. | +| Font name | A drop-down of the installed fonts. | At once. | +| Colour | A swatch and a text box. See [Colours](#colours). | When the picker closes, or when you leave the text box. | +| **Picture**, **Icon**, **MouseIcon** | The text `{N bytes}` and a **...** button that opens a file dialog. A red **X** clears a picture. | After you choose a file. | +| Text with several lines | The first line and a **...** button. It opens **Edit Value (multi-line)** with **OK** and **Cancel**. | When you choose **OK**. | +| **ImageList** | A drop-down of the **ImageList** controls on the form, and `(none)`. | At once. | +| **PropertyPages** | `{ N pages }` and a **...** button. It opens **Select Property Pages**, a list with check boxes and buttons that move a page up and down. | When you choose **OK**. | +| **(Custom)** | `{ PropertyPage }`. A click, or the **...** button, opens the control's own property page. | | +| **AboutBox** | `{ AboutBox }`. A click opens the box. | | +| Gradient fill | A gradient editor. | | +| Object or array | A row with a triangle that expands. The IDE remembers which rows are expanded for each form. An array has a **...** button for a box with one element on each line, a red minus on each element and a `(+)` row that adds an element. | | + +If the compiler is busy when you open a property page, the IDE says *Backend compiler seems to be busy. Check there isn't a modal dialog box open behind the IDE window.* + +> [!NOTE] +> In twinBASIC BETA 1005, Ctrl+A does nothing in a property box. It does not select the text, and the selection of the controls on the form is unchanged. Text that you type is added to the text already in the box. + +### Colours + +A colour property has a swatch. A click on the swatch, or a double-click on the property's name, opens a colour picker with buttons that confirm and cancel. The picker has no alpha channel. The IDE keeps the last seven colours you chose and offers them in the picker. + +The text box beside the swatch holds the colour as `&H` and hexadecimal digits followed by `(BGR)`: blue, green and red, as Windows colours are. A system colour shows in the form `&H8000000F`. A value of -1 shows as `(not set)` for a property that allows it. The colour properties of the CustomControls controls hold alpha too, and their text reads `(ABGR)`. + +![The PROPERTIES pane of Command1 with the colour picker open below its BackColor row, whose swatch and text box read &H8000000F (BGR). The picker has a grid of colour swatches with a row of recent colours under it, a colour field with a hue bar, and Cancel and Choose buttons.](Images/Properties_ColourPicker.png){:width="589" height="520"} + +### The right-click menu of a value + +Right-click a value to open a menu with: + +- **Copy**, which puts the value on the clipboard as JSON, and **Paste**, which is live when the clipboard holds a value of a compatible type. +- **Copy** *value* **To...**, a submenu that lists every control on the form with a property of the same name and type, as `Control.Property`, or `{ no matches }`. Choosing an entry copies the value to that control. +- **SYSTEM COLORS**, for a colour property: a submenu of the thirty system colour constants, from **vbScrollBars** to **vbMenuBarFlat**. + +There is no item that resets a property to its default, and nothing marks a property that still has its default. + +![The PROPERTIES pane of Command1 with the right-click menu of the BackColor value open: Copy, a greyed Paste, Copy '-2147483633' To... and SYSTEM COLORS, whose submenu lists the system colour constants from vbScrollBars to vbMenuBarFlat.](Images/Properties_SystemColours.png){:width="693" height="757"} + +### What an edit also changes + +Some properties change others when you set them: + +- Setting **ScaleLeft**, **ScaleTop**, **ScaleWidth** or **ScaleHeight** sets **ScaleMode** to `vbUser`. +- Setting **WhatsThisButton** to True turns **MinButton** and **MaxButton** off and **ControlBox** on. +- Setting **MDIChild** to True sets **StartUpPosition** to the Windows default. +- Setting an **OptionButton**'s **Value** to True clears the others in its container. +- A **Name** that is taken asks whether to make a [control array](Controls#control-arrays). +- A **Windowless** user control cannot also be a container. The IDE says *ERROR: A Windowless control cannot also be a container*. +- A **FramesCount** smaller than the number of frames present is refused. + +## Descriptions + +Pointing at a property's value shows a short description of the property as a tooltip. The compiler supplies the text for some properties only. For example, the description of **Left** reads *Adjusts the left position of this control, in pixels, relative to the container*. **Top**, **Width**, **Height**, **Enabled**, **ToolTipText** and **TabIndex** have one too. **Caption**, **Style**, **BackColor** and **Index** have none. + +## Events + +The **EVENTS** tab lists the events of a control that you can handle. It works with exactly one control selected. With two controls or the form selected, the tab is empty. + +Each row is an event. The value reads `Command1_Click() ...` when the code has a handler for it, and is empty when it has none. Each row has a **...** button. Click the value, click the **...**, or double-click the name, and the code editor opens the handler. If the handler does not exist, the editor inserts an empty one with the caret in its body, and the tab for the `.twin` file is marked changed. For a member of a control array, the handler takes an `Index As Integer` argument. The same handlers open when you [double-click a control](Controls#open-an-event-handler). + +![The EVENTS tab of PROPERTIES for Command1, listing its events from Click to OLEStartDrag, each with a ... button. The Click row's value reads Command1_Click() ..., and the other rows' values are empty.](Images/Properties_Events.png){:width="360" height="520"} + +## Units of position and size + +The designer file stores **Left**, **Top**, **Width** and **Height** in pixels at 96 DPI, which the display's scaling does not change. The surface and its readouts are in pixels too. The pane shows these four properties, and **MinWidth**, **MaxWidth**, **X1**, **Y1**, **X2**, **Y2** and the margins and section heights of a report, in the unit that the **ScaleMode** of the control's parent names. A Windows form has `vbTwips` by default, and the pane shows a value 15 times the pixel value. The other units are points, characters, inches, millimetres, centimetres, HiMetric and the form's own units for `vbUser`. + +For example, a button at **Left** 10 and **Top** 50 pixels, 100 pixels wide and 30 high, shows 150, 750, 1500 and 450. A form's own **Width** and **Height** are always in twips, so a form 300 pixels wide shows 4500. The pane writes no unit next to the number, and on a form in twips the tooltip of the four position properties says *in pixels*. + +A value you type is taken in the same unit. Typing **Left** 450 on that form moves the button to 30 pixels. A font size, **Opacity**, **DrawWidth** and other properties that are not positions show as they are. + +> [!NOTE] +> In twinBASIC BETA 1005, the pane shows **Left**, **Top**, **Width** and **Height** of a control on a CustomControls form multiplied by 15, as if the form were in twips, but takes the value you type as pixels. A button at **Left** 20 pixels shows 300. Typing 40 moves it to 40 pixels, and the box then shows 600. Do not type the value the pane shows. Type pixels, as the readouts on the surface show them. + +## See Also + +- [Working with controls](Controls) -- selecting and moving controls +- [Toolbox](Toolbox) -- the controls you can draw +- [Designer](Designer) -- the form and its grid +- [Controls](../../Controls) -- the properties of each control +- [Anchoring and docking](../../../Features/GUI-Components/Anchoring-Docking) -- the **Anchors** and **Dock** properties diff --git a/docs/IDE/Forms/Toolbox.md b/docs/IDE/Forms/Toolbox.md new file mode 100644 index 000000000..590096ed4 --- /dev/null +++ b/docs/IDE/Forms/Toolbox.md @@ -0,0 +1,81 @@ +--- +title: Toolbox +parent: Designing forms +grand_parent: IDE +nav_order: 5 +permalink: /tB/IDE/Forms/Toolbox +redirect_from: + - /tB/IDE/Project/Toolbox +index: + - Toolbox + - ActiveX controls, adding + - More components +--- + +# Toolbox + +The **TOOLBOX** pane lists the controls that you can draw on the active designer. You click a tool, and then click or drag on the form to draw the control. This page describes what the list holds, how to search it, and how to add ActiveX controls to it. + +## What the list holds + +The compiler supplies the list, and the IDE replaces it in every open designer when the project's references change. Each open designer has a Toolbox of its own, and the pane shows the one of the active designer. It is empty while a code tab is active. + +A designer lists the controls that suit its kind of form: + +- A **CustomControls form** lists the controls of the CustomControls package, such as **WaynesButton** and **WaynesGrid**. See [CustomControls](../../Packages/CustomControls/). +- Every other designer, that is a Windows form, an MDI form, a user control and a property page, lists the Windows controls. +- A report lists only seven tools. See [Designing reports](../Reports/). + +The forms themselves are never tools, and neither is **Menu**. A menu is made in the [menu editor](Menu-Editor). + +The first group has no title bar. It starts with **Pointer**, the tool for selecting, and then lists the standard controls in a fixed order: **PictureBox**, **Label**, **TextBox**, **Frame**, **CommandButton**, **CheckBox**, **OptionButton**, **ComboBox**, **ListBox**, **HScrollBar**, **VScrollBar**, **Timer**, **DriveListBox**, **DirListBox**, **FileListBox**, **Shape**, **Line**, **Image**, **CheckMark**, **QRCode**, **Data** and **OLE**. Any further controls of the package follow in alphabetical order, such as **MultiFrame**. Every other package that supplies controls gets a group of its own with a title bar, which names the package in capitals. The package **WinNativeCommonCtls** shows as **COMMON CONTROLS**. [Controls](../../Controls) describes the controls. + +Each tool is an icon. A tool whose control runs in a separate process shows a small label, **32-bit** or **64-bit**, and a tooltip says that the control is hosted by a separate 32-bit EXE process at run time. + +By default the Toolbox shows icons only. The **...** button in the title bar opens a menu with one item, **Show Control Names**, which is ticked while the names are shown. Choosing it saves the IDE option **IDE: Show toolbox icons only** at once. See [IDE Options](../Customising/IDE-Options#form-designer-and-toolbox). + +![The docked Toolbox of a Windows form with control names shown, scrolled to its end. The first group, without a title bar, ends with ListBox to MultiFrame; under it the COMMON CONTROLS group lists DTPicker, ImageList, ListView, MonthView, ProgressBar, Slider, TreeView and UpDown, and More components ends the list.](Images/Toolbox_Form.png){:width="216" height="791"} + +## Use a tool + +- Click a tool to select it. The pointer over the designer becomes a cross. Then [draw the control](Controls#draw-a-control). +- Double-click a tool to place the control at once, centred from left to right on the form. +- With the Toolbox focused, Left and Up select the previous tool, and Right and Down the next. + +The tool returns to **Pointer** after each control. + +## Search + +The magnifier in the title bar opens a search box under it. Typing a printable character with a tool focused opens it too, holding that character. The list narrows as you type. The text is a list of parts of names, separated by commas, and a tool matches when its name contains any of them, in either case. `Command, Text` leaves **Pointer**, **TextBox**, **CommandButton** and **More components**. Groups with no match are hidden. The first group stays. + +Esc, the close button of the box, or an empty box ends the search and shows the whole list. + +![The Toolbox with its search box holding Command, Text, beside three tool icons, the pointer, a text box and a command button, and More components below them. The menu of the ... button is open at the right, holding Show Control Names.](Images/Toolbox_Search.png){:width="377" height="129"} + +> [!NOTE] +> In twinBASIC BETA 1005, a **floating** Toolbox lists every tool when a report is open, 24 controls and **More components**, where the docked Toolbox lists seven. A tool that a report does not allow is refused when you use it, so nothing is lost. Dock the Toolbox, or choose only the seven tools that a report allows. + +## Add ActiveX controls and components + +The Toolbox has no list of components to pick from and no way to remove a control. ActiveX controls reach it as **COM references** of the project: + +1. Choose **More components...** at the end of the Toolbox. It is not shown for a report. +2. In the box, choose **GoTo COM References**. Project Settings opens on the **Available COM References** tab, filtered to the `project.references` setting. +3. Add the reference to the type library that holds the control, as [Project Settings](../Project-Settings#library-references) describes. + +A selected ActiveX control has an **ActiveX Properties...** item in its [right-click menu](Controls#the-right-click-menu). + +![The Toolbox button labelled More components, a grey plus sign beside the text inside a dashed outline.](../Images/Toolbox_MoreComponents.png){:width="208" height="44"} + +The box says *Components list coming soon!* and that, for now, you must add ordinary COM references to the appropriate ActiveX type library, after which the components appear in the form designer automatically. It has two buttons, **GoTo COM References** and **OK**, which closes it. + +![A twinBASIC information box reading Components list coming soon, explaining that for now ordinary COM references to the appropriate ActiveX type library must be added and the components will then appear in the form designer automatically, above a GoTo COM References button and an OK button.](../Images/Components_Message.png){:width="654" height="169"} + +[Project Settings](../Project-Settings#library-references) describes the references and the **Available COM References** tab. Package controls such as **WebView2**, **CefBrowser** and the Common Controls appear in groups named for their package, when the project refers to the package and the form is a Windows form. [Packages](../../../Features/Packages/) describes how to add a package. + +## See Also + +- [Working with controls](Controls) -- drawing and arranging what you chose +- [Properties](Properties) -- the properties of a control +- [Controls](../../Controls) -- every control, one page each +- [Project Settings](../Project-Settings) -- the references diff --git a/docs/IDE/Forms/index.md b/docs/IDE/Forms/index.md new file mode 100644 index 000000000..2a891e57b --- /dev/null +++ b/docs/IDE/Forms/index.md @@ -0,0 +1,64 @@ +--- +title: Designing forms +parent: IDE +nav_order: 5 +permalink: /tB/IDE/Forms/ +has_toc: false +redirect_from: + - /tB/IDE/Project/Editor/Form +index: + - form designer + - designer + - tbform + - tbcontrol + - tbppage +--- + +# Designing forms + +A designer is the window where you build a form by drawing controls on it, and where you set the properties of the form and of each control. The same designer builds user controls and property pages. + +- [**Designer**](Designer) --- adding a form, opening it, switching between design and code, the parts of the window, the grid, zoom, locks and the tab order. +- [**Working with controls**](Controls) --- drawing, selecting, moving and sizing controls, the clipboard, containers, control arrays, in-place editing, the right-click menu and the keys. +- [**Arranging controls**](Arranging) --- the **Format** menu: aligning, making the same size, spacing, centring and ordering a selection. +- [**Menu editor**](Menu-Editor) --- designing the menus of a form. +- [**Toolbox**](Toolbox) --- the list of controls you can draw, its search, and how to add ActiveX controls. +- [**Properties**](Properties) --- the **PROPERTIES** pane: its editors, the colour picker, the **EVENTS** tab and the units of positions. +- [**Previewing a form**](Previewing) --- running one form on its own, and the **RESYNC** button. + +Reports have their own page: [Designing reports](../Reports/). The commands of the **Format** menu are listed in the [Format menu](../Reference/Menu/Format) reference page. The controls themselves are listed in [Controls](../../Controls). The language side of forms is in [Forms](../../../Features/GUI-Components/Forms) and [Anchoring and docking](../../../Features/GUI-Components/Anchoring-Docking). + +## What a designer opens + +One designer opens four kinds of file. Each kind has a partner `.twin` file that holds its code. + +| File | What it designs | Added with | +|---|---|---| +| `.tbform` | A Windows form, a Windows MDI form, a CustomControls form, or a form of a kind that a package supplies. | **Add Windows Form**, **Add Windows MDI Form**, **Add CustomControls Form** or **Add** *name* **Form** | +| `.tbcontrol` | A user control. | **Add Windows UserControl** | +| `.tbppage` | A property page. | **Add Windows PropertyPage** | +| `.tbreport` | A report. See [Designing reports](../Reports/). | **Add Windows Report** | + +The kinds differ in a few ways: + +- The **Form Menu** bar, where the [menu editor](Menu-Editor) works, is on a Windows form, an MDI form and a user control. A CustomControls form and a report do not have it. +- The [Toolbox](Toolbox) lists the CustomControls for a CustomControls form, and the Windows controls for every other designer except a report, which lists seven. +- [Preview](Previewing) works for a form and a report. The button is greyed out in the designer of a user control, and F6 does nothing there. +- A CustomControls form has no control arrays. + +## How a designer works + +The IDE does not draw the controls. It asks the compiler for a picture of each one and lays the pictures on the form. What you see is the way the compiler draws the control, and a control the compiler cannot draw appears as a grey box. + +The designer sends every real change to the compiler, and writes the form to the `.tbform` file as JSON text when you save. Each form file and its `.twin` file have the same `FormDesignerId` attribute, which ties them together. + +Each open designer has a [Toolbox](Toolbox) and a [PROPERTIES](Properties) pane of its own. The two panes show those of the active designer, and they are empty while a code tab is active. + +Two kinds of setting differ in where they live. The grid, the outlines and the locks belong to the form, are saved in its file, and can differ from one form to the next. The zoom, the tab that **PROPERTIES** shows, its search boxes and the undo history belong to the session. The IDE forgets them when you close the designer's tab. + +## See Also + +- [Writing code](../Code/) -- the code editor, for the `.twin` half of a form +- [Project Explorer](../Project/Explorer) -- the **Add** menu that creates forms +- [Running and debugging](../Debugging/) -- running a program that shows forms +- [Customising the IDE](../Customising/IDE-Options#form-designer-and-toolbox) -- the options of the designer and the Toolbox diff --git a/docs/IDE/Menu/Format.md b/docs/IDE/Menu/Format.md index 4fa767328..32e41d16d 100644 --- a/docs/IDE/Menu/Format.md +++ b/docs/IDE/Menu/Format.md @@ -10,16 +10,55 @@ redirect_from: # Format Menu -![The Format menu open with every one of its commands greyed out: Align and Make Same Size, Horizontal Spacing and Vertical Spacing, the two Center In Container commands, Bring To Front and Send To Back, and Lock Controls. The first four carry submenu arrows.](Images/Menu_Format.png){:width="232" height="281"} +The Format menu holds the commands that line up and order the controls selected in the form designer. [Arranging controls](../../Forms/Arranging) explains them. -Every command on this menu acts on the controls selected in the form designer, so the whole menu is unavailable until a designer is open with a selection. The screenshot above shows that state; the one below shows the same menu with the commands available. +![The Format menu with two controls selected in the form designer: every command is available.](Images/Menu_Format_1.png){:width="232" height="281"} -![The same Format menu with two controls selected in the form designer: all of its commands are available rather than greyed out.](Images/Menu_Format_1.png){:width="232" height="281"} +Every command acts on the selection in a form designer, so every command is greyed out until a designer is open with a selection. The commands need different numbers of controls: **Align**, **Make Same Size** and the two spacing submenus need two or more, **Make Equal** three or more, and the other commands one or more. The same commands are in the designer's [right-click menu](../../Forms/Controls#the-right-click-menu). -![The Format menu with Align highlighted and its submenu open to the right. Left, Center (Horizontal) and Right sit above Top, Center (Vertical) and Bottom, with To Grid on its own below; the four edge commands carry ALT plus arrow-key shortcuts. All are available.](Images/Menu_Format_Align.png){:width="436" height="281"} +> [!WARNING] +> In twinBASIC BETA 1005, the spacing commands and **Make Same Size** can leave the new positions or sizes out of the saved form. [Arranging controls](../../Forms/Arranging) says what to do instead. -![The Format menu with Make Same Size highlighted and its submenu open to the right, offering Width (Widest), Width (Narrowest), Height (Tallest) and Height (Shortest), each with a CTRL plus SHIFT plus arrow-key shortcut.](Images/Menu_Format_MakeSameSize.png){:width="523" height="281"} +![The Format menu with no control selected, every command greyed out.](Images/Menu_Format.png){:width="232" height="281"} -![The Format menu with Horizontal Spacing highlighted and its submenu open to the right. Make Equal is greyed out; Increase, Decrease and Remove are available.](Images/Menu_Format_HorizontalSpacing.png){:width="345" height="281"} +## Align -![The Format menu with Vertical Spacing highlighted and its submenu open to the right. Make Equal is greyed out; Increase, Decrease and Remove are available.](Images/Menu_Format_VerticalSpacing.png){:width="345" height="281"} +**Left** (Alt+Left), **Right** (Alt+Right), **Top** (Alt+Up) and **Bottom** (Alt+Down) +: Move every selected control to the leftmost, rightmost, topmost or lowest edge of the selection. See [Align](../../Forms/Arranging#align). + +**Center (Horizontal)** and **Center (Vertical)** +: Centre each control on the left edge, or the top edge, of the selection. They have no keys. See [Align](../../Forms/Arranging#align). + +**To Grid** +: Rounds the position and the size of each selected control down to the grid, whether or not **Align Controls To Grid** is on. It has no key. The right-click menu has **Align to grid**, which works for one control. + +## Make Same Size + +**Width (Widest)** (Ctrl+Shift+Right), **Width (Narrowest)** (Ctrl+Shift+Left), **Height (Tallest)** (Ctrl+Shift+Down) and **Height (Shortest)** (Ctrl+Shift+Up) +: Give every selected control the width or the height of the widest, narrowest, tallest or shortest one. See [Make the controls the same size](../../Forms/Arranging#make-the-controls-the-same-size). + +## Horizontal Spacing and Vertical Spacing + +**Make Equal** +: Places the controls between the first and the last so that the gaps are equal. It needs three controls. + +**Increase** and **Decrease** +: Widen or narrow every gap by one grid width. The vertical commands use the grid height. + +**Remove** +: Places each control against the one before it, with no gap. + +The eight spacing commands, four on each submenu, have no keys. See [Space the controls evenly](../../Forms/Arranging#space-the-controls-evenly). + +## Center In Container + +**Center In Container (Horizontally)** and **Center In Container (Vertically)** +: Move the selection as a group to the middle of the container the controls share. They have no keys. See [Center in the container](../../Forms/Arranging#center-in-the-container). + +## Order and lock + +**Bring To Front** and **Send To Back** +: Move the selected controls to the top or the bottom of the order in which their parent draws them. They have no keys. See [Bring to front and send to back](../../Forms/Arranging#bring-to-front-and-send-to-back). + +**Lock Controls** +: Locks each selected control that is unlocked, and unlocks each one that is locked. It has no key. See [Lock controls](../../Forms/Designer#lock-controls). diff --git a/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.light.png b/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.light.png index ab9eadaf8..ee80a0d67 100644 Binary files a/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.light.png and b/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.light.png differ diff --git a/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.light.svg b/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.light.svg index 2b6728010..4364e9dac 100644 --- a/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.light.svg +++ b/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.light.svg @@ -1 +1 @@ -FormatAlignMake Same SizeHorizontal SpacingVertical SpacingCenter In Container (Horizontally)Center In Container (Vertically)Bring To FrontSend To BackLock ControlsMake EqualIncreaseDecreaseRemove +FormatAlignMake Same SizeHorizontal SpacingVertical SpacingCenter In Container (Horizontally)Center In Container (Vertically)Bring To FrontSend To BackLock ControlsMake EqualIncreaseDecreaseRemove diff --git a/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.png b/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.png index 9b67b3e43..8a41c84ac 100644 Binary files a/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.png and b/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.png differ diff --git a/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.svg b/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.svg index 4660eeb64..6c20b3573 100644 --- a/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.svg +++ b/docs/IDE/Menu/Images/Menu_Format_HorizontalSpacing.svg @@ -1 +1 @@ -FormatAlignMake Same SizeHorizontal SpacingVertical SpacingCenter In Container (Horizontally)Center In Container (Vertically)Bring To FrontSend To BackLock ControlsMake EqualIncreaseDecreaseRemove +FormatAlignMake Same SizeHorizontal SpacingVertical SpacingCenter In Container (Horizontally)Center In Container (Vertically)Bring To FrontSend To BackLock ControlsMake EqualIncreaseDecreaseRemove diff --git a/docs/IDE/Menu/View.md b/docs/IDE/Menu/View.md index ef803de41..2c8eb9fad 100644 --- a/docs/IDE/Menu/View.md +++ b/docs/IDE/Menu/View.md @@ -70,7 +70,7 @@ The last seventeen items show a panel. A panel that is in no layout in use comes : The row of buttons under the menu. See [Toolbar](../Toolbar). **TOOLBOX** and **PROPERTIES** -: The controls you can place on a form, and the properties of the selected one. See [Toolbox](../../Project/Toolbox) and [Properties](../../Project/Properties). +: The controls you can place on a form, and the properties of the selected one. See [Toolbox](../../Forms/Toolbox) and [Properties](../../Forms/Properties). **WEBPAGE** : A web browser pane inside the IDE. See [Webpage](../../Project/Webpage). diff --git a/docs/IDE/Project Explorer.md b/docs/IDE/Project Explorer.md index 89b5040e4..685e9caf6 100644 --- a/docs/IDE/Project Explorer.md +++ b/docs/IDE/Project Explorer.md @@ -78,7 +78,7 @@ Same as Right-Click ## ![](Images/tB-Green.png){:width="16" height="16"} Windows Form {: #windows-form } -[tbForm](Editor/Form) +The form designer opens on the new file. See [Add a form](../Forms/Designer#add-a-form). ## ![](Images/tB-Green.png){:width="16" height="16"} Windows MDI Form {: #windows-mdi-form } @@ -92,7 +92,7 @@ Same as Right-Click ## ![](Images/tB-Green.png){:width="16" height="16"} Windows Report {: #windows-report } -[tbReport](Editor/Report) +The report designer opens on the new file. See [Designing reports](../Reports/). ## ![](Images/tB-Green.png){:width="16" height="16"} CustomControls Forms {: #customcontrols-forms } diff --git a/docs/IDE/Properties.md b/docs/IDE/Properties.md deleted file mode 100644 index 1c03debe9..000000000 --- a/docs/IDE/Properties.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -title: Properties -parent: IDE -# nav_order: 2 -permalink: /tB/IDE/Project/Properties ---- - -# Properties - -![The Properties pane, empty apart from its title bar, with no property rows listed.](Images/Properties.png){:width="400" height="376"} - -The Properties pane shows the properties of the control or object currently selected in the form designer. Property names appear in the left column with their current values on the right; clicking a value edits it in place. - -The pane lists nothing when no project is open or when nothing is selected, which is the state shown above. diff --git a/docs/IDE/Reports/Images/Report_Controls.light.png b/docs/IDE/Reports/Images/Report_Controls.light.png new file mode 100644 index 000000000..bc1675810 Binary files /dev/null and b/docs/IDE/Reports/Images/Report_Controls.light.png differ diff --git a/docs/IDE/Reports/Images/Report_Controls.light.svg b/docs/IDE/Reports/Images/Report_Controls.light.svg new file mode 100644 index 000000000..d8f659446 --- /dev/null +++ b/docs/IDE/Reports/Images/Report_Controls.light.svg @@ -0,0 +1 @@ +EDITOR 1MyReport.tbreport1211🡶 Report Header🡶 Page Header🡶 Report Detail🡶 Page Footer🡶 Report FooterPAGE WIDTH123456789101112131415161718192021222324 diff --git a/docs/IDE/Reports/Images/Report_Controls.png b/docs/IDE/Reports/Images/Report_Controls.png new file mode 100644 index 000000000..f7f39470a Binary files /dev/null and b/docs/IDE/Reports/Images/Report_Controls.png differ diff --git a/docs/IDE/Reports/Images/Report_Controls.svg b/docs/IDE/Reports/Images/Report_Controls.svg new file mode 100644 index 000000000..88c99d793 --- /dev/null +++ b/docs/IDE/Reports/Images/Report_Controls.svg @@ -0,0 +1 @@ +EDITOR 1MyReport.tbreport1211🡶 Report Header🡶 Page Header🡶 Report Detail🡶 Page Footer🡶 Report FooterPAGE WIDTH123456789101112131415161718192021222324 diff --git a/docs/IDE/Reports/index.md b/docs/IDE/Reports/index.md new file mode 100644 index 000000000..9108d89ab --- /dev/null +++ b/docs/IDE/Reports/index.md @@ -0,0 +1,82 @@ +--- +title: Designing reports +parent: IDE +nav_order: 5.5 +permalink: /tB/IDE/Reports/ +has_toc: false +redirect_from: + - /tB/IDE/Project/Editor/Report +index: + - report designer + - tbreport + - report sections + - page width, report +--- + +# Designing reports + +The report designer lays out a report in bands, one for each part of the page, and lets you place labels, shapes, lines, pictures and QR codes in them. It is the [form designer](../Forms/Designer) in another mode, so selecting, moving, the grid, zoom, undo and the right-click menu work as they do for a form. This page describes what is different. How a report is filled with data and shown from code is in the [Report class](../../Packages/VB/Report/). + +## Add a report + +**Add Windows Report** creates the report. It is in the **Add** menu, and needs the WinNativeForms package. The command makes `MyReport.tbreport` and `MyReport.twin` and opens the designer, as [Add a form](../Forms/Designer#add-a-form) describes for a form. The class in the code file has an empty `Sub Report_Load()`. + +## The bands + +A new report has five sections, from top to bottom: + +| Section | Height | Height in PROPERTIES | Also | +|---|---|---|---| +| **Report Header** | 25 pixels | 375 | | +| **Page Header** | 100 pixels | 1500 | | +| **Report Detail** | 25 pixels | 375 | **KeepTogether** is True | +| **Page Footer** | 50 pixels | 750 | | +| **Report Footer** | 25 pixels | 375 | **KeepTogether** is True | + +![The report designer open on a tab named MyReport.tbreport, with a horizontal ruler across the top and five bands stacked down the page: Report Header, Page Header, Report Detail, Page Footer and Report Footer. Each band has a bar with its name above an empty dotted design surface, and Page Header is the tallest of the five.](../Images/tbReport.png){:width="1319" height="698"} + +Each section is a bar that shows the section's name, with a working area under it. The area is as wide as the report, and the page margins show as padding. A ruler in centimetres runs across the top, with a tick every quarter of a centimetre, and each section has a ruler at its left. + +The bar only selects the section. A click on it, or on an empty part of the section, shows the section's properties in [PROPERTIES](../Forms/Properties), and the object drop-down lists the section as its name followed by `ConstrolsSection`, spelled that way. A second click changes nothing. A band does not collapse. + +The sections have these properties, beside **Height**: **IdeCaption**, **SectionType**, **BackStyle**, **BackColor**, **BackColorAlternate**, **BorderBottomHeight**, **BorderBottomColor**, **ForcePageBreak** and **KeepTogether**. Heights show in twips, 15 to a pixel. Setting a back colour also sets **BackStyle** to opaque. + +To change a section's height, drag the blue bar at its bottom edge. The change sets the section's **Height**. + +## The page + +Click the corner where the rulers meet to select the report itself. Its properties include the page settings: **ReportWidth**, **ReportHeight** and the margins. A new report has margins of 10 mm, 37.795 pixels, on each side, a **ReportWidth** of 717.567 pixels and a **ReportHeight** of 1047.053 pixels. PROPERTIES shows them in twips, so **ReportWidth** reads 10763.5. + +The designer lists no page-size control. Set the page by typing **ReportWidth** and **ReportHeight** in PROPERTIES, or drag the bars that run the height of the report: + +- **PAGE WIDTH** sets **ReportWidth**. Dragging it 30 pixels to the right makes the report 30 pixels wider. +- **LEFT MARGIN** sets the left margin. The right edge of the report stays where it is. +- **RIGHT MARGIN** sets the right margin. Dragging it to the right makes the margin smaller and **ReportWidth** larger by the same distance. + +Each of the three bars shows only while the pointer is over it. + +![The report designer with a ruler along the top and the five section bars, Report Header to Report Footer. Report Header holds a label reading Sales by region, and Report Detail a short line and a label reading Region. The Report Detail bar is selected and drawn darker than the others. The PAGE WIDTH bar shows at the right as a blue vertical line with its name beside it.](Images/Report_Controls.png){:width="947" height="450"} + +## Controls in a report + +A report holds windowless controls only. Its Toolbox lists seven tools: **Pointer**, **Label**, **Shape**, **Line**, **Image**, **CheckMark** and **QRCode**. It shows no group title bars and no **More components**. + +![The Toolbox pane as it appears for a report, holding seven tool icons in a single column: the pointer arrow, a letter A for Label, a shape, a diagonal line, a picture, a ticked box and a QR code.](../Images/Toolbox_Report.png){:width="198" height="269"} + +A control belongs to the section where you draw it. A double-click on a tool places the control in the current section, and a control that you paste goes into the section that is shown. Drawing or pasting any other control gives the notification *Can't add* the control's type *to a report. Only windowless controls allowed.*, and adds nothing. + +> [!NOTE] +> In twinBASIC BETA 1005, a floating Toolbox lists all 24 controls and **More components** when a report is open. A control that a report does not allow is refused when you use it, so nothing is lost. Dock the Toolbox, or use only the seven tools above. + +A report has no **Form Menu** bar and no control arrays, and its controls have no **Index** property. The other designer features work: selection, moving and sizing, the grid, copy and paste, locks, undo, zoom, the right-click menu and the [Format](../Forms/Arranging) commands. + +## Preview a report + +**PREVIEW** and F6 open the report in a window titled with the project's name, with the design as it is on the screen. See [Previewing a form](../Forms/Previewing). The window and its toolbar are described in the [Report class](../../Packages/VB/Report/#built-in-preview-toolbar). + +## See Also + +- [Designer](../Forms/Designer) -- opening a designer, the grid, zoom and locks +- [Toolbox](../Forms/Toolbox) -- the list of tools +- [Properties](../Forms/Properties) -- the PROPERTIES pane and its units +- [Report class](../../Packages/VB/Report/) -- showing and printing a report from code diff --git a/docs/IDE/Toolbox.md b/docs/IDE/Toolbox.md deleted file mode 100644 index 09c25f9bf..000000000 --- a/docs/IDE/Toolbox.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -title: Toolbox -parent: IDE -# nav_order: 4 -permalink: /tB/IDE/Project/Toolbox ---- - -# Toolbox - -The Toolbox lists the controls available for placement on forms and designers in the current project. Additional COM components can be added through the **+ More Components** button, which opens the COM References section of Project Settings. - -See [Controls](../../Controls) - - - -![The Toolbox button labelled More components, a grey plus sign beside the text inside a dashed outline.](Images/Toolbox_MoreComponents.png){:width="208" height="44"} - -![A twinBASIC information box reading Components list coming soon, explaining that for now ordinary COM references to the appropriate ActiveX type library must be added and the components will then appear in the form designer automatically, above a GoTo COM References button and an OK button.](Images/Components_Message.png){:width="654" height="169"} - -The "GoTo COM References" button takes you to **Project Settings** and filters by "project.references". - -![The Project Settings dialog filtered to project.references, on its Enabled Libraries tab. Four ticked entries are listed in priority order against Library Symbol and Version columns: the twinBASIC VBA and VBRUN compatibility packages, OLE Automation as stdole, and the IDE Extensibility package as tbIDE.](Images/ProjectSettings_LibraryReferences.png){:width="1102" height="774"} - -Click on the _Available COM References_ tab. - -![The same dialog on its Available COM References tab, where a search box sits above an alphabetical list of unticked type libraries registered on the machine, ABBYY FineReader 16 SharePoint Integration Component, AccessibilityCplAdmin, Acrobat and Acrobat Access among them on this machine, against Library Symbol, Version and Publisher columns.](Images/ProjectSettings_AvailableCOMReferences.png){:width="1102" height="774"} - -See [Project Settings](../Project-Settings) for more info. diff --git a/docs/IDE/index.md b/docs/IDE/index.md index 36d6c120a..ee2992951 100644 --- a/docs/IDE/index.md +++ b/docs/IDE/index.md @@ -8,10 +8,10 @@ permalink: /tB/IDE ![The twinBASIC IDE with no project loaded. A menu bar and toolbar run across the top, the Toolbox is docked on the left, an empty Editor 1 fills the centre, and Project Explorer sits above Properties on the right. Debug Console, Diagnostics, Call Stack and Variables share a row along the bottom. Every pane is empty, and the status bar reports tB Services unavailable and tB Licence not ready.](Images/IDE.png){:width="1920" height="1032"} -The IDE consists of several fixed panes and tool windows. The [**Project Explorer**](IDE/Project/Explorer) shows the file tree of the open project; the [**Editor**](IDE/Code/Editor) is the main code and designer surface; the [**Properties**](IDE/Project/Properties) pane shows and edits properties for the selected item; the [**Toolbox**](IDE/Project/Toolbox) lists the controls available to drop onto a form. +The IDE consists of several fixed panes and tool windows. The [**Project Explorer**](IDE/Project/Explorer) shows the file tree of the open project; the [**Editor**](IDE/Code/Editor) is the main code and designer surface; the [**Properties**](IDE/Forms/Properties) pane shows and edits properties for the selected item; the [**Toolbox**](IDE/Forms/Toolbox) lists the controls available to drop onto a form. The debugging panels --- [**Call Stack**](IDE/Debugging/Inspecting#call-stack), [**Variables**](IDE/Debugging/Inspecting#variables), [**Debug Console**](IDE/Debugging/Debug-Console), [**Watches**](IDE/Debugging/Inspecting#watches) and [**Memory**](IDE/Debugging/Memory) --- are described in [**Running and debugging**](IDE/Debugging/). Nothing opens them when a program stops. The Call Stack, Variables and Debug Console are in the default layout. Watches and Memory are not: open them from the **View** menu. The panels you use while writing code --- [**Diagnostics**](IDE/Code/Diagnostics), [**Outline**](IDE/Code/Navigating#outline), [**Open Editors**](IDE/Code/Navigating#open-editors), [**History**](IDE/Code/Navigating#history) and [**Find / Replace**](IDE/Code/Finding) --- are described, with the editor, in [**Writing code**](IDE/Code/). -The [**tbForm**](IDE/Project/Editor/Form) and [**tbReport**](IDE/Project/Editor/Report) designers open when a form or report file is selected in the Project Explorer. Third-party and community [**addins**](IDE/AddIns/) extend the IDE with additional commands and tool windows. +The [**form designer**](IDE/Forms/) and the [**report designer**](IDE/Reports/) open when a form or report file is opened from the Project Explorer. Third-party and community [**addins**](IDE/AddIns/) extend the IDE with additional commands and tool windows. diff --git a/docs/IDE/tbForm.md b/docs/IDE/tbForm.md deleted file mode 100644 index d7df7c97f..000000000 --- a/docs/IDE/tbForm.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: tbForm -parent: IDE -# nav_order: 2 -permalink: /tB/IDE/Project/Editor/Form ---- - -# .tbForm - -![The form designer open on a tab named MyForm.tbform in Editor 1. An empty form occupies the top left corner as a white dotted grid, with a plus menu button above it, sizing handles on its right and bottom edges, and a 300 x 300 size readout at its lower corner.](Images/tbForm.png){:width="1319" height="698"} - -See [Toolbox](../Toolbox) for [Controls](../../../Controls) available to use. diff --git a/docs/IDE/tbReport.md b/docs/IDE/tbReport.md deleted file mode 100644 index 5c2c5d660..000000000 --- a/docs/IDE/tbReport.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: tbReport -parent: IDE -# nav_order: 2 -permalink: /tB/IDE/Project/Editor/Report ---- - -# .tbReport - -Contains - -- Report Header -- Page Header -- Report Detail -- Page Footer -- Report Footer - -![The report designer open on a tab named MyReport.tbreport, with a horizontal ruler across the top and five bands stacked down the page: Report Header, Page Header, Report Detail, Page Footer and Report Footer. Each band has a collapsible header and an empty dotted design surface beneath it, and Page Header is the tallest of the five.](Images/tbReport.png){:width="1319" height="698"} - -Only certain [Controls](../../../Controls) are available to a Report. - -- Pointer -- Label -- Shape -- Line -- Image -- CheckMark -- QRCode - -![The Toolbox pane as it appears for a report, holding seven tool icons in a single column: the pointer arrow, a letter A for Label, a shape, a diagonal line, a picture, a ticked box and a QR code.](Images/Toolbox_Report.png){:width="198" height="269"} diff --git a/docs/Tutorials/CustomControls/Property sheet and object serialization.md b/docs/Tutorials/CustomControls/Property sheet and object serialization.md index 8b6bed006..2041fe7c0 100644 --- a/docs/Tutorials/CustomControls/Property sheet and object serialization.md +++ b/docs/Tutorials/CustomControls/Property sheet and object serialization.md @@ -100,7 +100,7 @@ At the moment, the form-designer doesn't yet support code-behind-forms, so this > [!TIP] -> If you make changes to your CustomControl class, such as exposing new properties or changing how a control is drawn, these changes will get reflected immediately to any open form designers. Form designers will show a 'resync' button when you return to them, once pressed the changes will be apparent. +> If you make changes to your CustomControl class, such as exposing new properties or changing how a control is drawn, these changes will get reflected immediately to any open form designers. A form designer that is open behind the code refreshes itself when you return to it, and the changes are then apparent. See [RESYNC](../../tB/IDE/Forms/Previewing#resync). > [!TIP] > The serialization happens via JSON when running in the IDE, but via a binary format when running in a compiled DLL/EXE. The `SerializeInfo` object that `GetSerializer()` returns is a different implementation when running in the IDE, but this should be transparent to you as a CustomControl implementer. diff --git a/docs/Tutorials/WebView2/Building a browser shell.md b/docs/Tutorials/WebView2/Building a browser shell.md index 0c6b20392..c549df647 100644 --- a/docs/Tutorials/WebView2/Building a browser shell.md +++ b/docs/Tutorials/WebView2/Building a browser shell.md @@ -10,7 +10,7 @@ permalink: /Tutorials/WebView2/Building-A-Browser-Shell A short worked tutorial: turn a [**WebView2**](../../tB/Packages/WebView2/WebView2/) control into a working browser with an address bar, back / forward / reload buttons, zoom, and a few helpers (DevTools, Task Manager, PDF export). -The complete project ships as *Sample 0 --- WebView2 Examples* in the New-Project dialog (form *Example 1*). This tutorial describes its key pieces. +The complete project ships as *Sample 1a --- WebView2 Examples* in the New-Project dialog (form *Example 1*). This tutorial describes its key pieces. ## The form diff --git a/docs/Tutorials/WebView2/Driving Monaco.md b/docs/Tutorials/WebView2/Driving Monaco.md index d979cc3f7..2408acc0c 100644 --- a/docs/Tutorials/WebView2/Driving Monaco.md +++ b/docs/Tutorials/WebView2/Driving Monaco.md @@ -10,7 +10,7 @@ permalink: /Tutorials/WebView2/Driving-Monaco A case study combining everything from the previous tutorials: a form with **two** [**WebView2**](../../tB/Packages/WebView2/WebView2/) controls --- the Microsoft Monaco editor on the left, a live HTML preview on the right. As the user types, Monaco posts the edited source to twinBASIC, which mirrors it into the preview pane. -The complete project ships as *Sample 0 --- WebView2 Examples* in the New-Project dialog (form *Example 3*). +The complete project ships as *Sample 1a --- WebView2 Examples* in the New-Project dialog (form *Example 3*). ## Architecture diff --git a/docs/Tutorials/WebView2/Hosting local web assets.md b/docs/Tutorials/WebView2/Hosting local web assets.md index 57bc720d0..68dbeb8d3 100644 --- a/docs/Tutorials/WebView2/Hosting local web assets.md +++ b/docs/Tutorials/WebView2/Hosting local web assets.md @@ -10,7 +10,7 @@ permalink: /Tutorials/WebView2/Hosting-Local-Web-Assets A [**WebView2**](../../tB/Packages/WebView2/WebView2/) control can serve HTML, JavaScript, CSS, and any other assets straight from a folder on disk --- no embedded HTTP server required. Edge's [**SetVirtualHostNameToFolderMapping**](../../tB/Packages/WebView2/WebView2/#setvirtualhostnametofoldermapping) routes a virtual `https://` hostname to a local folder so that resources behave as if they came from a real origin: same-origin `fetch`, Content Security Policy, service workers, and so on all work as expected. -This tutorial demonstrates the pattern used by *Sample 0 --- WebView2 Examples* (forms *Example 2*, *Example 3*, *Example 4*). +This tutorial demonstrates the pattern used by *Sample 1a --- WebView2 Examples* (forms *Example 2*, *Example 3*, *Example 4*). ## The three-step pattern diff --git a/docs/Tutorials/WebView2/JavaScript interop.md b/docs/Tutorials/WebView2/JavaScript interop.md index fe3fa119a..28c416415 100644 --- a/docs/Tutorials/WebView2/JavaScript interop.md +++ b/docs/Tutorials/WebView2/JavaScript interop.md @@ -14,7 +14,7 @@ The [**WebView2**](../../tB/Packages/WebView2/WebView2/) control offers three co 2. **Messages** --- push a value (string, number, array, …) in either direction and listen for it on the other side. 3. **Scripted calls** --- call a named JavaScript function from BASIC and (optionally) wait for its return value. -This tutorial covers all three, with the matching JavaScript side shown next to each BASIC side. The worked code comes from *Sample 0 --- WebView2 Examples* (form *Example 2*). +This tutorial covers all three, with the matching JavaScript side shown next to each BASIC side. The worked code comes from *Sample 1a --- WebView2 Examples* (form *Example 2*). ## Bridge 1 --- Host objects diff --git a/docs/Tutorials/WebView2/index.md b/docs/Tutorials/WebView2/index.md index 06d120a77..4bca4173b 100644 --- a/docs/Tutorials/WebView2/index.md +++ b/docs/Tutorials/WebView2/index.md @@ -20,6 +20,6 @@ These tutorials demonstrate the most common patterns: - [JavaScript interop](JavaScript-Interop) -- the three bridges between BASIC and the page: host objects, messages, and scripted calls. - [Driving Monaco from twinBASIC](Driving-Monaco) -- a case study combining everything above: embed Microsoft's Monaco editor next to a live HTML preview pane. -The complete sample code for the last four tutorials ships as *Sample 0 --- WebView2 Examples* in the New-Project dialog. +The complete sample code for the last four tutorials ships as *Sample 1a --- WebView2 Examples* in the New-Project dialog. For the full set of members on the control itself, see the [**WebView2** class reference](../../tB/Packages/WebView2/WebView2/). diff --git a/docs/_book.yml b/docs/_book.yml index 0640276a1..2d001ca0c 100644 --- a/docs/_book.yml +++ b/docs/_book.yml @@ -277,6 +277,16 @@ parts: page: /tB/IDE/Code/ landing_is_target: true outline_closed: true + - title: Designing forms + landing_page: /tB/IDE/Forms/ + page: /tB/IDE/Forms/ + landing_is_target: true + outline_closed: true + - title: Designing reports + page: /tB/IDE/Reports/ + no_outline_entry: true + no_heading_shift: true + outline_closed: true - title: Running and debugging landing_page: /tB/IDE/Debugging/ page: /tB/IDE/Debugging/ @@ -705,12 +715,10 @@ left_out: - reason: IDE pages that are still screenshots and the labels they show, or headings with nothing under them no_descent: true pages: - - /tB/IDE/Project/Editor/Form - /tB/IDE/Project/Explorer - /tB/IDE/AddIns/Community - /tB/IDE/Reference/Menu - /tB/IDE/Reference/Menu/Project - - /tB/IDE/Reference/Menu/Format - /tB/IDE/Reference/Menu/AddIns - /tB/IDE/Reference/Menu/Help @@ -719,14 +727,12 @@ left_out: pages: - /tB/IDE/Reference/StatusBar - /tB/IDE/Reference - - /tB/IDE/Project/Toolbox - - /tB/IDE/Project/Editor/Report - - /tB/IDE/Project/Properties - /tB/IDE/Project/Webpage - /tB/IDE/Project/Splash - /tB/IDE/Project/PackagePublishing - /tB/IDE/AddIns/GlobalSearch - /tB/IDE/Reference/Menu/Edit + - /tB/IDE/Reference/Menu/Format - /tB/IDE/Reference/Menu/Run - /tB/IDE/Reference/Menu/Debug - /tB/IDE/Reference/Menu/View diff --git a/docs/index.md b/docs/index.md index 4c14a5416..d36c48789 100644 --- a/docs/index.md +++ b/docs/index.md @@ -80,7 +80,7 @@ Step-by-step guides, the first four written for someone new to twinBASIC. [Tutor ## The twinBASIC IDE -The [**IDE section**](tB/IDE) documents the [code editor and the panels beside it](tB/IDE/Code/) (diagnostics, find, outline), the project explorer, [running and debugging](tB/IDE/Debugging/) (breakpoints, call stack, watches, debug console), the [**tbForm**](tB/IDE/Project/Editor/Form) and [**tbReport**](tB/IDE/Project/Editor/Report) designers, and the per-feature side panes. To install third-party addins, see [**Add Ins**](tB/IDE/AddIns/); to author your own, the [**tbIDE package**](tB/Packages/tbIDE/) is the addin SDK. +The [**IDE section**](tB/IDE) documents the [code editor and the panels beside it](tB/IDE/Code/) (diagnostics, find, outline), the project explorer, [running and debugging](tB/IDE/Debugging/) (breakpoints, call stack, watches, debug console), the [**form designer**](tB/IDE/Forms/) and [**report designer**](tB/IDE/Reports/), and the per-feature side panes. To install third-party addins, see [**Add Ins**](tB/IDE/AddIns/); to author your own, the [**tbIDE package**](tB/Packages/tbIDE/) is the addin SDK. ## Community and external resources diff --git a/scripts/lib/tb-operate.mjs b/scripts/lib/tb-operate.mjs index 2299c4f0f..0a6f1cd96 100644 --- a/scripts/lib/tb-operate.mjs +++ b/scripts/lib/tb-operate.mjs @@ -346,6 +346,26 @@ export const editorState = (c) => c.evaluate(EDITOR_JS); export const editorText = (c) => c.evaluate("typeof editor !== 'undefined' && editor.getModel() ? editor.getModel().getValue() : null"); +// The IDE sends an edit to the compiler from a setTimeout(0) and marks the file +// pendingChangeConfirmation until the answer arrives. That answer goes to +// parseDocumentDecorations, which redraws every breakpoint glyph from the +// answer's own list: a breakpoint set with F9 before it arrives loses its glyph, +// and the next F9 in the file sends a list without it. So a script that edits +// and then sets a breakpoint waits for this first. The answer takes 20--60 ms. +const EDIT_ANSWERED_JS = `new Promise((done) => setTimeout(() => { + const tab = typeof openEditors === "undefined" ? null : openEditors.selectedEditorNode; + done(!tab || !tab.fileNode || !tab.fileNode.pendingChangeConfirmation); +}, 0))`; + +/** + * Wait until the compiler has answered the last edit of the file in the code + * editor. True once it has, false when `timeout` runs out first. + */ +export async function waitForEditAnswer(c, { timeout = 5 * 1000 } = {}) { + const ok = await waitFor(c, (cc) => cc.evaluate(EDIT_ANSWERED_JS, { awaitPromise: true }), { timeout, interval: 10 }); + return ok === true; +} + // How long the IDE may still put the cursor back where it last revealed a // line, in milliseconds. Opening a file at a place calls revealLineInEditor, // and whenever the compiler's decorations for the document arrive less than diff --git a/scripts/shoot_docs.mjs b/scripts/shoot_docs.mjs index 1daf35f42..3968a1a88 100644 --- a/scripts/shoot_docs.mjs +++ b/scripts/shoot_docs.mjs @@ -193,6 +193,7 @@ import { setCursor, typeText, waitFor, + waitForEditAnswer, } from "./lib/tb-operate.mjs"; import { claimPorts } from "./lib/tb-ports.mjs"; import { deleteSettings, finishTidy, restoreKeys, settingsKey, snapshotKeys, startTidy } from "./lib/tb-registry.mjs"; @@ -1321,6 +1322,28 @@ async function startCodeChapter(run, { extra = null } = {}) { return run.lane.open(src); } +// ---- forms-chapter: test/shots/forms, FormsDemo, for the pictures of the "Designing forms and +// reports" chapter (formsChapterShots): a form with a menu and controls of a few kinds, a +// Frame holding a TextBox, a control array, and a report with controls in two of its bands. +const FORMS_CHAPTER = path.join(REPO_ROOT, "test", "shots", "forms"); + +async function startFormsChapter(run) { + run.step = "open"; + say(run.name, `opening ${path.relative(REPO_ROOT, FORMS_CHAPTER)}`); + const src = path.join(run.work, "forms-src"); + cpSync(FORMS_CHAPTER, src, { recursive: true }); + return run.lane.open(src); +} + +// The IDE's desktop, where a previewed form's window shows, and a folder for its captures. +async function prepareFormsChapter(run) { + const ctx = await prepareWithDesigners(run); + ctx.desktop = `tbbuild-${run.ports[0]}`; + ctx.windows = path.join(run.work, "windows"); + mkdirSync(ctx.windows, { recursive: true }); + return ctx; +} + // ---- community: the demo with the compiler started on no licence key, the status bar's // COMMUNITY EDITION badge as the IDE draws it for a key that is not there (the Community // Edition), and its LIMITED services badge while a compiler starts.The IDE reads the key from its settings into @@ -1375,6 +1398,7 @@ const SETUPS = { sample6: { ports: 1, start: startSample6, prepare: prepareSample }, designer: { ports: 2, start: startDesigner, prepare: prepareDesigner }, forms: { ports: 1, start: (run) => startDesigner(run, { addin: false }), prepare: prepareWithDesigners }, + "forms-chapter": { ports: 1, start: startFormsChapter, prepare: prepareFormsChapter }, customcontrols: { ports: 1, start: startCustomControls, prepare: prepareWithDesigners }, code: { ports: 1, start: startCode, prepare: prepareSample }, sample9: { ports: 1, start: startSample9, prepare: prepareSample }, @@ -4041,7 +4065,17 @@ const designerShots = [ formatShot("Menu_Format_1"), formatShot("Menu_Format_Align", "Align"), formatShot("Menu_Format_MakeSameSize", "Make Same Size"), - formatShot("Menu_Format_HorizontalSpacing", "Horizontal Spacing"), + // with three controls selected, so that Make Equal is live (setup forms-chapter, frmMain: + // Command1, Check1 and Command2(0), at Left 10, 140 and 250 as in Arranging.md's example) + menuShot("Menu_Format_HorizontalSpacing", "Format", "Horizontal Spacing", { + setup: "forms-chapter", + before: async (c) => { + await formsDesigner(c); + await selectNamed(c, ["Command1", "Check1", "Command2(0)"]); + await parkMouse(c); + }, + after: formsReset, + }), formatShot("Menu_Format_VerticalSpacing", "Vertical Spacing"), // the toolbar with a form open in the designer searchToolbarShot("Toolbar_3", "designer", (c) => openDesigner(c, "MyForm.tbform")), @@ -4433,6 +4467,1041 @@ const formShots = [ }, ]; +// ---- the "Designing forms and reports" chapter (setup forms-chapter): FormsDemo, +// test/shots/forms. frmMain has a File menu (Open, a separator, Exit) and the controls the +// chapter's pages name: Text1, Label1, Command1, Check1, Frame1 holding Text2, and the control +// array Command2(0) and Command2(1); MyReport has a Label in its Report Header and a Line and a +// Label in its Report Detail. The project refers to WinNativeCommonCtls, so a form's Toolbox +// has a COMMON CONTROLS group. Every picture starts from no tab, no menu and the default layout +// (formsReset), and closes the designer's tab with Discard Changes after it, so that a change +// a picture makes (a lock, a menu, the outlines, the zoom) is never in the next one: the +// designer reads the form from its file again when it opens. + +const FORMS_IMAGE = (name) => `IDE/Forms/Images/${name}.png`; +const REPORTS_IMAGE = (name) => `IDE/Reports/Images/${name}.png`; +const FORMS_FILE = (name) => `/FormsDemo/Sources/${name}`; + +async function formsReset(c) { + await resetUi(c); + await closeTabs(c, [], { discard: true }); + await defaultLayout(c); + await parkMouse(c); +} + +// `file` of FormsDemo open in its designer, alone, from its file. +async function formsDesigner(c, file = "frmMain.tbform") { + await formsReset(c); + await openFile(c, FORMS_FILE(file)); + await designerShown(c); +} + +// The controls named selected as a person selects them: a click on the first, a Shift+click +// on each other one. A name with an index, `Command2(1)`, is that member of the array. +async function selectNamed(c, names) { + const centre = (n) => + c.evaluate(`(() => { + const [, name, index] = /^(\\w+)(?:\\((\\d+)\\))?$/.exec(${JSON.stringify(n)}); + const k = ${DESIGNER_GLOBALS}.allControls.find((x) => x.properties.Name === name && (index === undefined || x.properties.Index === Number(index))); + if (!k) return null; + const r = k.getBoundingClientRect(); + return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; +})()`); + for (const [i, n] of names.entries()) { + const at = await centre(n); + if (!at) throw new Error(`the designer does not draw ${n}`); + const modifiers = i ? 8 : 0; + await c.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: at.x, y: at.y, modifiers }); + for (const type of ["mousePressed", "mouseReleased"]) { + await c.send("Input.dispatchMouseEvent", { type, x: at.x, y: at.y, button: "left", clickCount: 1, modifiers }); + } + } + const want = names + .map((n) => n.replace(/\(\d+\)$/, "")) + .sort() + .join(); + if (!(await waitFor(c, async () => (await selectedControls(c)).join() === want, { timeout: 3000, interval: 50 }))) { + throw new Error(`the clicks selected ${JSON.stringify(await selectedControls(c))}, not ${want}`); + } + await frames(c); +} + +// The box of the open designer's form with its menu bar and the readouts under it. +const formArea = (c) => + c.evaluate(`(() => { + const g = ${DESIGNER_GLOBALS}; + const boxes = [g.designer, g.designerMenuBar, g.designerDragInfoLeft, g.designerDragInfoRight] + .filter((e) => e && e.getBoundingClientRect().width) + .map((e) => e.getBoundingClientRect()); + const x = Math.min(...boxes.map((r) => r.left)); + const y = Math.min(...boxes.map((r) => r.top)); + return { x, y, width: Math.max(...boxes.map((r) => r.right)) - x, height: Math.max(...boxes.map((r) => r.bottom)) - y }; +})()`); + +// The boxes of the selected control's middle grabber on its right edge and of its in-place +// toolbar (the toolbar's element has no size of its own: its buttons' boxes, joined). +const selectionParts = (c) => + c.evaluate(`(() => { + const box = (r) => ({ x: r.x, y: r.y, width: r.width, height: r.height }); + const pts = [...document.querySelectorAll(".selectedControlDragPointsContainer > *")] + .map((e) => e.getBoundingClientRect()) + .filter((r) => r.width); + const right = Math.max(...pts.map((r) => r.x)); + const column = pts.filter((r) => Math.abs(r.x - right) < 1).sort((a, b) => a.y - b.y); + const parts = [...document.querySelectorAll(".miniToolbarVisible *")] + .map((e) => e.getBoundingClientRect()) + .filter((r) => r.width && r.height); + const x = Math.min(...parts.map((r) => r.left)); + const y = Math.min(...parts.map((r) => r.top)); + return { + grab: column.length ? box(column[Math.floor(column.length / 2)]) : null, + mini: parts.length ? { x, y, width: Math.max(...parts.map((r) => r.right)) - x, height: Math.max(...parts.map((r) => r.bottom)) - y } : null, + }; +})()`); + +// The striped ground round a designer's form is drawn a little differently from one capture to +// the next (a few dozen pixels along the stripes, by more than a capture's noise), so that no two +// captures of a picture with much of it agree. These are capture's `unsteady` regions for the +// ground a clip shows: the clip less the form's own boxes (the form, its menu bar, the readouts) +// and the boxes `over` names (a menu, a list), so that nothing but the ground is left out. +async function groundUnsteady(c, clip, over = []) { + const { parts, ground, tabs } = await c.evaluate(`(() => { + const g = ${DESIGNER_GLOBALS}; + const box = (e) => { const r = e.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; }; + return { + parts: [g.designer, g.designerMenuBar, g.designerDragInfoLeft, g.designerDragInfoRight] + .filter((e) => e && e.getBoundingClientRect().width) + .map((e) => ({ ...box(e), pill: e !== g.designer && e !== g.designerMenuBar })), + ground: (() => { + const h = [...document.querySelectorAll(".sectionHeaderInner")].find((e) => e.textContent === "EDITOR 1"); + const p = h.closest(".toolWindowContainer").getBoundingClientRect(); + const tabs = document.getElementById("codePanelTabs").getBoundingClientRect(); + return { x: p.x, y: tabs.top, width: p.width, height: p.bottom - tabs.top }; + })(), + // the tabs and the Tabs List button, in the row the ground is drawn behind + tabs: [...document.querySelectorAll("#codePanelTabs > *, #codePanelTabs ~ *")] + .filter((e) => { + const r = e.getBoundingClientRect(); + const t = document.getElementById("codePanelTabs").getBoundingClientRect(); + return r.width && r.top < t.bottom && r.bottom <= t.bottom + 4; + }) + .map(box), + }; +})()`); + // the ground is the editor's area under its tabs + const x0 = Math.max(clip.x, ground.x); + const y0 = Math.max(clip.y, ground.y); + const x1 = Math.min(clip.x + clip.width, ground.x + ground.width); + const y1 = Math.min(clip.y + clip.height, ground.y + ground.height); + if (x0 >= x1 || y0 >= y1) return []; + let left = [{ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }]; + // The form's own boxes are compared only 2 pixels in from their edges, where the ground + // shows through their anti-aliased edges and shadows, and a readout, a pill, only between + // its rounded ends; a menu or a label over the ground is compared to 2 pixels beyond it. + const inner = (s) => { + const r = grow(s, -2); + return s.pill ? { ...r, x: s.x + s.height / 2, width: s.width - s.height } : r; + }; + const cuts = [...parts.map(inner), ...[...tabs, ...over.filter(Boolean)].map((s) => grow(s, 2))]; + for (const cut of cuts.filter((r) => r.width > 0 && r.height > 0)) { + left = left.flatMap((r) => { + const ix0 = Math.max(r.x, cut.x); + const iy0 = Math.max(r.y, cut.y); + const ix1 = Math.min(r.x + r.width, cut.x + cut.width); + const iy1 = Math.min(r.y + r.height, cut.y + cut.height); + if (ix0 >= ix1 || iy0 >= iy1) return [r]; + return [ + { x: r.x, y: r.y, width: r.width, height: iy0 - r.y }, + { x: r.x, y: iy1, width: r.width, height: r.y + r.height - iy1 }, + { x: r.x, y: iy0, width: ix0 - r.x, height: iy1 - iy0 }, + { x: ix1, y: iy0, width: r.x + r.width - ix1, height: iy1 - iy0 }, + ].filter((p) => p.width > 0 && p.height > 0); + }); + } + return left.map((rect) => ({ on: { rect }, pad: 0 })); +} + +// The boxes of what the annotation layer draws, each label, arrow and ring on its own: the +// pictures whose labels lie on a designer's ground leave only the ground round them unsteady. +const layerBoxes = (c) => + c.evaluate(`(() => { + const l = document.getElementById(${JSON.stringify(LAYER_ID)}); + if (!l) return []; + let kids = [...l.children].filter((e) => e.tagName !== "defs"); + while (kids.length === 1 && kids[0].children.length) kids = [...kids[0].children]; + return kids + .map((e) => e.getBoundingClientRect()) + .filter((r) => r.width || r.height) + .map((r) => ({ x: r.x, y: r.y, width: r.width, height: r.height })); +})()`); + +// A point of the open designer's form, in form pixels. +const formPoint = (c, x, y) => + c.evaluate(`(() => { + const r = ${DESIGNER_GLOBALS}.designer.getBoundingClientRect(); + return { x: r.x + ${x}, y: r.y + ${y} }; +})()`); + +const formsChapterShots = [ + { + // frmMain with Label1 selected: its grabbers and the in-place toolbar under it, and the + // readouts under the form, the pointer's position read with the mouse resting on an empty + // part of the form (no pointer is drawn). Labels on the editor's ground right of the form. + out: FORMS_IMAGE("Designer_Selected"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + await selectNamed(c, ["Label1"]); + const rest = await formPoint(c, 300, 245); + await mouseMove(c, rest.x - 4, rest.y); + await mouseMove(c, rest.x, rest.y); + const shown = await waitFor( + c, + () => c.evaluate(`/^x:\\d+ y:\\d+$/.test(${DESIGNER_GLOBALS}.designerDragInfoLeft.innerText)`), + { timeout: 3000, interval: 50 }, + ); + if (!shown) throw new Error("the readouts do not show the pointer's position"); + await frames(c); + const area = await formArea(c); + const { grab, mini } = await selectionParts(c); + const x = area.x + area.width + 36; + const mid = (r) => r.y + r.height / 2; + const READOUTS = { span: [{ css: ".designerDragInfoLeft" }, { css: ".designerDragInfoRight" }] }; + const under = { of: READOUTS, at: "bottom", dy: 34 }; + const annotate = [ + { type: "label", text: "Grabbers", on: POINT(x, mid(grab)), side: "right", gap: 0 }, + { type: "arrow", from: POINT(x - 4, mid(grab)), to: { rect: grab } }, + { type: "label", text: "In-place toolbar", on: POINT(x, mid(mini)), side: "right", gap: 0 }, + { type: "arrow", from: POINT(x - 4, mid(mini)), to: { rect: mini } }, + { type: "label", text: "Readouts", on: under, side: "below", gap: 0 }, + { type: "arrow", from: under, to: [{ css: ".designerDragInfoLeft" }, { css: ".designerDragInfoRight" }] }, + ]; + try { + const { box } = await annotateOver(c, annotate); + const clip = snapOut(union({ x: area.x, y: area.y, width: area.width + 6, height: area.height + 8 }, box)); + return await capture(c, "Designer_Selected", clip, { + unsteady: await groundUnsteady(c, clip, await layerBoxes(c)), + }); + } finally { + await unannotate(c); + } + } finally { + await formsReset(c); + } + }, + }, + formStateShot("Designer_TabIndex", async (c) => { + // the designer focused with a click on an empty part of the form, the pointer left there + // (moved away, the pointer readout shows or not by the moment it left), then Ctrl held + const at = await formPoint(c, 300, 245); + await pressAt(c, at.x, at.y); + await mouseMove(c, at.x, at.y); + const readout = () => + c.evaluate(`(() => { + const e = ${DESIGNER_GLOBALS}.designerDragInfoLeft; + return !!e && e.getBoundingClientRect().width > 0 && e.innerText.includes("x:300 y:245"); +})()`); + if (!(await waitFor(c, readout, { timeout: 3000, interval: 50 }))) { + throw new Error("the pointer readout does not show x:300 y:245"); + } + await c.send("Input.dispatchKeyEvent", { + type: "rawKeyDown", + key: "Control", + code: "ControlLeft", + windowsVirtualKeyCode: 17, + nativeVirtualKeyCode: 17, + modifiers: 2, + }); + const shown = () => c.evaluate(`${DESIGNER_GLOBALS}.designer.classList.contains("designerShowTabIndex")`); + if (!(await waitFor(c, shown, { timeout: 3000, interval: 50 }))) { + throw new Error("holding Ctrl showed no TabIndex circles"); + } + }), + formStateShot("Controls_MultiSelect", (c) => selectNamed(c, ["Command1", "Text1", "Check1"])), + formStateShot( + "Controls_RubberBand", + async (c) => { + // pressed on the form's empty top right, dragged down and left over Label1 and Text1 + const from = await formPoint(c, 300, 4); + const to = await formPoint(c, 60, 46); + await mouseMove(c, from.x, from.y); + await c.send("Input.dispatchMouseEvent", { + type: "mousePressed", + x: from.x, + y: from.y, + button: "left", + clickCount: 1, + buttons: 1, + }); + for (let i = 1; i <= 10; i++) { + await sleep(30); + const x = from.x + ((to.x - from.x) * i) / 10; + const y = from.y + ((to.y - from.y) * i) / 10; + await c.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, button: "left", buttons: 1 }); + } + await frames(c); + }, + { + after: async (c) => { + const to = await formPoint(c, 60, 46); + await c.send("Input.dispatchMouseEvent", { + type: "mouseReleased", + x: to.x, + y: to.y, + button: "left", + clickCount: 1, + }); + }, + }, + ), + formStateShot("Controls_Container", async (c) => { + // the OptionButton tool picked in the Toolbox, the pointer resting inside Frame1 + const tool = await rectOf(c, `${TOOLBOX_TILE("OptionButton")}`); + if (!tool) throw new Error("the Toolbox has no OptionButton"); + await pressAt(c, tool.x + tool.width / 2, tool.y + tool.height / 2); + const at = await formPoint(c, 120, 220); + await mouseMove(c, at.x - 6, at.y); + await mouseMove(c, at.x, at.y); + const red = await waitFor( + c, + () => + c.evaluate( + `(() => { const f = ${DESIGNER_GLOBALS}.currentFlashingContainer; return f?.properties?.Name === "Frame1" && f.classList.contains("boxShadowRed"); })()`, + ), + { timeout: 3000, interval: 50 }, + ); + if (!red) throw new Error("Frame1 was not outlined in red under the OptionButton tool"); + // The outline blinks (the IDE's borderBlink animation, red at its middle), and the quiet + // style sheet stops every animation at its start, where the outline is transparent: this + // one is held at its middle instead, as the IDE draws it half the time. + await c.evaluate(`(() => { + const s = document.createElement("style"); + s.id = "tbShotBlink"; + s.textContent = ".designer .boxShadowRed { animation: borderBlink 0.5s linear -0.25s 1 normal both paused !important }"; + document.head.appendChild(s); +})()`); + }), + formStateShot("Designer_Locked", async (c) => { + // the outlines on from the grid button's panel, then Command1 locked from the Format menu + await gridOptions(c); + const box = await rectOf(c, ".optionShowOutlines"); + if (!box) throw new Error("the grid options have no Show Outlines"); + await pressAt(c, box.x + box.width / 2, box.y + box.height / 2); + if ( + !(await waitFor( + c, + () => c.evaluate(`${DESIGNER_GLOBALS}.state.item.form[0].__IDEOptions.showOutlines === true`), + { timeout: 3000, interval: 50 }, + )) + ) { + throw new Error("Show Outlines did not turn the outlines on"); + } + await parkMouse(c); + await waitFor(c, () => c.evaluate(`menuBarGridOptions.style.visibility !== "visible"`), { + timeout: 3000, + interval: 50, + }); + await designerShown(c); + await selectNamed(c, ["Command1"]); + await openMenu(c, "Format", { close: false }); + const lock = (await menuItems(c)).find((i) => i.text.startsWith("Lock Controls")); + if (!lock) throw new Error("the Format menu has no Lock Controls"); + await clickAt(c, lock.x + lock.width / 2, lock.y + lock.height / 2); + if (!(await waitFor(c, () => rectOf(c, ".hasLockedIcon"), { timeout: 3000, interval: 50 }))) { + throw new Error("Lock Controls drew no lock"); + } + await parkMouse(c); + }), + { + // the grid button's panel, opened by the pointer resting on the button, over the editor + out: FORMS_IMAGE("Designer_GridOptions"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + await gridOptions(c); + const panel = await rectOf(c, ".menuBarGridOptions"); + const button = await rectOf(c, "#menuBarGrid, .menuBarGrid"); + const area = grow(union(panel, button), 12); + const clip = snapOut({ ...area, y: button.y - 4, height: area.y + area.height - button.y + 4 }); + return await capture(c, "Designer_GridOptions", clip, { + unsteady: await groundUnsteady(c, clip, [panel]), + }); + } finally { + await formsReset(c); + } + }, + }, + { + // 50 % picked in the zoom box, as a person picks it from the box's list, and the list drawn + // open over the toolbar, with the form at half size below + out: FORMS_IMAGE("Designer_Zoom"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + const full = (await formArea(c)).width; + await c.evaluate(`(() => { + zoomSelect.selectedIndex = [...zoomSelect.options].findIndex((o) => o.text === "50%"); + zoomSelect.dispatchEvent(new Event("change", { bubbles: true })); +})()`); + const half = await waitFor( + c, + async () => { + const w = (await formArea(c)).width; + return w < full * 0.6 ? w : null; + }, + { timeout: 5000, interval: 50 }, + ); + if (!half) throw new Error("50 % in the zoom box did not shrink the form"); + await designerShown(c); + const zoom = await rectOf(c, "#zoomSelect"); + try { + const { box } = await annotateOver(c, [{ type: "list", select: { css: "#zoomSelect" }, rows: 4 }]); + // the box and its list, with the toolbar's buttons left of it: the form drawn at half + // size is not in the picture, since its scaled dots come out differently in every + // capture + const area = grow(union(zoom, box), 8); + const clip = snapOut({ ...area, x: area.x - 160, width: area.width + 160 }); + // four rows (the list scrolls): the Light theme's list is drawn in a smaller font, and + // a list of every option makes its picture 100 px shorter than the dark one + return await capture(c, "Designer_Zoom", clip, { + away: () => parkMouse(c), + unsteady: await groundUnsteady(c, clip, await layerBoxes(c)), + }); + } finally { + await unannotate(c); + } + } finally { + await formsReset(c); + } + }, + }, + { + // a right click on Command1, with Command2(0) selected too, and the Align submenu open + out: FORMS_IMAGE("Controls_ContextMenu"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + // the panels under the editor out of the layout, so that the menu is over the editor alone + for (const id of ["DEBUG CONSOLE", "PROBLEMS", "CALL STACK", "VARIABLES"]) await undock(c, id); + await designerShown(c); + await selectNamed(c, ["Command1", "Command2(0)"]); + const at = await formPoint(c, 60, 85); + await mouseMove(c, at.x, at.y); + await c.send("Input.dispatchMouseEvent", { + type: "mousePressed", + x: at.x, + y: at.y, + button: "right", + buttons: 2, + clickCount: 1, + }); + await c.send("Input.dispatchMouseEvent", { + type: "mouseReleased", + x: at.x, + y: at.y, + button: "right", + buttons: 0, + clickCount: 1, + }); + if (!(await menuOpened(c))) throw new Error("the right click on Command1 opened no menu"); + const sub = await hoverItem(c, "Align"); + const menu = await rectOf(c, "#contextMenu"); + const clip = snapOut(grow(union(union(await formArea(c), menu), sub), 6)); + return await capture(c, "Controls_ContextMenu", clip, { unsteady: await groundUnsteady(c, clip, [menu, sub]) }); + } finally { + await closeMenus(c); + await formsReset(c); + } + }, + }, + { + // + menu clicked in the Form Menu bar, beside the File menu, and &Edit typed into the box + out: FORMS_IMAGE("MenuEditor_Bar"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + const add = await rectOf(c, ".designerMenuItemAdd"); + if (!add) throw new Error("the Form Menu bar has no + menu"); + await pressAt(c, add.x + add.width / 2, add.y + add.height / 2); + const box = await waitFor( + c, + () => c.evaluate(`document.activeElement?.tagName === "INPUT" ? document.activeElement.value : null`), + { + timeout: 3000, + interval: 50, + }, + ); + if (box !== "Menu1") throw new Error(`+ menu opened a box holding ${JSON.stringify(box)}`); + await typeText(c, "&Edit"); + await parkMouse(c); + await frames(c); + const bar = await c.evaluate( + `(() => { const r = ${DESIGNER_GLOBALS}.designerMenuBar.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; })()`, + ); + return await capture(c, "MenuEditor_Bar", snapOut({ ...bar, height: bar.height + 48 }), {}); + } finally { + await pressKey(c, "Escape"); + await formsReset(c); + } + }, + }, + { + // the File menu clicked in the Form Menu bar, its items listed under it, and the right-click + // menu of Open beside them + out: FORMS_IMAGE("MenuEditor_Open"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + const item = (sel, text) => + c.evaluate(`(() => { + const e = [...document.querySelectorAll(${JSON.stringify(sel)})].find((e) => e.getBoundingClientRect().width && e.textContent.trim() === ${JSON.stringify(text)}); + if (!e) return null; + const r = e.getBoundingClientRect(); + return { x: r.x, y: r.y, width: r.width, height: r.height }; +})()`); + const file = await item(".designerMenuItem", "File"); + if (!file) throw new Error("the Form Menu bar has no File"); + await pressAt(c, file.x + file.width / 2, file.y + file.height / 2); + const open = await waitFor(c, () => item(".designerPopUpMenuItem", "Open"), { timeout: 3000, interval: 50 }); + if (!open) throw new Error("File opened no list with Open"); + // at the item's right end, so that the menu opens beside the list rather than over it + const at = { x: open.x + open.width - 4, y: open.y + open.height / 2 }; + await mouseMove(c, at.x, at.y); + await c.send("Input.dispatchMouseEvent", { + type: "mousePressed", + x: at.x, + y: at.y, + button: "right", + buttons: 2, + clickCount: 1, + }); + await c.send("Input.dispatchMouseEvent", { + type: "mouseReleased", + x: at.x, + y: at.y, + button: "right", + buttons: 0, + clickCount: 1, + }); + if (!(await menuOpened(c))) throw new Error("the right click on Open opened no menu"); + await parkMouse(c); + await frames(c); + const pop = await rectOf(c, ".designerPopUpMenuBar"); + const bar = await c.evaluate( + `(() => { const r = ${DESIGNER_GLOBALS}.designerMenuBar.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; })()`, + ); + const menu = await rectOf(c, "#contextMenu"); + const clip = snapOut(grow(union(union(bar, pop), menu), 6)); + return await capture(c, "MenuEditor_Open", clip, { unsteady: await groundUnsteady(c, clip, [pop, menu]) }); + } finally { + await closeMenus(c); + await formsReset(c); + } + }, + }, + { + // Add > Add Windows Form on Sources in the Project Explorer: the name box in the tree, + // holding MyForm.tbform with MyForm selected; Escape then creates nothing + out: FORMS_IMAGE("Designer_AddName"), + setup: "forms-chapter", + async take({ c }) { + await formsReset(c); + try { + await peOpen(c, "Sources", true); + await peRightClick(c, "Sources"); + await hoverItem(c, "Add"); + const it = (await menuItems(c, true)).find((i) => i.text.startsWith("Add Windows Form")); + if (!it) throw new Error("the Add submenu has no Add Windows Form"); + await mouseMove(c, it.x + 20, it.y + it.height / 2); + await frames(c); + await clickAt(c, it.x + 20, it.y + it.height / 2); + const offered = await waitFor( + c, + () => c.evaluate(`document.activeElement?.tagName === "INPUT" ? document.activeElement.value : null`), + { timeout: 5000, interval: 50 }, + ); + if (offered !== "MyForm.tbform") + throw new Error(`Add Windows Form offered the name ${JSON.stringify(offered)}`); + await parkMouse(c); + await frames(c); + const panel = await dockedRect(c, "PROJECT EXPLORER"); + // down to the last row of the tree, Startup.twin + const last = await c.evaluate( + `Math.max(...[...document.querySelectorAll("*")].filter((e) => e.childElementCount === 0 && e.textContent.trim() === "Startup.twin" && e.getBoundingClientRect().width).map((e) => e.getBoundingClientRect().bottom))`, + ); + if (!(last > panel.y)) throw new Error("the Project Explorer shows no Startup.twin"); + return await capture(c, "Designer_AddName", snapOut({ ...panel, height: last + 6 - panel.y }), {}); + } finally { + await pressKey(c, "Escape"); + await formsReset(c); + if (await c.evaluate(`!!fs.tree.resolvePath(${JSON.stringify(`twinbasic:${FORMS_FILE("MyForm.tbform")}`)})`)) { + complain(c.shot.name, "Escape in the name box created MyForm.tbform"); + } + } + }, + }, + { + // MyReport with the Report Detail bar clicked: the bands, the controls in two of them, and + // the page's three bars + out: REPORTS_IMAGE("Report_Controls"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c, "MyReport.tbreport"); + try { + // the panels beside the editor out of the layout, so that the editor is wide enough for + // the report and the bar right of it + for (const id of DOCKED_BESIDE_EDITOR) await undock(c, id); + await designerShown(c); + const bar = await c.evaluate(`(() => { + const b = [...document.querySelectorAll(".sectionBarDetail")].find((e) => e.getBoundingClientRect().width); + if (!b) return null; + const r = b.getBoundingClientRect(); + return { x: r.x + 60, y: r.y + r.height / 2 }; +})()`); + if (!bar) throw new Error("the report shows no Report Detail bar"); + await pressAt(c, bar.x, bar.y); + const picked = await waitFor( + c, + () => + c.evaluate( + `(() => { const s = ${DESIGNER_GLOBALS}.controlSelecter; return s.options[s.selectedIndex]?.text.startsWith("Report Detail"); })()`, + ), + { timeout: 3000, interval: 50 }, + ); + if (!picked) throw new Error("the click on the Report Detail bar did not select the section"); + // The three bars are drawn only under the pointer (opacity 0, 1 on hover): the pointer + // rests on PAGE WIDTH, at the report's right edge, half way down + const width = await c.evaluate( + `(() => { const r = ${DESIGNER_GLOBALS}.reportWidthResizerBar.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2, height: r.height }; })()`, + ); + if (!width.height) throw new Error("the report has no PAGE WIDTH bar"); + await mouseMove(c, width.x - 20, width.y); + await mouseMove(c, width.x, width.y); + const shown = await waitFor( + c, + () => c.evaluate(`getComputedStyle(${DESIGNER_GLOBALS}.reportWidthResizerBar).opacity === "1"`), + { timeout: 3000, interval: 50 }, + ); + if (!shown) throw new Error("the PAGE WIDTH bar did not show under the pointer"); + await frames(c); + const panel = await editorPanelRect(c); + const bottom = await c.evaluate( + `Math.max(...[...document.querySelectorAll(".designerContainerSectionResizer")].filter((e) => e.getBoundingClientRect().width).map((e) => e.getBoundingClientRect().bottom))`, + ); + const label = await c.evaluate( + `(() => { const r = ${DESIGNER_GLOBALS}.reportWidthResizerBar.querySelector(".resizerBarLabel").getBoundingClientRect(); return r.right; })()`, + ); + const clip = snapOut({ ...panel, width: label + 24 - panel.x, height: bottom + 12 - panel.y }); + return await capture(c, "Report_Controls", clip, { unsteady: await groundUnsteady(c, clip) }); + } finally { + await formsReset(c); + } + }, + }, + { + // the docked Toolbox of frmMain with Show Control Names chosen from its ... menu, in a page + // tall enough for every tool: the standard group and the COMMON CONTROLS group + out: FORMS_IMAGE("Toolbox_Form"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + await toolboxNames(c, true); + // docked at the left the full height of the window, wide enough for the names, and + // scrolled to its end with the mouse wheel: the end of the first group, the COMMON + // CONTROLS group and More components (a capture holds no more than the window's + // own height, so a taller page does not show the whole list) + await useLayout(c, TOOLBOX_LAYOUT); + await designerShown(c); + const lastTile = () => + c.evaluate(`(() => { + const h = [...document.querySelectorAll(".sectionHeaderInner")].find((e) => e.textContent === "TOOLBOX"); + const p = h.closest(".toolWindowContainer"); + const t = [...p.querySelectorAll(".toolboxItemOuter")].filter((e) => e.getBoundingClientRect().width); + return { last: t[t.length - 1].getBoundingClientRect().bottom, bottom: p.getBoundingClientRect().bottom }; +})()`); + const panel = await dockedRect(c, "TOOLBOX"); + const at = { x: panel.x + panel.width / 2, y: panel.y + panel.height / 2 }; + await mouseMove(c, at.x, at.y); + for (let i = 0; i < 10 && (await lastTile()).last > (await lastTile()).bottom; i++) { + await c.send("Input.dispatchMouseEvent", { type: "mouseWheel", x: at.x, y: at.y, deltaX: 0, deltaY: 400 }); + await frames(c); + } + const end = await lastTile(); + if (end.last > end.bottom) throw new Error("the wheel did not scroll the Toolbox to its end"); + await c.evaluate("document.activeElement?.blur?.()"); + await parkMouse(c); + return await capture(c, "Toolbox_Form", snapOut(await dockedRect(c, "TOOLBOX")), { away: () => parkMouse(c) }); + } finally { + await toolboxNames(c, false); + await formsReset(c); + } + }, + }, + { + // the Toolbox's search box opened with its magnifier and "Command, Text" typed into it, and + // the ... menu open on Show Control Names + out: FORMS_IMAGE("Toolbox_Search"), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + // the Toolbox docked wide enough for the search box + await useLayout(c, TOOLBOX_LAYOUT); + await designerShown(c); + const icon = await dockedRect(c, "TOOLBOX", ".searchObjectsIcon"); + if (!icon) throw new Error("the Toolbox's title bar has no magnifier"); + await pressAt(c, icon.x + icon.width / 2, icon.y + icon.height / 2); + const focused = await waitFor( + c, + () => c.evaluate("document.activeElement === toolboxDockablePanel.searchBarEntry"), + { + timeout: 3000, + interval: 50, + }, + ); + if (!focused) throw new Error("the magnifier opened no search box"); + await typeText(c, "Command, Text"); + const shown = () => + c.evaluate( + `[...document.querySelectorAll(".toolboxItemOuter")].filter((e) => e.getBoundingClientRect().width).map((e) => e.title || e.innerText.trim()).join()`, + ); + const narrowed = await waitFor(c, async () => /^Pointer,TextBox,CommandButton(,|$)/.test(await shown()), { + timeout: 3000, + interval: 50, + }); + if (!narrowed) throw new Error(`the search left ${await shown()}`); + await openToolboxMenu(c); + const box = await toolboxBox(c); + const menu = await rectOf(c, "#contextMenu"); + const clip = snapOut(grow(union(box, menu), 2)); + return await capture(c, "Toolbox_Search", clip, { + away: () => parkMouse(c), + unsteady: await groundUnsteady(c, clip, [box, menu]), + }); + } finally { + await closeMenus(c); + // Escape in the box ends the search and shows every tool again + if (await c.evaluate("!!toolboxDockablePanel.searchBarEntry?.getBoundingClientRect().width")) { + await c.evaluate("toolboxDockablePanel.searchBarEntry.focus()"); + await pressKey(c, "Escape"); + } + const all = await waitFor(c, () => rectOf(c, TOOLBOX_TILE("OptionButton")), { timeout: 3000, interval: 50 }); + if (!all) complain(c.shot.name, "the Toolbox's search did not end"); + await formsReset(c); + } + }, + }, + propertiesShot( + "Properties_Control", + async (c) => { + // the panel's parts named from its left + const parts = await c.evaluate(`(() => { + const p = document.querySelector(${JSON.stringify(PROPS)}); + const box = (e) => { const r = e.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; }; + const own = (t) => [...p.querySelectorAll("*")].find((e) => e.childElementCount === 0 && e.textContent.trim() === t && e.getBoundingClientRect().width && !e.closest(".sectionHeader")); + const sel = p.querySelector(".controlSelecter"); + const cat = [...p.querySelectorAll(".propertyCategory")].find((e) => e.getBoundingClientRect().height); + const tab = own("PROPERTIES"); + return { sel: sel && box(sel), cat: cat && box(cat), tab: tab && box(tab), panel: box(p) }; +})()`); + if (!parts.sel || !parts.cat || !parts.tab) + throw new Error(`the PROPERTIES panel lacks a part: ${JSON.stringify(parts)}`); + const x = parts.panel.x - 64; + const mid = (r) => r.y + r.height / 2; + const row = (text, r) => [ + { type: "label", text, on: POINT(x, mid(r)), side: "left", gap: 0 }, + { type: "arrow", from: POINT(x + 4, mid(r)), to: { rect: { ...r, width: 24 } } }, + ]; + return [...row("Tabs", parts.tab), ...row("Object drop-down", parts.sel), ...row("Category", parts.cat)]; + }, + { left: 820 }, + ), + propertiesShot( + "Properties_ColourPicker", + async (c) => { + await scrollProperties(c, "APPEARANCE"); + const swatch = await rectOf(c, `${PROP_VALUE("BackColor")} .colorElement`); + if (!swatch) throw new Error("the BackColor row has no swatch"); + await pressAt(c, swatch.x + swatch.width / 2, swatch.y + swatch.height / 2); + if (!(await waitFor(c, () => rectOf(c, ".sp-container:not(.sp-hidden)"), { timeout: 3000, interval: 50 }))) { + throw new Error("the swatch opened no colour picker"); + } + await parkMouse(c); + return null; + }, + { + extra: async (c) => [await rectOf(c, ".sp-container:not(.sp-hidden)")], + cut: [".sp-container"], + after: async (c) => { + const cancel = await rectOf(c, ".sp-container:not(.sp-hidden) .sp-cancel"); + if (cancel) await pressAt(c, cancel.x + cancel.width / 2, cancel.y + cancel.height / 2); + }, + }, + ), + propertiesShot( + "Properties_SystemColours", + async (c) => { + await scrollProperties(c, "APPEARANCE"); + const v = await rectOf(c, PROP_VALUE("BackColor")); + if (!v) throw new Error("the PROPERTIES panel has no BackColor row"); + const at = { x: v.x + v.width - 30, y: v.y + v.height / 2 }; + await mouseMove(c, at.x, at.y); + await c.send("Input.dispatchMouseEvent", { + type: "mousePressed", + x: at.x, + y: at.y, + button: "right", + buttons: 2, + clickCount: 1, + }); + await c.send("Input.dispatchMouseEvent", { + type: "mouseReleased", + x: at.x, + y: at.y, + button: "right", + buttons: 0, + clickCount: 1, + }); + if (!(await menuOpened(c))) throw new Error("the right click on BackColor's value opened no menu"); + await hoverItem(c, "SYSTEM COLORS"); + return null; + }, + { + extra: async (c) => [await rectOf(c, "#contextMenu"), await rectOf(c, "#contextMenuSUB")], + after: closeMenus, + away: false, + cut: ["#contextMenu", "#contextMenuSUB"], + }, + ), + { + // Add Windows Form in a project without the VB package (the settings setup's sample has + // none) asks for WinNativeForms first + out: FORMS_IMAGE("Designer_PackageNeeded"), + setup: "settings", + async take({ c }) { + await resetUi(c); + await peRightClick(c, "Sources"); + await hoverItem(c, "Add"); + const it = (await menuItems(c, true)).find((i) => i.text.startsWith("Add Windows Form")); + if (!it) throw new Error("the Add submenu has no Add Windows Form"); + await mouseMove(c, it.x + 20, it.y + it.height / 2); + await frames(c); + await clickAt(c, it.x + 20, it.y + it.height / 2); + if (!(await waitModal(c, "twinBASIC", { timeout: 5000 }))) throw new Error("no Package needed message appeared"); + await dialogStill(c); + try { + return await dialogShot(c, "Designer_PackageNeeded"); + } finally { + await closeModal(c, "Cancel").catch(() => pressKey(c, "Escape")); + } + }, + }, + { + // frmMain previewed with the toolbar's PREVIEW: its window on the IDE's desktop, as it draws + // itself, with the File menu of the menu editor in its menu bar; taken once, as a running + // program's window is + out: FORMS_IMAGE("Preview_Window"), + setup: "forms-chapter", + once: true, + async take(ctx) { + const { c } = ctx; + await formsDesigner(c); + try { + const preview = await rectOf(c, "#menuBarPreview"); + if (!preview) throw new Error("the toolbar has no PREVIEW button"); + await pressAt(c, preview.x + preview.width / 2, preview.y + preview.height / 2); + await parkMouse(c); + const w = await formWindow(ctx.desktop, { title: "frmMain", timeout: 60000 }); + return besideEachOther([ + await windowPicture(ctx.desktop, w.hwnd, "preview", path.join(ctx.windows, "preview.png")), + ]); + } finally { + await stopRun(c); + await clearConsole(c); + await formsReset(c); + } + }, + }, + propertiesShot("Properties_Events", async (c) => { + const tab = await c.evaluate( + `(() => { const r = ${DESIGNER_GLOBALS}.toolbarEvents.getBoundingClientRect(); return { x: r.x, y: r.y, width: r.width, height: r.height }; })()`, + ); + await pressAt(c, tab.x + tab.width / 2, tab.y + tab.height / 2); + const shown = await waitFor( + c, + () => + c.evaluate( + `[...document.querySelectorAll(${JSON.stringify(`${PROPS} .propertyName`)})].some((e) => e.getBoundingClientRect().height && e.textContent.trim() === "Click")`, + ), + { timeout: 3000, interval: 50 }, + ); + if (!shown) throw new Error("the EVENTS tab lists no Click"); + await parkMouse(c); + return null; + }), +]; + +// The Toolbox docked at the left of the editor, the window's full height (set in the page and +// saved nowhere; formsReset puts the default layout back). +const TOOLBOX_LAYOUT = { + type: "horizontal", + variableSize: true, + size: "0%", + content: [ + { id: "TOOLBAR", variableSize: false, size: "fit-content" }, + { + type: "vertical", + variableSize: true, + size: "0%", + content: [ + { id: "TOOLBOX", variableSize: false, size: "17%" }, + { id: "EDITOR", variableSize: true, size: "0%" }, + ], + }, + ], +}; + +// The docked Toolbox, from its title bar to a little below its last tool. +const toolboxBox = (c) => + c.evaluate(`(() => { + const h = [...document.querySelectorAll(".sectionHeaderInner")].find((e) => e.textContent === "TOOLBOX"); + const p = h.closest(".toolWindowContainer"); + const r = p.getBoundingClientRect(); + const last = Math.max(...[...p.querySelectorAll(".toolboxItemOuter")].filter((e) => e.getBoundingClientRect().width).map((e) => e.getBoundingClientRect().bottom)); + return { x: r.x, y: r.y, width: r.width, height: Math.min(r.bottom, last + 10) - r.y }; +})()`); + +// The docked Toolbox's ... menu, opened with a real click. +async function openToolboxMenu(c) { + const dots = await dockedRect(c, "TOOLBOX", ".ellipsesIcon2"); + if (!dots) throw new Error("the Toolbox's title bar has no ... button"); + await pressAt(c, dots.x + dots.width / 2, dots.y + dots.height / 2); + if (!(await menuOpened(c))) throw new Error("the Toolbox's ... button opened no menu"); +} + +// Show Control Names ticked (`on`) or not, chosen from the Toolbox's ... menu when it is not +// so already. The option it saves is saved nowhere (PAGE_DEFAULTS). +async function toolboxNames(c, on) { + if ((await c.evaluate("!liveIDEOptions.showToolboxIconsOnly")) === on) return; + await openToolboxMenu(c); + const it = (await menuItems(c)).find((i) => i.text.startsWith("Show Control Names")); + if (!it) throw new Error("the Toolbox's ... menu has no Show Control Names"); + await clickAt(c, it.x + it.width / 2, it.y + it.height / 2); + if ( + !(await waitFor(c, async () => (await c.evaluate("!liveIDEOptions.showToolboxIconsOnly")) === on, { + timeout: 3000, + interval: 50, + })) + ) { + throw new Error("Show Control Names did not change the Toolbox"); + } + await closeMenus(c); + await frames(c); +} + +// frmMain open with Command1 selected and PROPERTIES floating on its own, brought to a state +// by `state(c)`, which returns the annotations (or null). `extra(c)` gives a box the picture +// holds as well as the panel (a colour picker, a menu); `after(c)` runs once it is taken. +function propertiesShot( + name, + state, + { extra = null, after = null, away = true, left = 300, top = 120, height = 520, cut = null } = {}, +) { + return { + out: FORMS_IMAGE(name), + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + await selectNamed(c, ["Command1"]); + await parkMouse(c); + await floatPanel(c, "PROPERTIES", { width: 360, height, left, top }); + const annotate = await state(c); + let area = await rectOf(c, PROPS); + const solids = [area]; + for (const box of extra ? await extra(c) : []) { + area = union(area, box); + solids.push(box); + } + try { + if (annotate) { + const { box } = await annotateOver(c, annotate); + area = union(area, box); + solids.push(...(await layerBoxes(c))); + } + const clip = snapOut(area); + return await capture(c, name, clip, { + away: away ? () => parkMouse(c) : null, + // a picture of the panel and what opens beside it is a cut-out of them, as a menu's is + ...(cut ? { keep: [PROPS, ...cut] } : { unsteady: await groundUnsteady(c, clip, solids) }), + }); + } finally { + if (annotate) await unannotate(c); + } + } finally { + if (after) await after(c); + await unfloatPanel(c, "PROPERTIES"); + await formsReset(c); + } + }, + }; +} + +// A Toolbox tile by its tool's name. +const TOOLBOX_TILE = (name) => `.toolboxItemOuter[title="${name}"]`; + +// The grid options panel, opened as the pointer opens it: moved onto the grid button. +async function gridOptions(c) { + const b = await rectOf(c, "#menuBarGrid, .menuBarGrid"); + if (!b) throw new Error("the toolbar has no grid button"); + await mouseMove(c, b.x - 30, b.y + b.height / 2); + await mouseMove(c, b.x + b.width / 2, b.y + b.height / 2); + if ( + !(await waitFor(c, () => c.evaluate(`menuBarGridOptions.style.visibility === "visible"`), { + timeout: 3000, + interval: 50, + })) + ) { + throw new Error("the grid options did not open under the pointer"); + } + await frames(c); +} + +// frmMain's form, its menu bar and readouts, in the state `state(c)` brings it to; `after(c)` +// runs once the picture is taken, before the tab is closed. +function formStateShot(name, state, { after = null, out = FORMS_IMAGE(name) } = {}) { + return { + out, + setup: "forms-chapter", + async take({ c }) { + await formsDesigner(c); + try { + await state(c); + const area = await formArea(c); + const clip = snapOut({ x: area.x, y: area.y, width: area.width + 6, height: area.height + 8 }); + return await capture(c, name, clip, { unsteady: await groundUnsteady(c, clip) }); + } finally { + if (after) await after(c); + await c.evaluate(`document.getElementById("tbShotBlink")?.remove()`); + await c.send("Input.dispatchKeyEvent", { + type: "keyUp", + key: "Control", + code: "ControlLeft", + windowsVirtualKeyCode: 17, + nativeVirtualKeyCode: 17, + }); + await formsReset(c); + } + }, + }; +} + // ---- the CustomControls tutorial's property sheet: composites of the fixture's code and the // PROPERTIES panel (setup customcontrols) @@ -6556,6 +7625,7 @@ async function debugReset(c) { // A breakpoint on the line of the open file holding `text`, set with F9 as a person sets it. async function breakpointOn(c, text) { const { line } = await codePlace(c, text); + if (!(await waitForEditAnswer(c))) throw new Error("the compiler never answered the last edit"); await setCursor(c, line, 9); await pressKey(c, "F9"); const set = await waitFor( @@ -9815,6 +10885,7 @@ const SHOTS = [ ...sample6Shots, ...designerShots, ...formShots, + ...formsChapterShots, ...customControlsShots, ...customControlsCodeShots, ...codeShots, @@ -9882,6 +10953,7 @@ const JOB_SECONDS = { sample6: 21, designer: 61, forms: 40, + "forms-chapter": 90, customcontrols: 54, "customcontrols-2": 53, "customcontrols-3": 35, diff --git a/test/ide/debugging.test.mjs b/test/ide/debugging.test.mjs index 53de3e703..93a7d8e27 100644 --- a/test/ide/debugging.test.mjs +++ b/test/ide/debugging.test.mjs @@ -45,7 +45,16 @@ import { before, test } from "node:test"; import { fileURLToPath } from "node:url"; import { sleep } from "../../scripts/lib/tb-ide.mjs"; import { consoleMark, linesSince } from "../../scripts/lib/tb-ide-console.mjs"; -import { click, clickAt, openFile, pressKey, setCursor, typeText, waitFor } from "../../scripts/lib/tb-operate.mjs"; +import { + click, + clickAt, + openFile, + pressKey, + setCursor, + typeText, + waitFor, + waitForEditAnswer, +} from "../../scripts/lib/tb-operate.mjs"; import { scenario } from "../addin/scenario.mjs"; const HERE = path.dirname(fileURLToPath(import.meta.url)); @@ -174,6 +183,7 @@ scenario("running and debugging", (lane) => { assert.ok(await waitFor(c, async () => !(await c.evaluate(GLYPHS_JS)).length), "a breakpoint is left"); } for (const line of lines) { + assert.ok(await waitForEditAnswer(c), "the compiler never answered the last edit"); await setCursor(c, line, 9); await pressKey(c, "F9"); assert.ok(await waitFor(c, async () => (await c.evaluate(GLYPHS_JS)).includes(line)), `no breakpoint on ${line}`); diff --git a/test/ide/forms.test.mjs b/test/ide/forms.test.mjs new file mode 100644 index 000000000..16f1039cd --- /dev/null +++ b/test/ide/forms.test.mjs @@ -0,0 +1,1058 @@ +// The form and report designers, tested against eighteen entries in BUGS-TO-REPORT.md: +// +// "Horizontal and Vertical Spacing move the controls with no undo step and without marking the +// form changed" (bugs/form-spacing-not-recorded) +// "Make Same Size marks the form changed, but Save keeps the old sizes unless another change +// follows" (bugs/form-same-size-not-saved) +// "Arrow keys in the designer's zoom box also move the selected controls" +// (bugs/form-zoom-box-arrows-move) +// "Lock Controls does not mark the form changed, so a lock made on its own is lost when the +// designer is closed" (bugs/form-lock-not-saved) +// "Align > Center (Horizontal) and Center (Vertical) centre the controls on the selection's left +// or top edge, not on its centre" (bugs/form-align-center-edge) +// "Center In Container puts a control off centre when the designer is zoomed" +// (bugs/form-center-in-container-zoom) +// "On a CustomControls form, PROPERTIES shows a control's position in twips but takes a typed +// value in pixels" (bugs/customcontrols-properties-twips) +// "Escape and then a pointer move over the form mark the form changed, with an undo step that +// changes nothing" (bugs/form-escape-marks-changed) +// "Ctrl+A does nothing in a PROPERTIES box" (bugs/properties-ctrl-a) +// "Alt+arrow in the form designer also opens the IDE's menu bar" (bugs/form-alt-arrow-menu) +// "Renaming a control to another control's name asks the control-array question twice" +// (bugs/form-array-question-twice) +// "Add Windows Form accepts a name that is not an identifier" (bugs/add-form-bad-name) +// "View > Zoom Out stops at 50 %, though the zoom box offers 25 %" (bugs/form-zoom-out-floor) +// "The History entry for a change in a designer reads [line: undefined]" +// (bugs/history-form-line-undefined) +// "Shift+F7 does not switch from a form's code to its designer when a folder in its path has +// .twin in its name" (bugs/form-switch-twin-folder) +// "With a report open, a floating Toolbox lists every form control" +// (bugs/report-floating-toolbox) +// "A package's read-only designer file opens an empty tab besides the notice that it cannot be +// opened" (bugs/readonly-designer-empty-tab) +// "A form file with bad JSON opens an empty designer that takes no edit" +// (bugs/form-bad-json-empty-designer) +// +// The lane opens three projects, one after the other. probes/forms has frmMain (Label1, Text1, +// Command1, Check1, Frame1 holding Text2, and the control array Command2(0) and Command2(1)), two +// copies of it, frmSizes and frmSaved, for the tests that save, and frmDeep in the folder +// Sources/v1.twin.forms. probes/forms-badjson has frmBad, whose JSON has a doubled comma, so it +// does not compile (TB5246, TB5247), which the lane accepts for that project only. +// probes/forms-custom is a copy of test/shots/customcontrols, a CustomControls form. +// +// A designer is driven with real input: clicks, Shift+clicks and keys through CDP, the Format and +// View menus clicked open. A test that needs frmMain as the file holds it closes its tab, +// answering Discard Changes, and opens it again: the designer then starts with no undo steps. +// Positions are what the designer holds in pixels; the lane's private desktop is at 150 %, so a +// position the designer rounds to a device pixel reads 54.67 for 55, and an arrow key moves a +// control 0.667 px. The page's currentDPI is set to its devicePixelRatio first, as the trials +// that found these defects did. +// +// THE IDE SAVES ITS OPTIONS AND PANEL LAYOUT IN THE USER'S OWN REGISTRY (HKCU\Software\VB and VBA +// Program Settings\twinBASIC_IDE\IDESettings). So, as soon as each project is open, the page's +// hostAppObject is replaced by a proxy whose SaveIDESetting, ChangeWindowSetting and recent-list +// calls do nothing, as debugging.test.mjs does; the tests float the HISTORY and TOOLBOX panels. +// Nothing here copies to the clipboard or reaches SaveSetting, so lanes.mjs names no settings. +// +// The tests share the lane's IDE in order. Each test states what BETA 1005 does; the tests named +// "control" pass whether or not the defects are fixed. If another fails after an IDE update, the +// IDE has changed: update the entry in BUGS-TO-REPORT.md, or its report in bugs/filed/, the +// callout on the page of docs/IDE/Forms/ or docs/IDE/Reports/ that carries it, and then this file. +// +// Run it with ide-test.bat, which gives it a lane; on its own it is skipped. + +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { before, test } from "node:test"; +import { fileURLToPath } from "node:url"; +import { compileOutcome, sleep, waitForCompile } from "../../scripts/lib/tb-ide.mjs"; +import { + editorText, + notifications, + openFile, + pressKey, + setCursor, + typeText, + waitFor, +} from "../../scripts/lib/tb-operate.mjs"; +import { unpackProject } from "../../scripts/lib/tb-project.mjs"; +import { scenario } from "../addin/scenario.mjs"; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const PROJECT = path.join(HERE, "probes", "forms"); +const BAD = path.join(HERE, "probes", "forms-badjson"); +const CUSTOM = path.join(HERE, "probes", "forms-custom"); +const SRC = (file) => `/FormsProbe/Sources/${file}`; +const G = "getCurrentFormEditorGlobals()"; +const J = JSON.stringify; + +// Blocks every call that writes the user's registry, and makes the designer's DPI the page's. +const GUARD_JS = `(() => { + if (!window.__guarded) { + window.__guarded = true; + const blocked = new Set(["SaveIDESetting", "ChangeWindowSetting", "AddRecentsList", "RemoveRecentsList"]); + hostAppObject = new Proxy(hostAppObject, { + get(target, key) { + if (blocked.has(key)) return async () => undefined; + return Reflect.get(target, key); + }, + }); + } + currentDPI = window.devicePixelRatio; + return true; +})()`; + +// Records the type of every message the active designer sends to the IDE (onChanged, +// onSignificantChange, ownerUndo...), once per designer. +const HOOK_JS = `(() => { + const g = ${G}; + if (!g || g.__hooked) return !!g; + g.__hooked = true; + window.__msgs = []; + const real = g.vscode.postMessage.bind(g.vscode); + g.vscode.postMessage = (m) => { window.__msgs.push(m && m.type); return real(m); }; + return true; +})()`; + +const ID_JS = `(p) => p.Name + (p.Index >= 0 ? "(" + p.Index + ")" : "")`; +// Every control of the active designer as the surface holds it, by name and index. +const SURFACE_JS = `(() => { + const g = ${G}; if (!g) return null; const id = ${ID_JS}; const out = {}; + for (const x of g.allControls || []) { const p = x.properties || {}; out[id(p)] = { L: p.Left, T: p.Top, W: p.Width, H: p.Height }; } + return out; +})()`; +// Every control as the designer's form state holds it (what Save writes), and the locked list. +const STATE_JS = `(() => { + const g = ${G}; if (!g) return null; const id = ${ID_JS}; const out = {}; + const f = g.state.item.form; + const walk = (o) => { for (const x of o._children || []) { out[id(x)] = { L: x.Left, T: x.Top, W: x.Width, H: x.Height }; walk(x); } }; + walk(f[0]); + return { controls: out, locked: (f[0].__IDEOptions || {}).lockedControls || [] }; +})()`; +const SELECTED_JS = `(() => { const g = ${G}; const id = ${ID_JS}; return (g.selectedControls || []).map((x) => x === g.designer ? "(form)" : id(x.properties)); })()`; +const AT_JS = (id) => `(() => { + const g = ${G}; const id = ${ID_JS}; + const x = (g.allControls || []).find((x) => id(x.properties) === ${J(id)}); + if (!x) return null; + const r = x.getBoundingClientRect(); + return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; +})()`; +const TABS_JS = `[...document.querySelectorAll("#codePanelTabs > .tabItem")].map((t) => ({ + name: t.innerText.trim(), selected: t.classList.contains("tabItemSelected"), dirty: !!t.querySelector(".circleIconSmall"), +}))`; +const MENU_OPEN_JS = `(() => { const m = document.querySelector("#contextMenu"); return !!m && m.getBoundingClientRect().width > 0; })()`; +const MENU_ITEMS_JS = ( + sub, +) => `[...document.querySelectorAll(${J(sub ? "#contextMenuSUB > *" : "#contextMenu > *")})].map((e) => { + const r = e.getBoundingClientRect(); + return { cls: String(e.className), text: e.innerText.replace(/\\s+/g, " ").trim(), x: r.x + r.width / 2, y: r.y + r.height / 2, w: r.width }; +}).filter((i) => i.w)`; +const BOXES_JS = `[...document.querySelectorAll(".modalDialogContainer")].filter((m) => m.getBoundingClientRect().width).map((m) => ({ + text: m.innerText.replace(/\\s+/g, " ").trim(), + buttons: [...m.querySelectorAll(".msgBoxButton, button")].map((b) => b.textContent.trim()).filter(Boolean), +}))`; +// The PROPERTIES rows shown: name and the value in its box. +const PROPS_JS = `[...document.querySelectorAll(".propertyName")].filter((e) => e.getBoundingClientRect().height).map((n) => { + const v = n.nextElementSibling; const i = v && v.querySelector("input, select, textarea"); + return { name: n.textContent.trim(), value: i ? (i.type === "checkbox" ? i.checked : i.value) : v ? v.innerText.trim() : null }; +})`; +const PROP_BOX_JS = (name) => `(() => { + const n = [...document.querySelectorAll(".propertyName")].find((e) => e.getBoundingClientRect().height && e.textContent.trim() === ${J(name)}); + const e = n && n.nextElementSibling && (n.nextElementSibling.querySelector("input, select, textarea") || n.nextElementSibling); + if (!e) return null; + e.scrollIntoView({ block: "center" }); + const r = e.getBoundingClientRect(); + return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; +})()`; +const TOOLS_JS = (scope) => `[...document.querySelectorAll(${J(`${scope} .toolboxItemOuter`)})] + .filter((e) => e.getBoundingClientRect().width).map((e) => { + const r = e.getBoundingClientRect(); + return { name: (e.title || e.innerText || "").trim(), x: r.x + r.width / 2, y: r.y + r.height / 2 }; + })`; +const HISTORY_ROWS_JS = `(() => { + const p = document.querySelector('.floatingPanel[id="HISTORY"]'); + if (!p) return null; + return [...p.querySelectorAll(".treeItemInner")].filter((e) => e.getBoundingClientRect().width).map((e) => { + const q = e.getBoundingClientRect(); + return { text: e.innerText.replace(/\\s+/g, " ").trim(), x: q.x + Math.min(60, q.width / 2), y: q.y + q.height / 2 }; + }); +})()`; +const TOOLTIP_JS = `(() => { const t = document.querySelector("#hoverTooltip"); return t && t.classList.contains("visibleTooltip") ? t.innerText : null; })()`; + +const near = (a, b, tolerance = 1) => Math.abs(Number(a) - Number(b)) <= tolerance; +const THREE = ["Command1", "Check1", "Command2(0)"]; +const pick = (all, ids) => Object.fromEntries(ids.map((id) => [id, all[id]])); + +scenario("designing forms and reports", (lane) => { + let c; + + // ---------------------------------------------------------------- input + + const mods = ({ alt, ctrl, shift } = {}) => (alt ? 1 : 0) | (ctrl ? 2 : 0) | (shift ? 8 : 0); + async function clickAt(x, y, { clickCount = 1, ...m } = {}) { + const modifiers = mods(m); + await c.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y, modifiers }); + await c.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount, modifiers }); + await c.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount, modifiers }); + await sleep(250); + } + async function doubleClickAt(x, y) { + await clickAt(x, y); + await c.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 2 }); + await c.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 2 }); + await sleep(400); + } + const moveTo = (x, y) => c.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y }); + + // ---------------------------------------------------------------- reading the designer + + const surface = () => c.evaluate(SURFACE_JS); + const formState = () => c.evaluate(STATE_JS); + const selected = () => c.evaluate(SELECTED_JS); + const tabs = () => c.evaluate(TABS_JS); + const tab = async (name) => (await tabs()).find((t) => t.name === name) ?? null; + const menuOpen = () => c.evaluate(MENU_OPEN_JS); + const boxes = () => c.evaluate(BOXES_JS); + const props = () => c.evaluate(PROPS_JS); + const prop = async (name) => (await props()).find((r) => r.name === name)?.value; + const zoom = () => c.evaluate("zoomSelect.value"); + // The designer's messages since the last call, the selection changes left out. + const messages = () => c.evaluate("(window.__msgs || []).splice(0).filter((m) => m !== 'onChangedSelection')"); + + async function clickControl(id, m = {}) { + const p = await c.evaluate(AT_JS(id)); + assert.ok(p, `the designer has no control ${id}`); + await clickAt(p.x, p.y, m); + } + // A click on an empty part of the form, near its bottom-right corner: the designer has the focus. + async function clickForm() { + const r = await c.evaluate( + `(() => { const r = ${G}.designer.getBoundingClientRect(); return { x: r.x + r.width * 0.97, y: r.y + r.height * 0.97 }; })()`, + ); + await clickAt(r.x, r.y); + } + async function select(...ids) { + await clickForm(); + for (const [i, id] of ids.entries()) await clickControl(id, i ? { shift: true } : {}); + await sleep(300); + assert.deepEqual(await selected(), ids); + } + async function answer(caption) { + const r = await c.evaluate(`(() => { + const ms = [...document.querySelectorAll(".modalDialogContainer")].filter((m) => m.getBoundingClientRect().width); + const m = ms[ms.length - 1]; + const b = m && [...m.querySelectorAll(".msgBoxButton, button")].find((e) => e.textContent.trim() === ${J(caption)} && e.getBoundingClientRect().width); + if (!b) return null; + const q = b.getBoundingClientRect(); + return { x: q.x + q.width / 2, y: q.y + q.height / 2 }; + })()`); + assert.ok(r, `no box with a ${caption} button`); + await clickAt(r.x, r.y); + await sleep(500); + } + + // ---------------------------------------------------------------- menus + + const menuItems = (sub = false) => c.evaluate(MENU_ITEMS_JS(sub)); + async function closeMenus() { + if (await menuOpen()) { + await pressKey(c, "Escape"); + await sleep(300); + } + } + // Clicks a menu of the IDE's menu bar open, then the item, then the item of its submenu. + async function choose(menu, item, sub = null) { + const r = await c.evaluate( + `(() => { const q = document.getElementById("rootMenu${menu}").getBoundingClientRect(); return { x: q.x + q.width / 2, y: q.y + q.height / 2 }; })()`, + ); + await clickAt(r.x, r.y); + await sleep(400); + const items = await menuItems(); + const it = items.find((i) => i.text.startsWith(item)); + if (!it || /Disabled/.test(it.cls)) { + await closeMenus(); + assert.fail(`${menu} > ${item} is ${it ? "greyed" : "missing"}: ${items.map((i) => i.text).join(" | ")}`); + } + await moveTo(it.x, it.y); + await clickAt(it.x, it.y); + await sleep(sub ? 400 : 600); + if (!sub) return; + const subs = await menuItems(true); + const s = subs.find((i) => i.text.startsWith(sub)); + if (!s || /Disabled/.test(s.cls)) { + await closeMenus(); + assert.fail(`${menu} > ${item} > ${sub} is ${s ? "greyed" : "missing"}: ${subs.map((i) => i.text).join(" | ")}`); + } + await clickAt(s.x, s.y); + await sleep(600); + } + + // ---------------------------------------------------------------- designers and tabs + + async function waitForDesigner() { + const ok = await waitFor( + c, + () => + c.evaluate( + `(() => { const g = ${G}; return !!g && g.gotInitialFormData === true && (!g.resyncOverlay || g.resyncOverlay.style.visibility !== "visible"); })()`, + ), + { timeout: 30 * 1000, interval: 150 }, + ); + assert.ok(ok, "the designer never showed its form"); + await sleep(800); + assert.equal(await c.evaluate(HOOK_JS), true); + await messages(); + } + // Opens a file as a double-click in Project Explorer does, without waiting for it to report + // itself open (a read-only designer file never does). + const openNode = (file) => + c.evaluate(`(() => { + const node = fs.tree.resolvePath(${J(`twinbasic:${file}`)}); + if (!node) return false; + openEditors.openFile(node, false, false, false, 1, 1); + return true; + })()`); + async function openDesigner(file) { + assert.ok(await openNode(file), `the project has no file ${file}`); + await waitForDesigner(); + } + // Where a tab's close icon is, or the tab itself when it has none. + const TAB_CLOSE_JS = (name) => `(() => { + const t = [...document.querySelectorAll("#codePanelTabs > .tabItem")].find((t) => t.innerText.trim() === ${J(name)}); + if (!t) return null; + const x = t.querySelector(".codicon-close"); + const r = (x || t).getBoundingClientRect(); + return { x: r.x + r.width / 2, y: r.y + r.height / 2, icon: !!x }; + })()`; + // Closes a tab with its close icon, or a middle click on a tab with none; answers a question + // about unsaved changes with `reply`, and returns the question's buttons, or null when nothing + // was asked. + async function closeTab(name, reply = "Discard Changes") { + const x = await c.evaluate(TAB_CLOSE_JS(name)); + if (!x) return null; + if (x.icon) await clickAt(x.x, x.y); + else { + await moveTo(x.x, x.y); + await c.send("Input.dispatchMouseEvent", { + type: "mousePressed", + x: x.x, + y: x.y, + button: "middle", + clickCount: 1, + }); + await c.send("Input.dispatchMouseEvent", { + type: "mouseReleased", + x: x.x, + y: x.y, + button: "middle", + clickCount: 1, + }); + } + await sleep(800); + const asked = await boxes(); + if (asked.length) await answer(reply); + await sleep(400); + assert.equal(await tab(name), null, `the tab ${name} is still open`); + return asked.length ? asked[0].buttons : null; + } + // The form as its file holds it, in a designer with no undo steps. + async function fresh(file = "frmMain.tbform") { + await closeMenus(); + await closeTab(file); + await openDesigner(SRC(file)); + assert.equal(await zoom(), "1", "the designer did not open at 100 %"); + } + + // ---------------------------------------------------------------- PROPERTIES + + // Types a value into a PROPERTIES box as a person does: a click in the box, End and Backspaces + // to clear it (Ctrl+A does nothing there), the value, Enter. + async function setProp(name, value) { + const r = await c.evaluate(PROP_BOX_JS(name)); + assert.ok(r, `PROPERTIES has no row ${name}`); + await clickAt(r.x, r.y); + await pressKey(c, "End"); + for (let i = 0; i < 30; i++) await pressKey(c, "Backspace"); + await typeText(c, String(value)); + await pressKey(c, "Enter"); + await sleep(800); + } + + // ---------------------------------------------------------------- a project of the lane's + + async function openProject(src, check = null) { + if (c) await lane.closeProject(); + if (check) lane.checkCompile = check; + try { + c = await lane.open(src); + } finally { + delete lane.checkCompile; + } + assert.equal(await c.evaluate(GUARD_JS), true); + } + + before(async () => { + await openProject(PROJECT); + }); + + // ---------------------------------------------------------------- History (bugs/history-form-line-undefined) + + // The tooltip of each row of the HISTORY panel, floated, as the pointer resting on it shows it. + async function historyTooltips() { + await c.evaluate(`executeIdeCommand("tbHistory_ShowPanel")`); + const rows = await waitFor(c, async () => { + const r = await c.evaluate(HISTORY_ROWS_JS); + return r?.length ? r : null; + }); + assert.ok(rows, "the HISTORY panel shows no row"); + const tips = []; + for (const r of rows) { + await moveTo(r.x - 2, r.y); + await moveTo(r.x, r.y); + tips.push( + (await waitFor(c, () => c.evaluate(TOOLTIP_JS), { timeout: 5000, interval: 100 })) ?? + `(no tooltip on ${r.text})`, + ); + await moveTo(2, 2); + await sleep(300); + } + await c.evaluate(`executeIdeCommand("tbHistory_HidePanel")`); + await sleep(500); + return tips; + } + + test("the History entry for a change in a designer reads [line: undefined] in its tooltip", async (t) => { + await fresh(); + await clickControl("Command1"); + await pressKey(c, "ArrowRight"); + await sleep(1200); + const tips = await historyTooltips(); + t.diagnostic(J(tips)); + assert.ok( + tips.some((tip) => tip.startsWith(`${SRC("frmMain.tbform")} [line: undefined]`)), + "no tooltip reads frmMain.tbform [line: undefined]", + ); + }); + + // The IDE adds an entry for an edit in a code file only inside a procedure: here Sub Main, on the + // line `frmMain.Show`, at whose end a comment is typed. + test("control: the History entry for an edit in a procedure names the line in its tooltip", async (t) => { + const line = 5; + await openFile(c, SRC("Startup.twin"), { line, column: 1 }); + await setCursor(c, line, (await c.evaluate(`editor.getModel().getLineMaxColumn(${line})`)) || 1); + await typeText(c, " 'x"); + await sleep(2500); + const tips = await historyTooltips(); + t.diagnostic(J(tips)); + assert.ok( + tips.some((tip) => tip.startsWith(`${SRC("Startup.twin")} [line: ${line}]`)), + `no tooltip reads Startup.twin [line: ${line}]`, + ); + }); + + // ---------------------------------------------------------------- Align > Center (bugs/form-align-center-edge) + + // Command1 10,50 100x30; Check1 140,55 100x24; Command2(0) 250,110 100x30. + test("control: Align > Left puts the selected controls' left edges on the leftmost one", async () => { + await fresh(); + await select(...THREE); + await choose("Format", "Align", "Left"); + const s = await surface(); + for (const id of THREE) assert.ok(near(s[id].L, 10), `${id} is at Left ${s[id].L}`); + }); + + test("Align > Center (Horizontal) centres every selected control on the leftmost left edge", async (t) => { + await fresh(); + await select(...THREE); + await choose("Format", "Align", "Center (Horizontal)"); + const s = pick(await surface(), THREE); + t.diagnostic(J(s)); + // The selection spans 10 to 350, whose centre is 180: each would be at Left 130. + for (const id of THREE) assert.ok(near(s[id].L + s[id].W / 2, 10), `${id}'s centre is at ${s[id].L + s[id].W / 2}`); + }); + + test("Align > Center (Vertical) centres every selected control on the topmost top edge", async (t) => { + await fresh(); + await select(...THREE); + await choose("Format", "Align", "Center (Vertical)"); + const s = pick(await surface(), THREE); + t.diagnostic(J(s)); + // The selection spans 50 to 140, whose centre is 95. + for (const id of THREE) assert.ok(near(s[id].T + s[id].H / 2, 50), `${id}'s centre is at ${s[id].T + s[id].H / 2}`); + }); + + // ---------------------------------------------------------------- Center In Container (bugs/form-center-in-container-zoom) + + // Frame1 is 200 x 130; Text2 in it is 120 x 24 at 20,30. Centred, it is at Left 40, Top 53. + test("control: at 100 %, Center In Container centres a control in its Frame", async (t) => { + await fresh(); + await select("Text2"); + await choose("Format", "Center In Container (Horizontally)"); + await select("Text2"); + await choose("Format", "Center In Container (Vertically)"); + const s = (await surface()).Text2; + t.diagnostic(J(s)); + assert.ok(near(s.L, 40) && near(s.T, 53), `Text2 is at ${s.L},${s.T}`); + }); + + test("at 50 %, Center In Container puts a control in a Frame off centre", async (t) => { + await fresh(); + await clickForm(); + await choose("View", "Zoom Out"); + await choose("View", "Zoom Out"); + assert.equal(await zoom(), "0.5"); + await select("Text2"); + await choose("Format", "Center In Container (Horizontally)"); + await select("Text2"); + await choose("Format", "Center In Container (Vertically)"); + const s = (await surface()).Text2; + t.diagnostic(J(s)); + await clickForm(); + await choose("View", "Zoom In"); + await choose("View", "Zoom In"); + assert.equal(await zoom(), "1"); + assert.ok(near(s.L, -10) && near(s.T, 20.5), `Text2 is at ${s.L},${s.T}, not at 40,53 and not at -10,20.5`); + }); + + // ---------------------------------------------------------------- Spacing (bugs/form-spacing-not-recorded) + + test("control: Align > Left sends a change, makes an undo step, marks the form changed and reaches the form's state", async () => { + await fresh(); + await select(...THREE); + await choose("Format", "Align", "Left"); + await sleep(1500); + assert.deepEqual(await messages(), ["onChanged", "onSignificantChange"]); + assert.equal((await tab("frmMain.tbform")).dirty, true); + assert.ok(near((await formState()).controls.Check1.L, 10)); + await pressKey(c, "z", { ctrl: true }); + await sleep(800); + assert.ok(near((await surface()).Check1.L, 140), "Ctrl+Z did not undo it"); + }); + + test("Horizontal Spacing > Remove moves the controls but sends no change, makes no undo step and leaves the form's state and the tab as they were", async (t) => { + await fresh(); + await select(...THREE); + await choose("Format", "Horizontal Spacing", "Remove"); + await sleep(1500); + const moved = pick(await surface(), THREE); + const state = pick((await formState()).controls, THREE); + t.diagnostic(J({ moved, state })); + assert.ok(near(moved.Check1.L, 110) && near(moved["Command2(0)"].L, 210), "the controls did not move"); + assert.ok(near(state.Check1.L, 140) && near(state["Command2(0)"].L, 250), "the form's state moved"); + assert.deepEqual(await messages(), []); + assert.equal((await tab("frmMain.tbform")).dirty, false); + await pressKey(c, "z", { ctrl: true }); + await sleep(800); + assert.ok(near((await surface()).Check1.L, 110), "Ctrl+Z undid the move"); + }); + + test("after Vertical Spacing > Remove, closing the designer asks nothing, and the form opens again with the old positions", async (t) => { + await fresh(); + await select(...THREE); + await choose("Format", "Vertical Spacing", "Remove"); + await sleep(1500); + const moved = pick(await surface(), THREE); + t.diagnostic(J(moved)); + assert.ok(near(moved.Check1.T, 80) && near(moved["Command2(0)"].T, 104), "the controls did not move"); + assert.equal(await closeTab("frmMain.tbform"), null, "closing asked about saving"); + await openDesigner(SRC("frmMain.tbform")); + const s = await surface(); + assert.ok( + near(s.Check1.T, 55) && near(s["Command2(0)"].T, 110), + `reopened at ${s.Check1.T} and ${s["Command2(0)"].T}`, + ); + }); + + // ---------------------------------------------------------------- Make Same Size (bugs/form-same-size-not-saved) + + test("Make Same Size > Height (Shortest) marks the form changed, but the form's state keeps the old heights, and Save writes them", async (t) => { + await fresh("frmSizes.tbform"); + await select(...THREE); + await choose("Format", "Make Same Size", "Height (Shortest)"); + await sleep(1500); + const made = pick(await surface(), THREE); + const state = pick((await formState()).controls, THREE); + t.diagnostic(J({ made, state })); + assert.ok(near(made.Command1.H, 24), "Command1 was not made 24 high"); + assert.equal((await tab("frmSizes.tbform")).dirty, true); + assert.ok(near(state.Command1.H, 30), "the form's state has the new height"); + // Straight after the menu: a click on the form first would itself save the new heights. + await pressKey(c, "s", { ctrl: true }); + await sleep(1500); + t.diagnostic(`asked on close: ${J(await closeTab("frmSizes.tbform"))}`); + await openDesigner(SRC("frmSizes.tbform")); + const s = await surface(); + assert.ok(near(s.Command1.H, 30) && near(s["Command2(0)"].H, 30), `reopened ${s.Command1.H} high`); + }); + + test("control: Make Same Size followed by another change is saved", async () => { + await fresh("frmSaved.tbform"); + await select(...THREE); + await choose("Format", "Make Same Size", "Height (Shortest)"); + await clickControl("Label1"); + await pressKey(c, "ArrowDown"); + await sleep(1200); + await pressKey(c, "s", { ctrl: true }); + await sleep(1500); + await closeTab("frmSaved.tbform", "Save"); + await openDesigner(SRC("frmSaved.tbform")); + const s = await surface(); + assert.ok(near(s.Command1.H, 24) && near(s["Command2(0)"].H, 24), `reopened ${s.Command1.H} high`); + }); + + // ---------------------------------------------------------------- Lock Controls (bugs/form-lock-not-saved) + + test("Lock Controls on its own does not mark the form changed: closing asks nothing, and the lock is gone when the form is opened again", async (t) => { + await fresh(); + await clickControl("Command1"); + await choose("Format", "Lock Controls"); + await sleep(1000); + assert.deepEqual(await c.evaluate(`${G}.lockedControls`), ["Command1"]); + assert.equal((await tab("frmMain.tbform")).dirty, false); + assert.deepEqual((await formState()).locked, []); + t.diagnostic(`messages: ${J(await messages())}`); + assert.equal(await closeTab("frmMain.tbform"), null, "closing asked about saving"); + await openDesigner(SRC("frmMain.tbform")); + assert.deepEqual(await c.evaluate(`${G}.lockedControls`), []); + }); + + test("control: Lock Controls followed by another change is saved", async () => { + await fresh("frmSaved.tbform"); + await clickControl("Command1"); + await choose("Format", "Lock Controls"); + await clickControl("Label1"); + await pressKey(c, "ArrowDown"); + await sleep(1200); + await pressKey(c, "s", { ctrl: true }); + await sleep(1500); + await closeTab("frmSaved.tbform", "Save"); + await openDesigner(SRC("frmSaved.tbform")); + assert.deepEqual(await c.evaluate(`${G}.lockedControls`), ["Command1"]); + }); + + // ---------------------------------------------------------------- the zoom box (bugs/form-zoom-box-arrows-move) + + test("Up Arrow in the zoom box zooms out and also moves the selected control up, as a change to the form", async (t) => { + await fresh(); + await clickControl("Command1"); + await c.evaluate("zoomSelect.focus()"); + await pressKey(c, "ArrowUp"); + await pressKey(c, "ArrowUp"); + await sleep(1500); + const z = await zoom(); + const s = (await surface()).Command1; + const state = (await formState()).controls.Command1; + const sent = await messages(); + t.diagnostic(J({ zoom: z, surface: s, state, sent })); + await clickForm(); + await choose("View", "Zoom In"); + await choose("View", "Zoom In"); + assert.equal(z, "0.5"); + assert.ok(s.T < 50 && state.T < 50, `Command1 is at Top ${s.T}`); + assert.equal((await tab("frmMain.tbform")).dirty, true); + assert.equal(sent.filter((m) => m === "onSignificantChange").length, 2); + }); + + // ---------------------------------------------------------------- Zoom Out (bugs/form-zoom-out-floor) + + test("View > Zoom Out goes 100 %, 75 %, 50 % and then stays at 50 %, though the zoom box offers 25 %", async () => { + await fresh(); + await clickForm(); + const steps = []; + for (let i = 0; i < 4; i++) { + await choose("View", "Zoom Out"); + steps.push(await zoom()); + } + const offered = await c.evaluate("[...zoomSelect.options].map((o) => o.value)"); + await choose("View", "Zoom In"); + await choose("View", "Zoom In"); + assert.ok(offered.includes("0.25"), `the box offers ${J(offered)}`); + assert.deepEqual(steps, ["0.75", "0.5", "0.5", "0.5"]); + }); + + test("control: View > Zoom In goes up one step from 50 %", async () => { + await fresh(); + await clickForm(); + await choose("View", "Zoom Out"); + await choose("View", "Zoom Out"); + assert.equal(await zoom(), "0.5"); + await choose("View", "Zoom In"); + assert.equal(await zoom(), "0.75"); + await choose("View", "Zoom In"); + assert.equal(await zoom(), "1"); + }); + + // ---------------------------------------------------------------- Escape (bugs/form-escape-marks-changed) + + test("control: a click on one control and then on another leaves the form unchanged", async () => { + await fresh(); + await clickControl("Command1"); + await clickControl("Text1"); + await sleep(1000); + assert.deepEqual(await messages(), []); + assert.equal((await tab("frmMain.tbform")).dirty, false); + }); + + // Not every try does it: in the lane's first four runs, two tries in seven did not, with the + // designer focused and no menu open. Each try is in a freshly opened designer, and a test passes + // on the first try that does it, out of TRIES; all five miss it about once in five hundred runs. + const TRIES = 5; + async function escapeThenMove() { + await clickControl("Command1"); + await pressKey(c, "Escape"); + await sleep(600); + assert.deepEqual(await selected(), ["(form)"]); + const r = await c.evaluate( + `(() => { const r = ${G}.designer.getBoundingClientRect(); return { x: r.right - 100, y: r.bottom - 50 }; })()`, + ); + await moveTo(r.x, r.y); + await sleep(1000); + } + + // What the surface shows is compared, not the form's state: the change writes the surface into + // the state, so a position the surface rounds to a device pixel (Check1's Top 55, shown 54.67) + // changes there. + test("Escape, which selects the form, and then a pointer move over it mark the form changed with no control moved", async (t) => { + const tries = []; + for (let i = 0; i < TRIES; i++) { + await fresh(); + const before = await surface(); + const stateBefore = (await formState()).controls; + await escapeThenMove(); + const sent = await messages(); + const dirty = (await tab("frmMain.tbform")).dirty; + const same = J(await surface()) === J(before); + const stateAfter = (await formState()).controls; + const rewritten = Object.keys(stateAfter).filter((id) => J(stateAfter[id]) !== J(stateBefore[id])); + tries.push({ + sent, + dirty, + same, + rewritten: Object.fromEntries(rewritten.map((id) => [id, [stateBefore[id], stateAfter[id]]])), + }); + if (dirty && sent.includes("onSignificantChange") && same) break; + } + t.diagnostic(J(tries)); + assert.ok( + tries.some((x) => x.dirty && x.sent.includes("onSignificantChange") && x.same), + "no try marked the form changed", + ); + }); + + test("the change that Escape and a pointer move make is an undo step that changes nothing", async (t) => { + const tries = []; + for (let i = 0; i < TRIES; i++) { + await fresh(); + await clickControl("Command1"); + await pressKey(c, "ArrowRight"); + await sleep(800); + const moved = (await surface()).Command1.L; + await messages(); + await escapeThenMove(); + const sent = await messages(); + // The designer still has the focus: a click on the form here could make a step of its own. + await pressKey(c, "z", { ctrl: true }); + await sleep(800); + const undone = (await surface()).Command1.L; + tries.push({ moved, sent, undone }); + if (sent.includes("onSignificantChange") && near(undone, moved, 0.01)) break; + } + t.diagnostic(J(tries)); + // The arrow press moved Command1 from 10 to 10.67; the first Ctrl+Z leaves it there. + assert.ok( + tries.some((x) => x.moved > 10.5 && x.sent.includes("onSignificantChange") && near(x.undone, x.moved, 0.01)), + "Ctrl+Z undid the arrow press at once", + ); + }); + + // ---------------------------------------------------------------- Ctrl+A (bugs/properties-ctrl-a) + + test("control: Ctrl+A with the designer focused selects every control", async () => { + await fresh(); + await clickForm(); + await pressKey(c, "a", { ctrl: true }); + await sleep(600); + assert.deepEqual([...(await selected())].sort(), [ + "Check1", + "Command1", + "Command2(0)", + "Command2(1)", + "Frame1", + "Label1", + "Text1", + "Text2", + ]); + }); + + test("Ctrl+A in a PROPERTIES box selects nothing", async () => { + await fresh(); + await clickControl("Command1"); + const r = await c.evaluate(PROP_BOX_JS("Caption")); + await clickAt(r.x, r.y); + await pressKey(c, "End"); + await pressKey(c, "a", { ctrl: true }); + await sleep(400); + const box = await c.evaluate( + `(() => { const a = document.activeElement; return a && "selectionStart" in a ? { value: a.value, start: a.selectionStart, end: a.selectionEnd } : null; })()`, + ); + await pressKey(c, "Escape"); + assert.deepEqual(box, { value: "Command1", start: 8, end: 8 }); + assert.deepEqual(await selected(), ["Command1"]); + }); + + // ---------------------------------------------------------------- Alt+arrow (bugs/form-alt-arrow-menu) + + test("control: Alt+Left aligns the selected controls' left edges", async () => { + await fresh(); + await select("Command1", "Check1"); + await pressKey(c, "ArrowLeft", { alt: true }); + await sleep(900); + await closeMenus(); + assert.ok(near((await surface()).Check1.L, 10)); + }); + + test("Alt+Left in the designer also opens the IDE's menu bar", async () => { + await fresh(); + await select("Command1", "Check1"); + await pressKey(c, "ArrowLeft", { alt: true }); + await sleep(900); + const open = await menuOpen(); + await closeMenus(); + assert.equal(open, true); + }); + + // ---------------------------------------------------------------- the control-array question (bugs/form-array-question-twice) + + const QUESTION = (name) => `A control already exists with the name '${name}'. Do you want to create a control array?`; + + test("control: renaming a control to another's name and answering NO asks once and keeps the name", async () => { + await fresh(); + await clickControl("Check1"); + await setProp("Name", "Text1"); + const asked = await boxes(); + assert.equal(asked.length, 1); + assert.ok(asked[0].text.includes(QUESTION("Text1")), asked[0].text); + await answer("NO"); + await sleep(800); + assert.deepEqual(await boxes(), []); + assert.equal(await prop("Name"), "Check1"); + }); + + test("renaming Label1 to Check1 asks the control-array question twice: the first YES makes Check1 an array, the second renames Label1", async (t) => { + await fresh(); + await clickControl("Label1"); + await setProp("Name", "Check1"); + assert.ok((await boxes())[0]?.text.includes(QUESTION("Check1")), "no question"); + await answer("YES"); + await sleep(1500); + const between = { boxes: (await boxes()).map((b) => b.text), names: Object.keys(await surface()) }; + t.diagnostic(J(between)); + assert.ok(between.boxes[0]?.includes(QUESTION("Check1")), "the question was not asked again"); + assert.ok(between.names.includes("Check1(0)") && between.names.includes("Label1")); + await answer("YES"); + await sleep(1500); + const names = Object.keys(await surface()); + assert.ok(names.includes("Check1(1)") && !names.includes("Label1"), J(names)); + }); + + // ---------------------------------------------------------------- units: the control for bugs/customcontrols-properties-twips + + test("control: on a form of the VB package, PROPERTIES shows a control's Left in twips and takes a typed value in twips", async () => { + await fresh(); + await clickControl("Command1"); + assert.equal(await prop("Left"), "150"); + await setProp("Left", "450"); + assert.ok(near((await surface()).Command1.L, 30)); + assert.equal(await prop("Left"), "450"); + }); + + // ---------------------------------------------------------------- Shift+F7 (bugs/form-switch-twin-folder) + + const NOT_A_FORM = "Unable to switch form design mode: this document is not part of a form"; + const selectedTab = async () => (await tabs()).find((x) => x.selected)?.name ?? null; + + test("Shift+F7 in the code of a form in the folder v1.twin.forms says the document is not part of a form", async () => { + await openFile(c, SRC("v1.twin.forms/frmDeep.twin"), { line: 4, column: 1 }); + await setCursor(c, 4, 1); + await pressKey(c, "F7", { shift: true }); + assert.ok(await waitFor(c, async () => (await notifications(c)).includes(NOT_A_FORM)), "no notification"); + assert.equal(await selectedTab(), "frmDeep.twin"); + assert.equal(await tab("frmDeep.tbform"), null); + }); + + // From a designer, the toolbar's Switch Between Form And Code button's command. + test("control: Shift+F7 switches from a form's code to its designer, and the switch from frmDeep's designer opens its code", async () => { + await openFile(c, SRC("frmMain.twin"), { line: 4, column: 1 }); + await setCursor(c, 4, 1); + await pressKey(c, "F7", { shift: true }); + assert.ok( + await waitFor(c, async () => (await selectedTab()) === "frmMain.tbform"), + "frmMain's designer did not open", + ); + await closeTab("frmDeep.twin"); + await openDesigner(SRC("v1.twin.forms/frmDeep.tbform")); + await c.evaluate(`executeIdeCommand("tbEditor_SwitchToggleFormDesignCode")`); + assert.ok(await waitFor(c, async () => (await selectedTab()) === "frmDeep.twin"), "frmDeep's code did not open"); + }); + + // ---------------------------------------------------------------- a package's designer file (bugs/readonly-designer-empty-tab) + + test("a package's read-only designer file opens an empty tab of its name besides the notice that it cannot be opened", async (t) => { + const file = "/FormsProbe/Packages/VB/Sources/DESIGNERS/tbObjectDesigner_PROPERTYPAGE.tbppage"; + assert.ok(await openNode(file), `the project has no file ${file}`); + const notes = await waitFor(c, async () => { + const n = await notifications(c); + return n.includes("Can't open designer for a read-only document.") ? n : null; + }); + assert.ok(notes, "no notification"); + await sleep(1000); + const name = "tbObjectDesigner_PROPERTYPAGE.tbppage"; + const opened = await tab(name); + const close = await c.evaluate(TAB_CLOSE_JS(name)); + t.diagnostic(J({ opened, close })); + assert.equal(opened?.selected, true); + // The tab has no close icon; a middle click closes it. + assert.equal(close.icon, false); + await closeTab(name); + }); + + // ---------------------------------------------------------------- Add (bugs/add-form-bad-name) + + // Add Windows Form, as the Project Explorer's Add menu does; the name box offers MyForm.tbform + // with MyForm selected, and the form goes into Sources. + async function addForm(name) { + await closeMenus(); + await c.evaluate(`executeIdeCommand("tbFileSystem_AddWindowsForm")`); + const offered = await waitFor(c, () => + c.evaluate( + `(() => { const a = document.activeElement; return a && a.tagName === "INPUT" && /\\.tbform$/.test(a.value) ? a.value : null; })()`, + ), + ); + assert.ok(offered, "no name box"); + await typeText(c, name); + await pressKey(c, "Enter"); + assert.ok(await waitFor(c, () => tab(`${name}.tbform`), { timeout: 20 * 1000 }), `no tab ${name}.tbform`); + await waitForDesigner(); + } + + // The folder Add puts a file in follows what Project Explorer has selected, so the file is + // looked for in the whole project. + async function codeOf(fileName) { + const found = await c.evaluate(`(() => { + const out = []; + const walk = (n, p) => { for (const v of Object.values(n.entries || {})) { const q = p + "/" + v.name; if (v.name === ${J(fileName)}) out.push(q); walk(v, q); } }; + walk(fs.tree.resolvePath("twinbasic:/FormsProbe"), "/FormsProbe"); + return out; + })()`); + assert.equal(found.length, 1, `${fileName} is at ${J(found)}`); + await openFile(c, found[0]); + return { file: found[0], code: await editorText(c) }; + } + + test("control: Add Windows Form named My Form writes Class [My Form]", async (t) => { + await addForm("My Form"); + const { file, code } = await codeOf("My Form.twin"); + t.diagnostic(file); + assert.match(code, /^Class \[My Form\]$/m); + }); + + test("Add Windows Form accepts the name 1bad, which is not an identifier, and writes Class 1bad, which does not compile", async (t) => { + await addForm("1bad"); + assert.deepEqual(await boxes(), []); + const { file, code } = await codeOf("1bad.twin"); + assert.match(code, /^Class 1bad$/m); + const outcome = compileOutcome(await waitForCompile(c, { project: lane.project, timeout: 60 * 1000 }), { + name: "the forms probe", + }); + t.diagnostic(J(outcome.rows ?? outcome.message)); + assert.ok(outcome.ok, outcome.message); + assert.deepEqual(outcome.rows, [`{ERROR} ${file} [4,7]: TB5174 Invalid number format`]); + }); + + // ---------------------------------------------------------------- a report (bugs/report-floating-toolbox) + + test("control: with a report open, the docked Toolbox lists the report's seven tools", async () => { + await closeMenus(); + await c.evaluate(`executeIdeCommand("tbFileSystem_AddWindowsReport")`); + assert.ok( + await waitFor(c, () => + c.evaluate( + `(() => { const a = document.activeElement; return a && a.tagName === "INPUT" && /\\.tbreport$/.test(a.value); })()`, + ), + ), + "no name box", + ); + await pressKey(c, "Enter"); + assert.ok(await waitFor(c, () => tab("MyReport.tbreport"), { timeout: 20 * 1000 }), "no tab MyReport.tbreport"); + await waitForDesigner(); + const docked = (await c.evaluate(TOOLS_JS("#bodyInner"))).map((x) => x.name); + assert.deepEqual(docked, ["Pointer", "Label", "Shape", "Line", "Image", "CheckMark", "QRCode"]); + }); + + test("with a report open, a floating Toolbox lists every tool of a form", async (t) => { + await c.evaluate(`executeIdeCommand("tbToolbox_HidePanel")`); + await sleep(800); + await c.evaluate(`executeIdeCommand("tbToolbox_ShowPanel")`); + await sleep(1500); + const floating = (await c.evaluate(TOOLS_JS('.floatingPanel[id="TOOLBOX"]'))).map((x) => x.name); + t.diagnostic(J(floating)); + assert.equal(await selectedTab(), "MyReport.tbreport"); + for (const tool of ["TextBox", "CommandButton", "Frame", "Timer", "MultiFrame"]) { + assert.ok(floating.includes(tool), `the floating Toolbox has no ${tool}`); + } + assert.equal(floating.filter((x) => x !== "Add component...").length, 24); + }); + + // ---------------------------------------------------------------- bad JSON (bugs/form-bad-json-empty-designer) + + // The project does not compile, on purpose: its only errors are the two frmBad gives. + function badJsonCompiles(waited) { + const outcome = compileOutcome(waited, { name: "probes/forms-badjson" }); + if (!outcome.ok) throw new Error(`probes/forms-badjson: ${outcome.message}`); + const codes = outcome.rows.map((r) => /TB\d+/.exec(r)?.[0]).sort(); + if (J(codes) !== J(["TB5246", "TB5247"])) throw new Error(`probes/forms-badjson: ${outcome.rows.join("; ")}`); + } + + test("a form file with bad JSON opens an empty designer that adds no control, and Save leaves the file as it was", async (t) => { + await openProject(BAD, badJsonCompiles); + const file = "/FormsBadJson/Sources/frmBad.tbform"; + assert.ok(await openNode(file)); + const notes = await waitFor(c, async () => { + const n = await notifications(c); + return n.includes("ERROR: bad JSON encountered when parsing tbform") ? n : null; + }); + assert.ok(notes, "no notification"); + await sleep(2000); + assert.equal((await tab("frmBad.tbform"))?.selected, true); + assert.deepEqual(await surface(), {}); + assert.deepEqual(await props(), []); + const tool = (await c.evaluate(TOOLS_JS("#bodyInner"))).find((x) => x.name === "CommandButton"); + assert.ok(tool, "the Toolbox has no CommandButton"); + await doubleClickAt(tool.x, tool.y); + await sleep(1500); + assert.deepEqual(await surface(), {}); + assert.equal((await tab("frmBad.tbform")).dirty, false); + await pressKey(c, "s", { ctrl: true }); + await sleep(2500); + const out = path.join(lane.work, "forms-badjson-saved"); + unpackProject(lane.project, out); + const flat = (s) => s.replace(/\s+/g, ""); + const saved = readFileSync(path.join(out, "Sources", "frmBad.tbform"), "utf8"); + t.diagnostic(saved.slice(0, 120)); + assert.equal(flat(saved), flat(readFileSync(path.join(BAD, "Sources", "frmBad.tbform"), "utf8"))); + }); + + // ---------------------------------------------------------------- CustomControls (bugs/customcontrols-properties-twips) + + test("on a CustomControls form, PROPERTIES shows a control's Left in twips but takes a typed value in pixels", async () => { + await openProject(CUSTOM); + await openDesigner("/CustomControlsDemo/Sources/frmCustom.tbform"); + await clickControl("MyButton1"); + assert.ok(near((await surface()).MyButton1.L, 20)); + assert.equal(await prop("Left"), "300"); + await setProp("Left", "40"); + assert.ok(near((await surface()).MyButton1.L, 40), "the typed 40 was not taken as pixels"); + assert.equal(await prop("Left"), "600"); + }); +}); diff --git a/test/ide/lanes.mjs b/test/ide/lanes.mjs index 5e06bde49..d7d347719 100644 --- a/test/ide/lanes.mjs +++ b/test/ide/lanes.mjs @@ -34,4 +34,5 @@ export default [ { file: "code-editor.test.mjs" }, { file: "memory.test.mjs" }, { file: "debugging.test.mjs" }, + { file: "forms.test.mjs" }, ]; diff --git a/test/ide/probes/forms-badjson/Settings b/test/ide/probes/forms-badjson/Settings new file mode 100644 index 000000000..2804b5c24 --- /dev/null +++ b/test/ide/probes/forms-badjson/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "FormsBadJson", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "test/ide/forms.test.mjs opens it after the forms probe: frmBad.tbform has a JSON syntax error (a doubled comma), on purpose, so the project does not compile (TB5246 and TB5247).", + "project.exportPathIsV2": true, + "project.id": "{F0A3B7A1-0000-4000-9000-F0A3B7A100B6}", + "project.name": "FormsBadJson", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/test/ide/probes/forms-badjson/Sources/Startup.twin b/test/ide/probes/forms-badjson/Sources/Startup.twin new file mode 100644 index 000000000..b1d77388c --- /dev/null +++ b/test/ide/probes/forms-badjson/Sources/Startup.twin @@ -0,0 +1,7 @@ +' test/ide/forms.test.mjs: frmBad.tbform's JSON has a syntax error on purpose. +Module Startup + + Public Sub Main() + End Sub + +End Module diff --git a/test/ide/probes/forms-badjson/Sources/frmBad.tbform b/test/ide/probes/forms-badjson/Sources/frmBad.tbform new file mode 100644 index 000000000..a7ff8348f --- /dev/null +++ b/test/ide/probes/forms-badjson/Sources/frmBad.tbform @@ -0,0 +1,25 @@ +[ + { + "Caption": "frmBad", + "FormDesignerId": "{F0A3F031-3B2E-4D69-8A14-7E91C2D4C615}", + "Height": 300,, + "Name": "frmBad", + "Width": 300, + "_children": [ + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "KeepMe", + "Left": 20, + "Top": 20, + "Width": 100, + "Height": 30, + "Index": -1, + "TabIndex": 0, + "Caption": "Keep me" + } + ], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/test/ide/probes/forms-badjson/Sources/frmBad.twin b/test/ide/probes/forms-badjson/Sources/frmBad.twin new file mode 100644 index 000000000..5091a8000 --- /dev/null +++ b/test/ide/probes/forms-badjson/Sources/frmBad.twin @@ -0,0 +1,9 @@ +[Description("")] +[FormDesignerId("F0A3F031-3B2E-4D69-8A14-7E91C2D4C615")] +[PredeclaredId] +Class frmBad + + Sub New() + End Sub + +End Class diff --git a/test/ide/probes/forms-custom/Settings b/test/ide/probes/forms-custom/Settings new file mode 100644 index 000000000..52709493f --- /dev/null +++ b/test/ide/probes/forms-custom/Settings @@ -0,0 +1,62 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "CustomControlsDemo", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "A copy of test/shots/customcontrols: two custom controls of its own on a CustomControls form. test/ide/forms.test.mjs opens it last, to read a control's position in PROPERTIES.", + "project.exportPathIsV2": true, + "project.id": "{5C0F8A71-3B2E-4D69-8A14-7E91C2D4C601}", + "project.name": "CustomControlsDemo", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{697A5CBF-2012-48BD-9D2F-3EA64CDE149A}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] Custom Controls Package", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "CustomControlsPackage", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 5, + "overrideSymbol": "*CustomControlsPackage" + }, + { + "id": "{EA18E2B1-80BA-478D-AC7C-E955E6AA1B05}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] CustomControls", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "CustomControls", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 1 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "frmCustom", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/test/ide/probes/forms-custom/Sources/MyButton.twin b/test/ide/probes/forms-custom/Sources/MyButton.twin new file mode 100644 index 000000000..2b845e3bd --- /dev/null +++ b/test/ide/probes/forms-custom/Sources/MyButton.twin @@ -0,0 +1,62 @@ +[ClassId("B4E1D7A2-6C3F-4E58-9A1B-2D7F0C5E8A94")] +Class MyButtonState + Public Corners As CustomControlsPackage.Corners + Public BackgroundFill As CustomControlsPackage.Fill + Public Borders As CustomControlsPackage.Borders + Public TextRendering As CustomControlsPackage.TextRendering + + Public Sub New() + Set Corners = New CustomControlsPackage.Corners + Set BackgroundFill = New CustomControlsPackage.Fill + Set Borders = New CustomControlsPackage.Borders + Set TextRendering = New CustomControlsPackage.TextRendering + End Sub +End Class + +[CustomControl("/Miscellaneous/MyButton.png")] +[ClassId("E7A3C915-2B6D-4F80-8D4E-1C9B6A2F7D35")] +[COMCreatable(False)] +Class MyButton + Implements CustomControls.ICustomControl + + Event Click() + Event GotFocus() + Event LostFocus() + + Public Name As String + Public Left As CustomControls.PixelCount + Public Top As CustomControls.PixelCount + Public Width As CustomControls.PixelCount + Public Height As CustomControls.PixelCount + Public Anchors As CustomControlsPackage.Anchors = New CustomControlsPackage.Anchors + Public Dock As CustomControls.DockMode + Public Visible As Boolean + + Public NormalState As MyButtonState = New MyButtonState + + Public Caption As String = "Button" + + Private Sub OnInitialize(ByVal Context As CustomControls.CustomControlContext) _ + Implements CustomControls.ICustomControl.Initialize + If Not Context.GetSerializer.RuntimeUISrzDeserialize(Me, False) Then + Visible = True + NormalState.BackgroundFill.ColorPoints.SetSolidColor RGB(70, 130, 200) + End If + End Sub + + Private Sub OnDestroy() Implements CustomControls.ICustomControl.Destroy + End Sub + + Private Sub OnPaint(ByVal Canvas As CustomControls.Canvas) _ + Implements CustomControls.ICustomControl.Paint + Dim Face As CustomControlsPackage.ElementDescriptor + Face.Width = Canvas.RuntimeUICCGetWidth() + Face.Height = Canvas.RuntimeUICCGetHeight() + Set Face.BackgroundFill = NormalState.BackgroundFill + Set Face.Corners = NormalState.Corners + Set Face.Borders = NormalState.Borders + Face.Text = Caption + Set Face.TextRenderingOptions = NormalState.TextRendering + Canvas.RuntimeUICCCanvasAddElement Face + End Sub +End Class diff --git a/test/ide/probes/forms-custom/Sources/MyGrid.twin b/test/ide/probes/forms-custom/Sources/MyGrid.twin new file mode 100644 index 000000000..a1b65583a --- /dev/null +++ b/test/ide/probes/forms-custom/Sources/MyGrid.twin @@ -0,0 +1,56 @@ +[ClassId("3F6B2C1A-8E4D-4B7A-9C25-6A1D0E7F4B21")] +Class GridColumn + Public Caption As String = "Column" + Public Width As CustomControls.PixelCount = 100 +End Class + +[CustomControl("/Miscellaneous/MyGrid.png")] +[ClassId("8C2E5A47-1D3B-4F69-A0B8-5E7C9D2F1A63")] +[COMCreatable(False)] +Class MyGrid + Implements CustomControls.ICustomControl + + Public Name As String + Public Left As CustomControls.PixelCount + Public Top As CustomControls.PixelCount + Public Width As CustomControls.PixelCount + Public Height As CustomControls.PixelCount + Public Anchors As CustomControlsPackage.Anchors = New CustomControlsPackage.Anchors + Public Dock As CustomControls.DockMode + Public Visible As Boolean + + Public MyField As Long + + Enum MyEnum + Value1 + Value2 + Value3 + End Enum + + Public MyEnumField As MyEnum + + Public Columns() As GridColumn + + Private Background As CustomControlsPackage.Fill + + Private Sub OnInitialize(ByVal Context As CustomControls.CustomControlContext) _ + Implements CustomControls.ICustomControl.Initialize + If Not Context.GetSerializer.RuntimeUISrzDeserialize(Me, False) Then + Visible = True + End If + Set Background = New CustomControlsPackage.Fill + Background.ColorPoints.SetSolidColor RGB(220, 230, 240) + End Sub + + Private Sub OnDestroy() Implements CustomControls.ICustomControl.Destroy + End Sub + + Private Sub OnPaint(ByVal Canvas As CustomControls.Canvas) _ + Implements CustomControls.ICustomControl.Paint + Dim Area As CustomControlsPackage.ElementDescriptor + Area.Width = Canvas.RuntimeUICCGetWidth() + Area.Height = Canvas.RuntimeUICCGetHeight() + Set Area.BackgroundFill = Background + Canvas.RuntimeUICCCanvasAddElement Area + End Sub +End Class diff --git a/test/ide/probes/forms-custom/Sources/MyGridProperty.twin b/test/ide/probes/forms-custom/Sources/MyGridProperty.twin new file mode 100644 index 000000000..df8c563e8 --- /dev/null +++ b/test/ide/probes/forms-custom/Sources/MyGridProperty.twin @@ -0,0 +1,16 @@ +Class MyGridProperty + + Private ControlContext As CustomControls.CustomControlContext + + Private _MyField As Long ' private backing field + + Public Property Get MyField() As Long + Return _MyField + End Property + + Public Property Let MyField(Value As Long) + _MyField = Value + ControlContext.Repaint + End Property + +End Class diff --git a/test/ide/probes/forms-custom/Sources/frmCustom.tbform b/test/ide/probes/forms-custom/Sources/frmCustom.tbform new file mode 100644 index 000000000..e30e51b7f --- /dev/null +++ b/test/ide/probes/forms-custom/Sources/frmCustom.tbform @@ -0,0 +1,103 @@ +[ + { + "BackgroundFill": { + "ColorPoints": { + "Granularity": 100, + "Values": [ + { + "Color": -1710618, + "PositionPercent": 0, + "_className": "FillColorPoint", + "_clsid": "{089D4C70-CF05-432E-ADFF-8E72CD22FFA8}" + } + ], + "_className": "FillColorPoints", + "_clsid": "{DE27CE92-3E3D-4738-8688-CFB118F87D88}" + }, + "Pattern": "tbGradientNorthToSouth", + "_className": "Fill", + "_clsid": "{CA65F55C-1A19-4003-A6F9-D89A7D84F5EA}" + }, + "Caption": "frmCustom", + "FormDesignerId": "{2A7C4E91-5B3D-4F18-9E6A-0D8B1C3F5E72}", + "Height": 320, + "Left": 0, + "Name": "frmCustom", + "Top": 0, + "Width": 480, + "WindowsOptions": { + "BorderStyle": "tbFixedSingle", + "ControlBox": true, + "MaximizeButton": true, + "MinimizeButton": true, + "ShowInTaskbar": true, + "StartUpPosition": "tbStartUpCenterScreen", + "WindowState": "tbNormal", + "_className": "WindowsFormOptions", + "_clsid": "{D3086FE1-A140-4DC6-85A3-03E8B94DC6E3}" + }, + "_children": [ + { + "Anchors": { + "Bottom": false, + "Left": true, + "Right": false, + "Top": true, + "_className": "Anchors", + "_clsid": "{8524D4B5-72A9-40A9-A189-29E7905C40CA}" + }, + "Columns": [ + { + "Caption": "Name", + "Width": 120, + "_className": "GridColumn", + "_clsid": "{3F6B2C1A-8E4D-4B7A-9C25-6A1D0E7F4B21}" + }, + { + "Caption": "Price", + "Width": 80, + "_className": "GridColumn", + "_clsid": "{3F6B2C1A-8E4D-4B7A-9C25-6A1D0E7F4B21}" + }, + { + "Caption": "Stock", + "Width": 60, + "_className": "GridColumn", + "_clsid": "{3F6B2C1A-8E4D-4B7A-9C25-6A1D0E7F4B21}" + } + ], + "Dock": "tbDockNone", + "Height": 160, + "Left": 20, + "Name": "MyGrid1", + "Top": 20, + "Visible": true, + "Width": 300, + "_className": "MyGrid", + "_clsid": "{8C2E5A47-1D3B-4F69-A0B8-5E7C9D2F1A63}" + }, + { + "Anchors": { + "Bottom": false, + "Left": true, + "Right": false, + "Top": true, + "_className": "Anchors", + "_clsid": "{8524D4B5-72A9-40A9-A189-29E7905C40CA}" + }, + "Caption": "Button", + "Dock": "tbDockNone", + "Height": 32, + "Left": 20, + "Name": "MyButton1", + "Top": 200, + "Visible": true, + "Width": 120, + "_className": "MyButton", + "_clsid": "{E7A3C915-2B6D-4F80-8D4E-1C9B6A2F7D35}" + } + ], + "_className": "WaynesForm", + "_clsid": "{18C42F40-B03D-474E-B99E-7853A314E664}" + } +] diff --git a/test/ide/probes/forms-custom/Sources/frmCustom.twin b/test/ide/probes/forms-custom/Sources/frmCustom.twin new file mode 100644 index 000000000..cd9569bad --- /dev/null +++ b/test/ide/probes/forms-custom/Sources/frmCustom.twin @@ -0,0 +1,5 @@ +[Description("")] +[FormDesignerId("2A7C4E91-5B3D-4F18-9E6A-0D8B1C3F5E72")] +Class frmCustom + +End Class diff --git a/test/ide/probes/forms-custom/Sources/frmCustomJson.tbform b/test/ide/probes/forms-custom/Sources/frmCustomJson.tbform new file mode 100644 index 000000000..1d0a53431 --- /dev/null +++ b/test/ide/probes/forms-custom/Sources/frmCustomJson.tbform @@ -0,0 +1,64 @@ +[ + { + "BackgroundFill": { + "ColorPoints": { + "Granularity": 100, + "Values": [ + { + "Color": -1710618, + "PositionPercent": 0, + "_className": "FillColorPoint", + "_clsid": "{089D4C70-CF05-432E-ADFF-8E72CD22FFA8}" + } + ], + "_className": "FillColorPoints", + "_clsid": "{DE27CE92-3E3D-4738-8688-CFB118F87D88}" + }, + "Pattern": "tbGradientNorthToSouth", + "_className": "Fill", + "_clsid": "{CA65F55C-1A19-4003-A6F9-D89A7D84F5EA}" + }, + "Caption": "frmCustomJson", + "FormDesignerId": "{7D3E1A64-9C20-4B85-A6F1-3E58B2C09D47}", + "Height": 320, + "Left": 0, + "Name": "frmCustomJson", + "Top": 0, + "Width": 480, + "WindowsOptions": { + "BorderStyle": "tbFixedSingle", + "ControlBox": true, + "MaximizeButton": true, + "MinimizeButton": true, + "ShowInTaskbar": true, + "StartUpPosition": "tbStartUpCenterScreen", + "WindowState": "tbNormal", + "_className": "WindowsFormOptions", + "_clsid": "{D3086FE1-A140-4DC6-85A3-03E8B94DC6E3}" + }, + "_children": [ + { + "Anchors": { + "Bottom": false, + "Left": true, + "Right": false, + "Top": true, + "_className": "Anchors", + "_clsid": "{8524D4B5-72A9-40A9-A189-29E7905C40CA}" + }, + "Dock": "tbDockNone", + "Height": 160, + "Left": 20, + "MyField": 0, + "Name": "MyGrid1", + "Top": 20, + "Visible": true, + "Width": 300, + "_className": "MyGrid", + "_clsid": "{8C2E5A47-1D3B-4F69-A0B8-5E7C9D2F1A63}" + } + ], + "_className": "WaynesForm", + "_clsid": "{18C42F40-B03D-474E-B99E-7853A314E664}" + } +] diff --git a/test/ide/probes/forms-custom/Sources/frmCustomJson.twin b/test/ide/probes/forms-custom/Sources/frmCustomJson.twin new file mode 100644 index 000000000..6922b2381 --- /dev/null +++ b/test/ide/probes/forms-custom/Sources/frmCustomJson.twin @@ -0,0 +1,5 @@ +[Description("")] +[FormDesignerId("7D3E1A64-9C20-4B85-A6F1-3E58B2C09D47")] +Class frmCustomJson + +End Class diff --git a/test/ide/probes/forms/Settings b/test/ide/probes/forms/Settings new file mode 100644 index 000000000..9113cdaca --- /dev/null +++ b/test/ide/probes/forms/Settings @@ -0,0 +1,61 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "FormsProbe", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "test/ide/forms.test.mjs opens it in a lane's copy of the install and works its form designer: the Format menu, the zoom, locks, PROPERTIES, the History panel, the code/design switch, a package's designer file and the Add commands. v1.twin.forms/frmDeep is in a folder whose name holds .twin, on purpose.", + "project.exportPathIsV2": true, + "project.id": "{F0A3B7A1-0000-4000-9000-F0A3B7A100B5}", + "project.name": "FormsProbe", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/test/ide/probes/forms/Sources/Startup.twin b/test/ide/probes/forms/Sources/Startup.twin new file mode 100644 index 000000000..e584cf2cb --- /dev/null +++ b/test/ide/probes/forms/Sources/Startup.twin @@ -0,0 +1,8 @@ +' test/ide/forms.test.mjs: Main shows frmMain. +Module Startup + + Public Sub Main() + frmMain.Show + End Sub + +End Module diff --git a/test/ide/probes/forms/Sources/frmMain.tbform b/test/ide/probes/forms/Sources/frmMain.tbform new file mode 100644 index 000000000..bc764d42b --- /dev/null +++ b/test/ide/probes/forms/Sources/frmMain.tbform @@ -0,0 +1,138 @@ +[ + { + "BackColor": -2147483633, + "BorderStyle": "vbSizable", + "Caption": "FormsProbe main", + "FontName": "Segoe UI", + "FontSize": 9, + "FontBold": false, + "FontItalic": false, + "FontStrikethru": false, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{F0A3F001-3B2E-4D69-8A14-7E91C2D4C615}", + "Height": 320, + "Left": 0, + "Name": "frmMain", + "ScaleHeight": 4800, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 6000, + "StartUpPosition": "vbStartUpScreen", + "Top": 0, + "Visible": true, + "Width": 400, + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": true, + "showGrid": true, + "showOutlines": false + }, + "_children": [ + { + "_className": "Label", + "_clsid": "{33AD4ED8-6699-11CF-B70C-00AA0060D393}", + "Name": "Label1", + "Left": 10, + "Top": 10, + "Width": 100, + "Height": 20, + "Index": -1, + "TabIndex": 0, + "Caption": "Label1" + }, + { + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}", + "Name": "Text1", + "Left": 120, + "Top": 10, + "Width": 150, + "Height": 24, + "Index": -1, + "TabIndex": 1, + "Text": "Text1" + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command1", + "Left": 10, + "Top": 50, + "Width": 100, + "Height": 30, + "Index": -1, + "TabIndex": 2, + "Caption": "Command1" + }, + { + "_className": "CheckBox", + "_clsid": "{33AD4EF8-6699-11CF-B70C-00AA0060D393}", + "Name": "Check1", + "Left": 140, + "Top": 55, + "Width": 100, + "Height": 24, + "Index": -1, + "TabIndex": 3, + "Caption": "Check1" + }, + { + "_className": "Frame", + "_clsid": "{33AD4EE8-6699-11CF-B70C-00AA0060D393}", + "Name": "Frame1", + "Left": 10, + "Top": 100, + "Width": 200, + "Height": 130, + "Index": -1, + "TabIndex": 4, + "Caption": "Frame1", + "_children": [ + { + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}", + "Name": "Text2", + "Left": 20, + "Top": 30, + "Width": 120, + "Height": 24, + "Index": -1, + "TabIndex": 5, + "Text": "Text2" + } + ] + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command2", + "Left": 250, + "Top": 110, + "Width": 100, + "Height": 30, + "Index": 0, + "TabIndex": 6, + "Caption": "Command2(0)" + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command2", + "Left": 250, + "Top": 160, + "Width": 100, + "Height": 30, + "Index": 1, + "TabIndex": 7, + "Caption": "Command2(1)" + } + ], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/test/ide/probes/forms/Sources/frmMain.twin b/test/ide/probes/forms/Sources/frmMain.twin new file mode 100644 index 000000000..54e5a278b --- /dev/null +++ b/test/ide/probes/forms/Sources/frmMain.twin @@ -0,0 +1,25 @@ +[Description("")] +[FormDesignerId("F0A3F001-3B2E-4D69-8A14-7E91C2D4C615")] +[PredeclaredId] +Class frmMain + + Sub New() + End Sub + + Private Sub Form_Load() + Debug.Print "form load" + End Sub + + Private Sub Command1_Click() + Debug.Print "command1 click" + End Sub + + Private Sub Text1_Change() + Debug.Print "text1 change" + End Sub + + Private Sub Command2_Click(Index As Integer) + Debug.Print "command2 click " & Index + End Sub + +End Class diff --git a/test/ide/probes/forms/Sources/frmSaved.tbform b/test/ide/probes/forms/Sources/frmSaved.tbform new file mode 100644 index 000000000..32a9c822e --- /dev/null +++ b/test/ide/probes/forms/Sources/frmSaved.tbform @@ -0,0 +1,138 @@ +[ + { + "BackColor": -2147483633, + "BorderStyle": "vbSizable", + "Caption": "frmSaved", + "FontName": "Segoe UI", + "FontSize": 9, + "FontBold": false, + "FontItalic": false, + "FontStrikethru": false, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{F0A3F003-3B2E-4D69-8A14-7E91C2D4C615}", + "Height": 320, + "Left": 0, + "Name": "frmSaved", + "ScaleHeight": 4800, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 6000, + "StartUpPosition": "vbStartUpScreen", + "Top": 0, + "Visible": true, + "Width": 400, + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": true, + "showGrid": true, + "showOutlines": false + }, + "_children": [ + { + "_className": "Label", + "_clsid": "{33AD4ED8-6699-11CF-B70C-00AA0060D393}", + "Name": "Label1", + "Left": 10, + "Top": 10, + "Width": 100, + "Height": 20, + "Index": -1, + "TabIndex": 0, + "Caption": "Label1" + }, + { + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}", + "Name": "Text1", + "Left": 120, + "Top": 10, + "Width": 150, + "Height": 24, + "Index": -1, + "TabIndex": 1, + "Text": "Text1" + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command1", + "Left": 10, + "Top": 50, + "Width": 100, + "Height": 30, + "Index": -1, + "TabIndex": 2, + "Caption": "Command1" + }, + { + "_className": "CheckBox", + "_clsid": "{33AD4EF8-6699-11CF-B70C-00AA0060D393}", + "Name": "Check1", + "Left": 140, + "Top": 55, + "Width": 100, + "Height": 24, + "Index": -1, + "TabIndex": 3, + "Caption": "Check1" + }, + { + "_className": "Frame", + "_clsid": "{33AD4EE8-6699-11CF-B70C-00AA0060D393}", + "Name": "Frame1", + "Left": 10, + "Top": 100, + "Width": 200, + "Height": 130, + "Index": -1, + "TabIndex": 4, + "Caption": "Frame1", + "_children": [ + { + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}", + "Name": "Text2", + "Left": 20, + "Top": 30, + "Width": 120, + "Height": 24, + "Index": -1, + "TabIndex": 5, + "Text": "Text2" + } + ] + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command2", + "Left": 250, + "Top": 110, + "Width": 100, + "Height": 30, + "Index": 0, + "TabIndex": 6, + "Caption": "Command2(0)" + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command2", + "Left": 250, + "Top": 160, + "Width": 100, + "Height": 30, + "Index": 1, + "TabIndex": 7, + "Caption": "Command2(1)" + } + ], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/test/ide/probes/forms/Sources/frmSaved.twin b/test/ide/probes/forms/Sources/frmSaved.twin new file mode 100644 index 000000000..5b46cc91c --- /dev/null +++ b/test/ide/probes/forms/Sources/frmSaved.twin @@ -0,0 +1,9 @@ +[Description("")] +[FormDesignerId("F0A3F003-3B2E-4D69-8A14-7E91C2D4C615")] +[PredeclaredId] +Class frmSaved + + Sub New() + End Sub + +End Class diff --git a/test/ide/probes/forms/Sources/frmSizes.tbform b/test/ide/probes/forms/Sources/frmSizes.tbform new file mode 100644 index 000000000..ba894d7a4 --- /dev/null +++ b/test/ide/probes/forms/Sources/frmSizes.tbform @@ -0,0 +1,138 @@ +[ + { + "BackColor": -2147483633, + "BorderStyle": "vbSizable", + "Caption": "frmSizes", + "FontName": "Segoe UI", + "FontSize": 9, + "FontBold": false, + "FontItalic": false, + "FontStrikethru": false, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{F0A3F002-3B2E-4D69-8A14-7E91C2D4C615}", + "Height": 320, + "Left": 0, + "Name": "frmSizes", + "ScaleHeight": 4800, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 6000, + "StartUpPosition": "vbStartUpScreen", + "Top": 0, + "Visible": true, + "Width": 400, + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": true, + "showGrid": true, + "showOutlines": false + }, + "_children": [ + { + "_className": "Label", + "_clsid": "{33AD4ED8-6699-11CF-B70C-00AA0060D393}", + "Name": "Label1", + "Left": 10, + "Top": 10, + "Width": 100, + "Height": 20, + "Index": -1, + "TabIndex": 0, + "Caption": "Label1" + }, + { + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}", + "Name": "Text1", + "Left": 120, + "Top": 10, + "Width": 150, + "Height": 24, + "Index": -1, + "TabIndex": 1, + "Text": "Text1" + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command1", + "Left": 10, + "Top": 50, + "Width": 100, + "Height": 30, + "Index": -1, + "TabIndex": 2, + "Caption": "Command1" + }, + { + "_className": "CheckBox", + "_clsid": "{33AD4EF8-6699-11CF-B70C-00AA0060D393}", + "Name": "Check1", + "Left": 140, + "Top": 55, + "Width": 100, + "Height": 24, + "Index": -1, + "TabIndex": 3, + "Caption": "Check1" + }, + { + "_className": "Frame", + "_clsid": "{33AD4EE8-6699-11CF-B70C-00AA0060D393}", + "Name": "Frame1", + "Left": 10, + "Top": 100, + "Width": 200, + "Height": 130, + "Index": -1, + "TabIndex": 4, + "Caption": "Frame1", + "_children": [ + { + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}", + "Name": "Text2", + "Left": 20, + "Top": 30, + "Width": 120, + "Height": 24, + "Index": -1, + "TabIndex": 5, + "Text": "Text2" + } + ] + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command2", + "Left": 250, + "Top": 110, + "Width": 100, + "Height": 30, + "Index": 0, + "TabIndex": 6, + "Caption": "Command2(0)" + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command2", + "Left": 250, + "Top": 160, + "Width": 100, + "Height": 30, + "Index": 1, + "TabIndex": 7, + "Caption": "Command2(1)" + } + ], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/test/ide/probes/forms/Sources/frmSizes.twin b/test/ide/probes/forms/Sources/frmSizes.twin new file mode 100644 index 000000000..e83ca60af --- /dev/null +++ b/test/ide/probes/forms/Sources/frmSizes.twin @@ -0,0 +1,9 @@ +[Description("")] +[FormDesignerId("F0A3F002-3B2E-4D69-8A14-7E91C2D4C615")] +[PredeclaredId] +Class frmSizes + + Sub New() + End Sub + +End Class diff --git a/test/ide/probes/forms/Sources/v1.twin.forms/frmDeep.tbform b/test/ide/probes/forms/Sources/v1.twin.forms/frmDeep.tbform new file mode 100644 index 000000000..d60f5c237 --- /dev/null +++ b/test/ide/probes/forms/Sources/v1.twin.forms/frmDeep.tbform @@ -0,0 +1,13 @@ +[ + { + "Caption": "frmDeep", + "FormDesignerId": "{F0A3F011-3B2E-4D69-8A14-7E91C2D4C615}", + "Height": 300, + "Name": "frmDeep", + "ScaleMode": "vbPixels", + "Width": 300, + "_children": [], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/test/ide/probes/forms/Sources/v1.twin.forms/frmDeep.twin b/test/ide/probes/forms/Sources/v1.twin.forms/frmDeep.twin new file mode 100644 index 000000000..814100e60 --- /dev/null +++ b/test/ide/probes/forms/Sources/v1.twin.forms/frmDeep.twin @@ -0,0 +1,9 @@ +[Description("")] +[FormDesignerId("F0A3F011-3B2E-4D69-8A14-7E91C2D4C615")] +[PredeclaredId] +Class frmDeep + + Sub New() + End Sub + +End Class diff --git a/test/shots/forms/Settings b/test/shots/forms/Settings new file mode 100644 index 000000000..85909cd5f --- /dev/null +++ b/test/shots/forms/Settings @@ -0,0 +1,75 @@ +{ + "configuration.inherits": "Defaults", + "project.appTitle": "FormsDemo", + "project.buildPath": "${SourcePath}\\Build\\${ProjectName}_${Architecture}.${FileExtension}", + "project.buildType": "Standard EXE", + "project.description": "The \"Designing forms and reports\" chapter's pictures (shoot_docs setup forms-chapter).", + "project.exportPathIsV2": true, + "project.id": "{F0A3B7A1-0000-4000-9000-F0A3B7A10101}", + "project.name": "FormsDemo", + "project.optionExplicit": true, + "project.references": [ + { + "id": "{00020430-0000-0000-C000-000000000046}", + "lcid": 0, + "name": "OLE Automation", + "path32": "C:\\Windows\\SysWOW64\\stdole2.tlb", + "path64": "C:\\Windows\\System32\\stdole2.tlb", + "symbolId": "stdole", + "versionMajor": 2, + "versionMinor": 0 + }, + { + "id": "{F50B82D0-DCAB-43FE-9631-11959D4A4728}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - VB Compatibility Package (Forms)", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "VB", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 31 + }, + { + "id": "{C192FB39-64CA-4D9B-B477-A5502F48EFCC}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - App global class object", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "AppGlobalClassProject", + "versionBuild": 0, + "versionMajor": 1, + "versionMinor": 0, + "versionRevision": 0 + }, + { + "id": "{D63CB78D-9F5B-42FE-B94C-A545F2B2BFC3}", + "isCompilerPackage": true, + "licence": "MIT", + "name": "[COMPILER PACKAGE] twinBASIC - Common Controls Compatibility Package", + "path32": "", + "path64": "", + "publisher": "TWINBASIC-COMPILER", + "symbolId": "WinNativeCommonCtls", + "versionBuild": 0, + "versionMajor": 0, + "versionMinor": 0, + "versionRevision": 1 + } + ], + "project.settingsVersion": 1, + "project.startupObject": "Sub Main", + "project.warnings": { + "errors": [], + "hints": [], + "ignored": [], + "info": [], + "warnings": [] + }, + "runtime.useUnicodeStandardLibrary": true +} diff --git a/test/shots/forms/Sources/MyReport.tbreport b/test/shots/forms/Sources/MyReport.tbreport new file mode 100644 index 000000000..c7a2f7a81 --- /dev/null +++ b/test/shots/forms/Sources/MyReport.tbreport @@ -0,0 +1,154 @@ +[ + { + "_clsid": "{09040293-ECF8-4DEB-B7DE-DB264DC6ABF5}", + "_className": "Report", + "PaperColor": 16777215, + "InternalControlSections": [ + { + "_clsid": "{4A5D20CA-AC85-4CF8-B656-3D1ECCF22CAC}", + "_className": "ControlsSection", + "Height": 100, + "IdeCaption": "Page Header", + "SectionType": "PageHeader", + "BackStyle": "vbBFTransparent", + "BackColor": 16777215, + "BackColorAlternate": 15263976, + "BorderBottomHeight": 0, + "BorderBottomColor": 0, + "ForcePageBreak": "rptPageBreakNone", + "KeepTogether": false + }, + { + "_clsid": "{4A5D20CA-AC85-4CF8-B656-3D1ECCF22CAC}", + "_className": "ControlsSection", + "Height": 50, + "IdeCaption": "Page Footer", + "SectionType": "PageFooter", + "BackStyle": "vbBFTransparent", + "BackColor": 16777215, + "BackColorAlternate": 15263976, + "BorderBottomHeight": 0, + "BorderBottomColor": 0, + "ForcePageBreak": "rptPageBreakNone", + "KeepTogether": false + }, + { + "_clsid": "{4A5D20CA-AC85-4CF8-B656-3D1ECCF22CAC}", + "_className": "ControlsSection", + "Height": 30, + "IdeCaption": "Report Detail", + "SectionType": "Detail", + "BackStyle": "vbBFTransparent", + "BackColor": 16777215, + "BackColorAlternate": 15263976, + "BorderBottomHeight": 0, + "BorderBottomColor": 0, + "ForcePageBreak": "rptPageBreakNone", + "KeepTogether": true + }, + { + "_clsid": "{4A5D20CA-AC85-4CF8-B656-3D1ECCF22CAC}", + "_className": "ControlsSection", + "Height": 40, + "IdeCaption": "Report Header", + "SectionType": "ReportHeader", + "BackStyle": "vbBFTransparent", + "BackColor": 16777215, + "BackColorAlternate": 15263976, + "BorderBottomHeight": 0, + "BorderBottomColor": 0, + "ForcePageBreak": "rptPageBreakNone", + "KeepTogether": false + }, + { + "_clsid": "{4A5D20CA-AC85-4CF8-B656-3D1ECCF22CAC}", + "_className": "ControlsSection", + "Height": 25, + "IdeCaption": "Report Footer", + "SectionType": "ReportFooter", + "BackStyle": "vbBFTransparent", + "BackColor": 16777215, + "BackColorAlternate": 15263976, + "BorderBottomHeight": 0, + "BorderBottomColor": 0, + "ForcePageBreak": "rptPageBreakNone", + "KeepTogether": true + } + ], + "ReportWidth": 717.567454066668, + "ReportHeight": 1047.05301833333, + "LeftMargin": 37.7952755906, + "RightMargin": 37.7952755906, + "TopMargin": 37.7952755906, + "BottomMargin": 37.7952755906, + "FormDesignerId": "{F0A3F102-3B2E-4D69-8A14-7E91C2D4C615}", + "StartUpPosition": "vbStartUpWindowsDefault", + "Width": 400, + "Height": 600, + "Name": "MyReport", + "ScaleMode": "vbTwips", + "FontName": "Segoe UI", + "FontSize": 8, + "__IDEOptions": { + "showGrid": true, + "alignToGrid": false, + "showOutlines": false, + "gridWidth": 10, + "gridHeight": 10, + "lockedControls": [], + "multiColoredGrabbers": true + }, + "_children": [ + { + "_className": "Label", + "_clsid": "{33AD4ED8-6699-11CF-B70C-00AA0060D393}", + "Name": "Label1", + "InternalSectionId": 3, + "Left": 10, + "Top": 6, + "Width": 300, + "Height": 28, + "Index": -1, + "TabIndex": 0, + "Caption": "Sales by region", + "FontName": "Segoe UI", + "FontSize": 14, + "FontBold": true, + "BackStyle": "vbBFTransparent", + "_paintedByParent": true + }, + { + "_className": "Line", + "_clsid": "{33AD4F88-6699-11CF-B70C-00AA0060D393}", + "Name": "Line1", + "InternalSectionId": 2, + "BorderColor": 12369084, + "BorderStyle": "vbBSSolid", + "BorderWidth": 2, + "X1": 10, + "Y1": 15, + "X2": 40, + "Y2": 15, + "Index": -1, + "_paintedByParent": true + }, + { + "_className": "Label", + "_clsid": "{33AD4ED8-6699-11CF-B70C-00AA0060D393}", + "Name": "Label2", + "InternalSectionId": 2, + "Left": 50, + "Top": 6, + "Width": 200, + "Height": 20, + "Index": -1, + "TabIndex": 1, + "Caption": "Region", + "FontName": "Segoe UI", + "FontSize": 9, + "BackStyle": "vbBFTransparent", + "_paintedByParent": true + } + ] + } +] diff --git a/test/shots/forms/Sources/MyReport.twin b/test/shots/forms/Sources/MyReport.twin new file mode 100644 index 000000000..e1678fa99 --- /dev/null +++ b/test/shots/forms/Sources/MyReport.twin @@ -0,0 +1,12 @@ +[Description("")] +[FormDesignerId("F0A3F102-3B2E-4D69-8A14-7E91C2D4C615")] +[PredeclaredId] +Class MyReport + + Sub New() + End Sub + + Private Sub Report_Load() + End Sub + +End Class diff --git a/test/shots/forms/Sources/Startup.twin b/test/shots/forms/Sources/Startup.twin new file mode 100644 index 000000000..7a776c95f --- /dev/null +++ b/test/shots/forms/Sources/Startup.twin @@ -0,0 +1,8 @@ +' The "Designing forms and reports" chapter's pictures: Main shows frmMain. +Module Startup + + Public Sub Main() + frmMain.Show + End Sub + +End Module diff --git a/test/shots/forms/Sources/frmMain.tbform b/test/shots/forms/Sources/frmMain.tbform new file mode 100644 index 000000000..0c443c491 --- /dev/null +++ b/test/shots/forms/Sources/frmMain.tbform @@ -0,0 +1,216 @@ +[ + { + "BackColor": -2147483633, + "BorderStyle": "vbSizable", + "Caption": "frmMain", + "FontName": "Segoe UI", + "FontSize": 9, + "FontBold": false, + "FontItalic": false, + "FontStrikethru": false, + "FontUnderline": false, + "ForeColor": -2147483630, + "FormDesignerId": "{F0A3F101-3B2E-4D69-8A14-7E91C2D4C615}", + "Height": 270, + "Left": 0, + "Name": "frmMain", + "ScaleHeight": 4050, + "ScaleLeft": 0, + "ScaleMode": "vbTwips", + "ScaleTop": 0, + "ScaleWidth": 5700, + "StartUpPosition": "vbStartUpScreen", + "Top": 0, + "Visible": true, + "Width": 380, + "__IDEOptions": { + "alignToGrid": false, + "gridHeight": 10, + "gridWidth": 10, + "lockedControls": [], + "multiColoredGrabbers": true, + "showGrid": true, + "showOutlines": false + }, + "_children": [ + { + "_clsid": "{33AD4F68-6699-11CF-B70C-00AA0060D393}", + "_className": "Menu", + "Caption": "&File", + "Checked": false, + "Enabled": true, + "Visible": true, + "HelpContextID": 0, + "WindowList": false, + "NegotiatePosition": "vbNegotiatePositionNone", + "Shortcut": null, + "ShortcutId": "vbShortcutNone", + "Picture": [], + "IconSizeX": 0, + "IconSizeY": 0, + "Index": -1, + "Name": "File", + "Tag": null, + "_children": [ + { + "_clsid": "{33AD4F68-6699-11CF-B70C-00AA0060D393}", + "_className": "Menu", + "Caption": "E&xit", + "Checked": false, + "Enabled": true, + "Visible": true, + "HelpContextID": 0, + "WindowList": false, + "NegotiatePosition": "vbNegotiatePositionNone", + "Shortcut": null, + "ShortcutId": "vbShortcutNone", + "Picture": [], + "IconSizeX": 0, + "IconSizeY": 0, + "Index": -1, + "Name": "Exit", + "Tag": null + }, + { + "_clsid": "{33AD4F68-6699-11CF-B70C-00AA0060D393}", + "_className": "Menu", + "Caption": "-", + "Checked": false, + "Enabled": true, + "Visible": true, + "HelpContextID": 0, + "WindowList": false, + "NegotiatePosition": "vbNegotiatePositionNone", + "Shortcut": null, + "ShortcutId": "vbShortcutNone", + "Picture": [], + "IconSizeX": 0, + "IconSizeY": 0, + "Index": -1, + "Name": "Menu", + "Tag": null + }, + { + "_clsid": "{33AD4F68-6699-11CF-B70C-00AA0060D393}", + "_className": "Menu", + "Caption": "&Open", + "Checked": false, + "Enabled": true, + "Visible": true, + "HelpContextID": 0, + "WindowList": false, + "NegotiatePosition": "vbNegotiatePositionNone", + "Shortcut": null, + "ShortcutId": "vbShortcutNone", + "Picture": [], + "IconSizeX": 0, + "IconSizeY": 0, + "Index": -1, + "Name": "Open", + "Tag": null + } + ] + }, + { + "_className": "Label", + "_clsid": "{33AD4ED8-6699-11CF-B70C-00AA0060D393}", + "Name": "Label1", + "Left": 130, + "Top": 12, + "Width": 100, + "Height": 20, + "Index": -1, + "TabIndex": 1, + "Caption": "Label1" + }, + { + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}", + "Name": "Text1", + "Left": 10, + "Top": 10, + "Width": 110, + "Height": 24, + "Index": -1, + "TabIndex": 0, + "Text": "Text1" + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command1", + "Left": 10, + "Top": 70, + "Width": 100, + "Height": 30, + "Index": -1, + "TabIndex": 2, + "Caption": "Command1" + }, + { + "_className": "CheckBox", + "_clsid": "{33AD4EF8-6699-11CF-B70C-00AA0060D393}", + "Name": "Check1", + "Left": 140, + "Top": 75, + "Width": 100, + "Height": 24, + "Index": -1, + "TabIndex": 3, + "Caption": "Check1" + }, + { + "_className": "Frame", + "_clsid": "{33AD4EE8-6699-11CF-B70C-00AA0060D393}", + "Name": "Frame1", + "Left": 10, + "Top": 120, + "Width": 200, + "Height": 130, + "Index": -1, + "TabIndex": 4, + "Caption": "Frame1", + "_children": [ + { + "_className": "TextBox", + "_clsid": "{33AD4EE0-6699-11CF-B70C-00AA0060D393}", + "Name": "Text2", + "Left": 20, + "Top": 30, + "Width": 120, + "Height": 24, + "Index": -1, + "TabIndex": 5, + "Text": "Text2" + } + ] + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command2", + "Left": 250, + "Top": 130, + "Width": 100, + "Height": 30, + "Index": 0, + "TabIndex": 6, + "Caption": "Command2(0)" + }, + { + "_className": "CommandButton", + "_clsid": "{33AD4EF0-6699-11CF-B70C-00AA0060D393}", + "Name": "Command2", + "Left": 250, + "Top": 180, + "Width": 100, + "Height": 30, + "Index": 1, + "TabIndex": 7, + "Caption": "Command2(1)" + } + ], + "_className": "Form", + "_clsid": "{33AD4F38-6699-11CF-B70C-00AA0060D393}" + } +] diff --git a/test/shots/forms/Sources/frmMain.twin b/test/shots/forms/Sources/frmMain.twin new file mode 100644 index 000000000..f06e08983 --- /dev/null +++ b/test/shots/forms/Sources/frmMain.twin @@ -0,0 +1,25 @@ +[Description("")] +[FormDesignerId("F0A3F101-3B2E-4D69-8A14-7E91C2D4C615")] +[PredeclaredId] +Class frmMain + + Sub New() + End Sub + + Private Sub Command1_Click() + Text1.Text = "Clicked" + End Sub + + Private Sub Text1_Change() + Label1.Caption = Text1.Text + End Sub + + Private Sub Command2_Click(Index As Integer) + Text2.Text = "Command2(" & Index & ")" + End Sub + + Private Sub Exit_Click() + Unload Me + End Sub + +End Class