Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DropdownMenu Control #3638

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
27 changes: 18 additions & 9 deletions packages/flet/lib/src/controls/create_control.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import 'divider.dart';
import 'drag_target.dart';
import 'draggable.dart';
import 'dropdown.dart';
import 'dropdown_menu.dart';
import 'elevated_button.dart';
import 'error.dart';
import 'expansion_panel.dart';
Expand Down Expand Up @@ -820,6 +821,15 @@ Widget createWidget(
control: controlView.control,
parentDisabled: parentDisabled,
backend: backend);
case "dropdownmenu":
return DropdownMenuControl(
key: key,
parent: parent,
control: controlView.control,
children: controlView.children,
parentDisabled: parentDisabled,
parentAdaptive: parentAdaptive,
backend: backend);
case "dropdown":
return DropdownControl(
key: key,
Expand Down Expand Up @@ -1218,15 +1228,14 @@ Widget _positionedControl(
}

Widget _sizedControl(Widget widget, Control? parent, Control control) {
var width = control.attrDouble("width", null);
var height = control.attrDouble("height", null);
if (width != null || height != null) {
if (control.type != "container" && control.type != "image") {
widget = ConstrainedBox(
constraints: BoxConstraints.tightFor(width: width, height: height),
child: widget,
);
}
var width = control.attrDouble("width");
var height = control.attrDouble("height");
if ((width != null || height != null) &&
!["container", "image"].contains(control.type)) {
ndonkoHenri marked this conversation as resolved.
Show resolved Hide resolved
widget = ConstrainedBox(
constraints: BoxConstraints.tightFor(width: width, height: height),
child: widget,
);
}
var animation = parseAnimation(control, "animateSize");
if (animation != null) {
Expand Down
33 changes: 23 additions & 10 deletions packages/flet/lib/src/controls/dropdown.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ class _DropdownControlState extends State<DropdownControl> with FletStoreMixin {
.where((c) => c.control.name == "prefix" && c.control.isVisible);
var suffixControls = itemsView.controlViews
.where((c) => c.control.name == "suffix" && c.control.isVisible);
var counterControls = itemsView.controlViews
.where((c) => c.control.name == "counter" && c.control.isVisible);
var helperControls = itemsView.controlViews
.where((c) => c.control.name == "helper" && c.control.isVisible);
var errorControls = itemsView.controlViews
.where((c) => c.control.name == "error" && c.control.isVisible);

var focusValue = widget.control.attrString("focus");
if (focusValue != null && focusValue != _lastFocusValue) {
Expand Down Expand Up @@ -176,18 +182,25 @@ class _DropdownControlState extends State<DropdownControl> with FletStoreMixin {
icon: iconCtrl.isNotEmpty
? createControl(widget.control, iconCtrl.first.id, disabled)
: null,
hint: iconCtrl.isNotEmpty
hint: hintCtrl.isNotEmpty
? createControl(widget.control, hintCtrl.first.id, disabled)
: null,
decoration: buildInputDecoration(
context,
widget.control,
prefixControls.isNotEmpty ? prefixControls.first.control : null,
suffixControls.isNotEmpty ? suffixControls.first.control : null,
null,
_focused,
disabled,
widget.parentAdaptive),
decoration: buildInputDecoration(context, widget.control,
prefix:
prefixControls.isNotEmpty ? prefixControls.first.control : null,
suffix:
suffixControls.isNotEmpty ? suffixControls.first.control : null,
counter: counterControls.isNotEmpty
? counterControls.first.control
: null,
helper:
helperControls.isNotEmpty ? helperControls.first.control : null,
error:
errorControls.isNotEmpty ? errorControls.first.control : null,
customSuffix: null,
focused: _focused,
disabled: disabled,
adaptive: widget.parentAdaptive),
onTap: !disabled
? () {
widget.backend.triggerControlEvent(widget.control.id, "click");
Expand Down
235 changes: 235 additions & 0 deletions packages/flet/lib/src/controls/dropdown_menu.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

import '../flet_control_backend.dart';
import '../models/control.dart';
import '../models/control_view_model.dart';
import '../utils/buttons.dart';
import '../utils/edge_insets.dart';
import '../utils/form_field.dart';
import '../utils/icons.dart';
import '../utils/menu.dart';
import '../utils/text.dart';
import '../utils/textfield.dart';
import 'create_control.dart';
import 'flet_store_mixin.dart';
import 'textfield.dart';

class DropdownMenuControl extends StatefulWidget {
final Control? parent;
final Control control;
final List<Control> children;
final bool parentDisabled;
final bool? parentAdaptive;
final FletControlBackend backend;

const DropdownMenuControl(
{super.key,
this.parent,
required this.control,
required this.children,
required this.parentDisabled,
required this.parentAdaptive,
required this.backend});

@override
State<DropdownMenuControl> createState() => _DropdownMenuControlState();
}

class _DropdownMenuControlState extends State<DropdownMenuControl>
with FletStoreMixin {
String? _value;
bool _focused = false;
late final FocusNode _focusNode;
String? _lastFocusValue;

@override
void initState() {
super.initState();
_focusNode = FocusNode();
_focusNode.addListener(_onFocusChange);
}

void _onFocusChange() {
setState(() {
_focused = _focusNode.hasFocus;
});
widget.backend.triggerControlEvent(
widget.control.id, _focusNode.hasFocus ? "focus" : "blur");
}

@override
void dispose() {
_focusNode.removeListener(_onFocusChange);
_focusNode.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
debugPrint("DropdownMenu build: ${widget.control.id}");
return withControls(widget.control.childIds, (context, itemsView) {
debugPrint("DropdownMenuFletControlState build: ${widget.control.id}");

bool disabled = widget.control.isDisabled || widget.parentDisabled;

var textSize = widget.control.attrDouble("textSize");
var label = widget.control.attrString("label");
var suffixCtrl =
widget.children.where((c) => c.name == "suffix" && c.isVisible);
var selectedSuffixCtrl = widget.children
.where((c) => c.name == "selectedSuffix" && c.isVisible);
var prefixCtrl =
widget.children.where((c) => c.name == "prefix" && c.isVisible);
var labelCtrl =
widget.children.where((c) => c.name == "label" && c.isVisible);
var selectedSuffixIcon = widget.control.attrString("selectedSuffixIcon");
var prefixIcon = widget.control.attrString("prefixIcon");
var suffixIcon = widget.control.attrString("suffixIcon");
var color = widget.control.attrColor("color", context);
var focusedColor = widget.control.attrColor("focusedColor", context);

TextStyle? textStyle =
parseTextStyle(Theme.of(context), widget.control, "textStyle");
if (textSize != null || color != null || focusedColor != null) {
textStyle = (textStyle ?? const TextStyle()).copyWith(
fontSize: textSize,
color: (_focused ? focusedColor ?? color : color) ??
Theme.of(context).colorScheme.onSurface);
}

var items = itemsView.controlViews
.where((c) =>
c.control.name == null &&
c.control.type == "dropdownmenuoption" &&
c.control.isVisible)
.map<DropdownMenuEntry<String>>((ControlViewModel itemCtrlView) {
var itemCtrl = itemCtrlView.control;
bool itemDisabled = disabled || itemCtrl.isDisabled;
ButtonStyle? style =
parseButtonStyle(Theme.of(context), itemCtrl, "style");

var contentCtrls = itemCtrlView.children
.where((c) => c.name == "content" && c.isVisible);

var prefixIconCtrls = itemCtrlView.children
.where((c) => c.name == "prefix" && c.isVisible);
var suffixIconCtrls = itemCtrlView.children
.where((c) => c.name == "suffix" && c.isVisible);

return DropdownMenuEntry<String>(
enabled: !itemDisabled,
value: itemCtrl.attrs["key"] ?? itemCtrl.attrs["text"] ?? itemCtrl.id,
label: itemCtrl.attrs["text"] ?? itemCtrl.attrs["key"] ?? itemCtrl.id,
labelWidget: contentCtrls.isNotEmpty
? createControl(
itemCtrlView.control, contentCtrls.first.id, itemDisabled)
: null,
leadingIcon: prefixIconCtrls.isNotEmpty
? createControl(
itemCtrlView.control, prefixIconCtrls.first.id, itemDisabled)
: itemCtrlView.control.attrString("prefixIcon") != null
? Icon(
parseIcon(itemCtrlView.control.attrString("prefixIcon")))
: null,
trailingIcon: suffixIconCtrls.isNotEmpty
? createControl(
itemCtrlView.control, suffixIconCtrls.first.id, itemDisabled)
: itemCtrlView.control.attrString("suffixIcon") != null
? Icon(
parseIcon(itemCtrlView.control.attrString("suffixIcon")))
: null,
style: style,
);
}).toList();

String? value = widget.control.attrString("value");
if (_value != value) {
_value = value;
}

if (items.where((item) => item.value == value).isEmpty) {
_value = null;
}

var focusValue = widget.control.attrString("focus");
if (focusValue != null && focusValue != _lastFocusValue) {
_lastFocusValue = focusValue;
_focusNode.requestFocus();
}

TextCapitalization textCapitalization = parseTextCapitalization(
widget.control.attrString("capitalization"),
TextCapitalization.none)!;

FilteringTextInputFormatter? inputFilter =
parseInputFilter(widget.control, "inputFilter");

List<TextInputFormatter>? inputFormatters = [];
// add non-null input formatters
if (inputFilter != null) {
inputFormatters.add(inputFilter);
}
if (textCapitalization != TextCapitalization.none) {
inputFormatters.add(TextCapitalizationFormatter(textCapitalization));
}

Widget dropDown = DropdownMenu<String>(
enabled: !disabled,
enableFilter: widget.control.attrBool("enableFilter", false)!,
enableSearch: widget.control.attrBool("enableSearch", true)!,
errorText: widget.control.attrString("errorText"),
helperText: widget.control.attrString("helperText"),
hintText: widget.control.attrString("hintText"),
initialSelection: _value,
requestFocusOnTap: widget.control.attrBool("requestFocusOnTap", true)!,
menuHeight: widget.control.attrDouble("menuHeight"),
width: widget.control.attrDouble("width"),
textStyle: textStyle,
inputFormatters: inputFormatters,
expandedInsets: parseEdgeInsets(widget.control, "expandedInsets"),
menuStyle: parseMenuStyle(Theme.of(context), widget.control, "style"),
focusNode: _focusNode,
label: labelCtrl.isNotEmpty
? createControl(widget.control, labelCtrl.first.id, disabled)
: label != null
? Text(label,
style: parseTextStyle(
Theme.of(context), widget.control, "labelStyle"))
: null,
trailingIcon: suffixCtrl.isNotEmpty
? createControl(widget.control, suffixCtrl.first.id, disabled)
: suffixIcon != null
? Icon(parseIcon(suffixIcon))
: null,
leadingIcon: prefixCtrl.isNotEmpty
? createControl(widget.control, prefixCtrl.first.id, disabled)
: prefixIcon != null
? Icon(parseIcon(prefixIcon))
: null,
selectedTrailingIcon: selectedSuffixCtrl.isNotEmpty
? createControl(
widget.control, selectedSuffixCtrl.first.id, disabled)
: selectedSuffixIcon != null
? Icon(parseIcon(selectedSuffixIcon))
: null,
inputDecorationTheme:
buildInputDecorationTheme(context, widget.control, _focused),
onSelected: disabled
? null
: (String? value) {
debugPrint("DropdownMenu selected value: $value");
_value = value!;
widget.backend
.updateControlState(widget.control.id, {"value": value});
widget.backend
.triggerControlEvent(widget.control.id, "change", value);
},
dropdownMenuEntries: items,
);

return constrainedControl(
context, dropDown, widget.parent, widget.control);
});
}
}
26 changes: 17 additions & 9 deletions packages/flet/lib/src/controls/textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ class _TextFieldControlState extends State<TextFieldControl>
widget.children.where((c) => c.name == "prefix" && c.isVisible);
var suffixControls =
widget.children.where((c) => c.name == "suffix" && c.isVisible);
var counterControls =
widget.children.where((c) => c.name == "counter" && c.isVisible);
var helperControls =
widget.children.where((c) => c.name == "helper" && c.isVisible);
var errorControls =
widget.children.where((c) => c.name == "error" && c.isVisible);

bool shiftEnter = widget.control.attrBool("shiftEnter", false)!;
bool multiline =
Expand Down Expand Up @@ -220,15 +226,17 @@ class _TextFieldControlState extends State<TextFieldControl>
.triggerControlEvent(widget.control.id, "submit", value);
}
: null,
decoration: buildInputDecoration(
context,
widget.control,
prefixControls.isNotEmpty ? prefixControls.first : null,
suffixControls.isNotEmpty ? suffixControls.first : null,
revealPasswordIcon,
_focused,
disabled,
adaptive),
decoration: buildInputDecoration(context, widget.control,
prefix: prefixControls.isNotEmpty ? prefixControls.first : null,
suffix: suffixControls.isNotEmpty ? suffixControls.first : null,
counter:
counterControls.isNotEmpty ? counterControls.first : null,
helper: helperControls.isNotEmpty ? helperControls.first : null,
error: errorControls.isNotEmpty ? errorControls.first : null,
customSuffix: revealPasswordIcon,
focused: _focused,
disabled: disabled,
adaptive: adaptive),
showCursor: widget.control.attrBool("showCursor"),
textAlignVertical: textVerticalAlign != null
? TextAlignVertical(y: textVerticalAlign)
Expand Down
18 changes: 9 additions & 9 deletions packages/flet/lib/src/utils/buttons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import 'text.dart';
import 'theme.dart';

ButtonStyle? parseButtonStyle(ThemeData theme, Control control, String propName,
{required Color defaultForegroundColor,
required Color defaultBackgroundColor,
required Color defaultOverlayColor,
required Color defaultShadowColor,
required Color defaultSurfaceTintColor,
required double defaultElevation,
required EdgeInsets defaultPadding,
required BorderSide defaultBorderSide,
required OutlinedBorder defaultShape}) {
{Color? defaultForegroundColor,
Color? defaultBackgroundColor,
Color? defaultOverlayColor,
Color? defaultShadowColor,
Color? defaultSurfaceTintColor,
double? defaultElevation,
EdgeInsets? defaultPadding,
BorderSide? defaultBorderSide,
OutlinedBorder? defaultShape}) {
var v = control.attrString(propName, null);
if (v == null) {
return null;
Expand Down
Loading