Foundation 6

5. List windows

A list window shows a table's records in a listbox with a toolbar above it. In Foundation 6 this is the Fnd_Out module: one shared form, columns built at run time from an entity selection, and a View Options palette through which the user chooses, orders, sizes and styles the columns and saves the result as a layout. The developer decides which fields are offered, which are hidden, and what the list looks like before anyone has touched it.

This replaces the classic output form of the 5.4 manual, a per-table Output form with Header, Detail and Footer lines inheriting Fnd_Tlbr_Toolbar. That form still works where a host has one and Fnd_Out is not switched on; nothing here requires you to convert it. New hosts should not build one.

What you need

  1. The Fnd_Out form, a project form named exactly that, once per host and shared by every table. Foundation cannot carry it, because a component's forms cannot be the host's. Copy the folder HooksAndHosts/Fnd_Out/ into your Project/Sources/Forms/. It holds one collection listbox named Fnd_Out_ListBox with no columns, bound to Form.listData, plus two invisible buttons for Cmd-W and Cmd-Option-W, and a three-line form method:
   Fnd_Gen_FormMethod
   Fnd_Tlbr_FormMethod
   Fnd_Out_FormMethod

The listbox's object method is Fnd_Out_ListBoxMethod. The form inherits Fnd_Tlbr_Toolbar, which gives it the toolbar and status line.

  1. Fnd_Out_Active(True) in Fnd_Hook_IO_DisplayTable. The switch is per process and per opening, so the hook sets it every time. Without it Foundation looks for the classic Output form.
  2. An Input form for the table (chapter 6), so that double-click opens a record.

Then Fnd_IO_DisplayTable(->[Invoices]) opens the list: from the navigation palette, the Open Table dialog, a related-records command or your own code. Foundation supplies the rest: toolbar, status line, Find and Sort, the Window menu, the View Options palette, saved widths and order, and the two hooks below.

The standard columns

With nothing else said, Foundation picks a few of the table's own fields in field order, skipping the primary key. Say what you want instead in the same hook, per table:

Case of
	: (Fnd_Gen_CurrentTable=(->[Invoices]))
		Fnd_Out_Active(True)
		Fnd_Out_AddField("InvoiceNumber"; 0; "Inv. No")
		Fnd_Out_AddField("InvoiceDate"; 0; "Date")
		Fnd_Out_AddField("contact.reversedFullName()"; 0; "Contact")
		Fnd_Out_AddField("Total"; 0; ""; 0; <>Gen_MoneyFormat_t)
End case

Fnd_Out_AddField takes a field pointer, a path through the relations in ORDA form (contact.Company, invoice.contact.City), a formula on the entity (contact.reversedFullName(), an exposed function of your entity class), or an object with fo_field, fo_title, fo_type, fo_format, fo_hAlign, fo_vAlign, fo_width, fo_minWidth, fo_maxWidth, fo_style and fo_visible. Width 0 means "estimate from the type": text 100, numbers 65 right-aligned, dates and times 65, booleans 19. This set is the layout called Standard; "Reset to Standard" in the palette returns the user to it.

Columns are formula columns over This, the row's entity, so a path or a function of the entity class is a column like any field. Headers sort nothing by themselves: Sort is on the Select menu, as before.

What the user may add

The palette offers the table's own fields and, block by block, the fields of every related table up to five relations deep. Narrow that in the hook:

CallEffect
Fnd_Out_HideField("Password"), Fnd_Out_HideField("@.Password")never offer this path; @ wildcards
Fnd_Out_OfferField("Subtotal")the first call for a table switches to "offer only what is listed"
Fnd_Out_OfferFormula("This.fullName()"; "Contact"; Is text)offer a formula under Other Columns
Fnd_Out_MaxRelationDepth(1)how far to walk the relations; 0 is the table's own fields only
Fnd_Out_AllowCustomColumns(False)the user may not add formulas of their own to this list

