Context Menus
Context menus are a good place for commands that belong to the current record or cell. FDC builds them from FdcMenuEntry objects and gives the builder a rich FdcGridMenuContext describing the row, column, value, selection state, edit state, and record commands that are currently valid.
FdcGrid(
dataSet: customers,
options: const FdcGridOptions(autoEdit: true),
menuBuilder: (context) {
final company = '${context.row['company_name'] ?? 'Customer'}';
final status = '${context.row['status'] ?? ''}';
final isActive = context.row['active'] == true;
return <FdcMenuEntry>[
FdcMenuTitle(text: company),
FdcMenuAction(
text: 'Open customer details',
icon: Icons.open_in_new,
onPressed: () => openCustomer(context.recordId),
),
const FdcMenuSeparator(),
FdcSubMenu(
text: 'Set status',
icon: Icons.flag_outlined,
children: <FdcMenuEntry>[
for (final nextStatus in const <String>[
'Lead',
'Active',
'On Hold',
'Inactive',
])
FdcMenuCheckAction(
text: nextStatus,
checked: status == nextStatus,
onPressed: () => setCustomerStatus(
context,
nextStatus,
),
),
],
),
FdcMenuCheckAction(
text: 'Active account',
icon: Icons.verified_outlined,
checked: isActive,
onPressed: () => setAccountActive(
context,
!isActive,
),
),
const FdcMenuSeparator(),
FdcMenuAction(
text: 'Insert customer here',
icon: Icons.add_box_outlined,
enabled: context.canInsertRecord,
onPressed: context.insertRecord,
),
FdcMenuAction(
text: 'Append customer',
icon: Icons.playlist_add,
enabled: context.canAppendRecord,
onPressed: context.appendRecord,
),
FdcMenuAction(
text: 'Cancel current edit',
icon: Icons.undo,
enabled: context.canCancelEdit,
onPressed: context.cancelEdit,
),
];
},
columns: customerColumns,
);
Right-click any customer row to open the sample menu. The menu combines record-specific actions, a status submenu, a checked account-state action, a business operation over the credit limit, and dataset commands whose enabled state follows the current grid lifecycle.
Build a grid-level menu
Use menuBuilder on FdcGrid when most columns should share the same record-oriented menu:
FdcGrid(
dataSet: customers,
menuBuilder: (context) {
final company = '${context.row['company_name']}';
final status = '${context.row['status']}';
final isActive = context.row['active'] == true;
return [
FdcMenuTitle(text: company),
FdcMenuAction(
text: 'Open customer details',
icon: Icons.open_in_new,
onPressed: () => openCustomer(context.recordId),
),
const FdcMenuSeparator(),
FdcSubMenu(
text: 'Set status',
icon: Icons.flag_outlined,
children: [
for (final nextStatus in const [
'Lead',
'Active',
'On Hold',
'Inactive',
])
FdcMenuCheckAction(
text: nextStatus,
checked: status == nextStatus,
onPressed: () => setCustomerStatus(
context,
nextStatus,
),
),
],
),
FdcMenuCheckAction(
text: 'Active account',
checked: isActive,
onPressed: () => setAccountActive(
context,
!isActive,
),
),
const FdcMenuSeparator(),
FdcMenuAction(
text: 'Insert customer here',
enabled: context.canInsertRecord,
onPressed: context.insertRecord,
),
FdcMenuAction(
text: 'Append customer',
enabled: context.canAppendRecord,
onPressed: context.appendRecord,
),
FdcMenuAction(
text: 'Cancel current edit',
enabled: context.canCancelEdit,
onPressed: context.cancelEdit,
),
];
},
columns: customerColumns,
)
The builder runs for the cell that opened the menu, so menu labels, checked states, enabled states, and actions can all be derived from that exact row.
What the context provides
FdcGridMenuContext exposes the information needed for cell- and record-oriented commands:
dataSet— the dataset connected to the grid;row— read-only row access throughrow['field_name']orvalueOf();rowIndexandsourceRowIndex— view and adapter positions when available;recordId— internal record identifier;column,columnIndex, andvalue— the cell that opened the menu;isEditing— whether an edit buffer is active;isCellSelectedandisRowSelected— current selection state;canInsertRecord,canAppendRecord, andcanCancelEdit— command availability;insertRecord,appendRecord, andcancelEdit— grid-owned record commands.
Use row for decisions and labels. Use dataSet or application services when the action needs to change business data.
Menu entry types
The shared menu model includes:
FdcMenuActionfor immediate commands;FdcMenuCheckActionfor checked or toggle-style commands;FdcMenuTitlefor a non-interactive group caption;FdcMenuSeparatorfor visual grouping;FdcSubMenufor nested commands;FdcMenuWidgetEntryfor custom widget content.
The sample deliberately uses several entry types together. The customer name is a title, status choices live in a submenu, account state is a check action, and record commands are separated from business actions.
Context-sensitive enabled states
Do not duplicate grid lifecycle rules in application code when the context already exposes them. For example:
FdcMenuAction(
text: 'Append customer',
enabled: context.canAppendRecord,
onPressed: context.appendRecord,
)
The grid decides whether the operation is currently valid. The menu only reflects that state.
This is especially useful around active edits and inserts, where a command that is normally available may temporarily need to be disabled or replaced by cancelEdit.
Override a specific column
A column-level menuBuilder takes precedence over the grid-level builder for cells in that column. Use it when one field has specialized commands:
FdcTextColumn(
fieldName: 'email',
label: 'Email',
menuBuilder: (context) => [
FdcMenuAction(
text: 'Compose email',
icon: Icons.mail_outline,
onPressed: () => composeEmail('${context.value ?? ''}'),
),
FdcMenuAction(
text: 'Copy email address',
icon: Icons.copy,
onPressed: () => copyText('${context.value ?? ''}'),
),
],
)
Use grid-level menus for record-wide workflows and column overrides for value-specific actions. Keeping that distinction usually produces shorter, more predictable menus.
Keep business work outside the builder
The menu builder should stay lightweight. It is appropriate to inspect context and construct entries, but long-running workflows should hand off to application services:
FdcMenuAction(
text: 'Generate statement',
onPressed: () => statementService.generate(
customerId: context.row['customer_id'] as int,
),
)
This keeps menu construction synchronous and avoids embedding persistence, networking, or navigation orchestration inside the menu definition itself.
Range-selection context menu
Range Selection has its own copy/paste context-menu behavior. Configure it through FdcGridRangeSelection rather than the body-cell menuBuilder.