None of these touches a column you added with Fnd_Out_AddField. The same rules can be written once, declaratively, in the host's Resources/Fnd_Out/<Table>.json: offer.hide, offer.only, formulas, appearance, allowCustomColumns, and builtInLayouts. FPS has three such files to copy from.

Appearance for every list

Fnd_Out_Default(tag; value) sets a programmed default for every list in the session; one argument reads it back. Tags are the constants Fnd_Out_FontName, Fnd_Out_TextSize, Fnd_Out_RowHeight, Fnd_Out_RowPixels, Fnd_Out_VAlign, Fnd_Out_Fill, Fnd_Out_AltFill, Fnd_Out_RelationDepth, Fnd_Out_MaxColumns, Fnd_Out_AllowCustom and Fnd_Out_AllowPublish. Set them in Fnd_Hook_Shell_Setup or at the top of Fnd_Hook_IO_DisplayTable, as FPS sets Fnd_Out_Default("allowPublish"; True).

What the user sees

Select > View Options… opens one floating palette that follows whichever list window is in front.

Everything the user does is saved when the window closes, in <4D application folder>/<DatabaseName>/Fnd_Out/<Table>.json on that machine, and comes back next time. Hold Option and Shift while a list opens to ignore the saved order and widths once.

Layouts

A layout is a named set of visible columns, order, widths, font, text size, row height and row-style rules. The Layout menu lists them and Manage Layouts… renames, deletes and sets defaults.

KindWho makes itWhere it livesShown as
Standardthe developer's AddField callsnot storedStandard
Userany user, Save As…the user's Fnd_Out/<Table>.jsonName
Shareda user with publish rights, Publish…the data file, as a shared preferenceName (Shared)
Built-inthe developer, in an interpreted hostthe host's Resources/Fnd_Out/<Table>.jsonName (Built-in)

Publish rights are the Designer, the Administrator and the Administration group, or whatever Fnd_Out_Default("allowPublish"; …) says. A layout marked default opens for every new user of that table, built-in first, then shared.

Developer mode

An interpreted host is developer mode; nothing switches it on. The palette then shows an offer tick per candidate column, which writes offer.hide and offer.only into the table's Resources/Fnd_Out/<Table>.json for you, and the Layout menu gains Save as Application Layout… and Delete Application Layout, which write builtInLayouts there. Compiled, those two refuse to run and hidden candidates are not even listed. Ship the JSON files with your application.

Row styles

Two sources, the first that applies wins: a layout's row-style rules, made in the Advanced panel from a condition written in the formula editor and a style (bold, italic, underline, text and fill colour); and the hook Fnd_Hook_Out_RowStyle, evaluated for every row with This as the entity:

#DECLARE()->$style_o : Object

$style_o:=New object

Case of
	: (Fnd_Gen_CurrentTable=(->[Invoices]))
		If (This.Total>500)
			$style_o.fontWeight:="bold"
		End if
	: (Fnd_Gen_CurrentTable=(->[Products]))
		If (This.ListPrice>500)
			$style_o.fontWeight:="bold"
			$style_o.stroke:="red"
		End if
End case

Return any of fontWeight, fontStyle, textDecoration, stroke, fill; an empty object leaves the row alone. It is a 4D meta-info expression, so keep it cheap: it runs for every visible row on every redraw.

The context menu

A right-click (Control-click on the Mac) in a list calls Fnd_Hook_Out_ContextMenu(->table; $selected_es; $clicked_e; $row_i) with the table, the highlighted records as an entity selection, the entity under the pointer (Null over empty space) and the clicked row number. The hook builds and shows the menu itself. The generated template, which FPS uses as it stands, offers New, Edit Record(s) up to a limit, Delete, Show Subset and Omit Subset, calling Fnd_Rec_NewRecord, Fnd_Out_EditRecord($row_i), Fnd_Out_EditSelection($selected_es), Fnd_Rec_DeleteUserSet, Fnd_Rec_ShowSubset and Fnd_Rec_OmitSubset. Add your own items after them; a commented example in the template archives the selection through ORDA.

A list can show a search box in its toolbar band. It searches the whole table, not the current selection, as the user types; Enter searches at once, and emptying the box shows every record.

Add a subform object named exactly Fnd_Out_Search, with detail form Fnd_Out_Search, to your Fnd_Out form once, drawn anywhere in the toolbar band at about 192 × 26. Foundation places it when the list opens and on every resize, and hides it when the window is too narrow.

Then declare what each table searches, in Fnd_Hook_Out_SearchFields, which Foundation generates the first time Fnd_Out runs. FPS does this for three tables:

Case of
	: ($table_ptr=(->[Contacts]))
		Fnd_Out_AddSearchField(->[Contacts]LastName; "contains")
		Fnd_Out_AddSearchField(->[Contacts]FirstName; "contains")
		Fnd_Out_AddSearchField(->[Contacts]Company; "contains")

	: ($table_ptr=(->[Invoices]))
		Fnd_Out_AddSearchField(->[Invoices]InvoiceNumber; ""; "Inv. #")
		Fnd_Out_AddSearchField(->[Invoices]InvoiceDate)
		Fnd_Out_AddSearchField("contact.LastName"; ""; "Customer")

	: ($table_ptr=(->[Products]))
		Fnd_Out_AddSearchField(->[Products]ProductNumber)
		Fnd_Out_AddSearchField(->[Products]ProductName; "keyword")
		Fnd_Out_SearchHint("Number or name"; "Product numbers match from the start; names match whole words."+Char(Carriage return)+"Searches: "+Fnd_Out_SearchHelp)
End case

The empty box shows the fields it searches in grey, as many as fit, and hovering over it lists them all.

Each field has a title, which is the name used in the hint and the tip. It is the field's virtual field name by default, and Fnd_VS_SetFieldTitle changes it everywhere. The optional third parameter changes it for the search box only. A path with no virtual field name shows its last segment. The second parameter may be "", which means begins.

The object form is for the tech-savvy. It takes the same three things as keys, using the constants fo_field, fo_operator and fo_title, and a key outranks the matching parameter:

Fnd_Out_AddSearchField(New object(fo_field; "contact.LastName"; fo_title; "Customer"))

To replace the hint or the tip altogether, call Fnd_Out_SearchHint(hint; tooltip) after the declarations. The hint is shown as written. The tip may run to several lines. An empty text keeps Foundation's own. Fnd_Out_SearchHelp returns the generated list of field names, so the tip can include it, as the Products branch does.

A table with no declared fields has no box. A field is a pointer, or a text path for one reached through relations. A text field takes an operator: begins (the default), contains, keyword or exact. A number field is searched only when the text is a number. A date field is searched only when the text is a date, which Fnd_Out_SearchDate reads: a full date, today, yesterday, a bare day of this month, or a day and month in the system's order.

For anything the declared fields cannot express, Fnd_Hook_Out_Search is called first and may return an entity selection to display; returning Null falls back to the declared fields.

Working with the list from your code

CallDoes
Fnd_Out_Updateredraw the list after you changed records, and refresh the status line
Fnd_Out_SynchroniseSelectionmake the classic current selection match what the list shows, before a classic command such as a print or export
Fnd_Out_EditRecord($row_i), Fnd_Out_EditSelection($selected_es)open a row, or every highlighted record in turn
Fnd_Out_Info("highlighted"), ("inSelection"), ("state")counts and state for your own status text
Fnd_IO_MultiWindow(False)show the record in the list's own window rather than a new one (FPS Products)
Fnd_IO_AddMultipleRecords(True)after Save, offer another new record until Cancel (FPS Products)

The selection the list shows is an entity selection in Form.listData; Find, Sort, Show Subset and the rest replace it and the list follows. Foundation's classic current selection is kept in step at the moments it matters, which is what Fnd_Out_SynchroniseSelection is for when you need it yourself.

Known limits in the beta