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
2 changes: 0 additions & 2 deletions ci/update-flet-wheel-deps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
Expand Down
19 changes: 9 additions & 10 deletions packages/flet/lib/src/controls/animated_switcher.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:flet/src/utils/time.dart';
import 'package:flutter/material.dart';

import '../models/control.dart';
Expand Down Expand Up @@ -27,12 +28,6 @@ class AnimatedSwitcherControl extends StatelessWidget {
var contentCtrls =
children.where((c) => c.name == "content" && c.isVisible);

var switchInCurve =
parseCurve(control.attrString("switchInCurve"), Curves.linear)!;
var switchOutCurve =
parseCurve(control.attrString("switchOutCurve"), Curves.linear)!;
var duration = control.attrInt("duration", 1000)!;
var reverseDuration = control.attrInt("reverseDuration", 1000)!;
bool disabled = control.isDisabled || parentDisabled;

if (contentCtrls.isEmpty) {
Expand All @@ -46,10 +41,14 @@ class AnimatedSwitcherControl extends StatelessWidget {
return constrainedControl(
context,
AnimatedSwitcher(
duration: Duration(milliseconds: duration),
reverseDuration: Duration(milliseconds: reverseDuration),
switchInCurve: switchInCurve,
switchOutCurve: switchOutCurve,
duration: parseDuration(
control, "duration", const Duration(milliseconds: 1000))!,
reverseDuration: parseDuration(control, "reverseDuration",
const Duration(milliseconds: 1000))!,
switchInCurve:
parseCurve(control.attrString("switchInCurve"), Curves.linear)!,
switchOutCurve: parseCurve(
control.attrString("switchOutCurve"), Curves.linear)!,
transitionBuilder: (child, animation) {
switch (control.attrString("transition", "")!.toLowerCase()) {
case "rotation":
Expand Down
10 changes: 10 additions & 0 deletions packages/flet/lib/src/controls/create_control.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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 @@ -835,6 +836,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
9 changes: 5 additions & 4 deletions packages/flet/lib/src/controls/dismissible.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import '../flet_control_backend.dart';
import '../models/control.dart';
import '../utils/dismissible.dart';
import '../utils/time.dart';
import 'create_control.dart';
import 'error.dart';

Expand Down Expand Up @@ -121,10 +122,10 @@ class _DismissibleControlState extends State<DismissibleControl> {
return completer.future;
}
: null,
movementDuration: Duration(
milliseconds: widget.control.attrInt("duration", 200)!),
resizeDuration: Duration(
milliseconds: widget.control.attrInt("resizeDuration", 300)!),
movementDuration: parseDuration(widget.control, "movementDuration",
const Duration(milliseconds: 200))!,
resizeDuration: parseDuration(widget.control, "resizeDuration",
const Duration(milliseconds: 300))!,
crossAxisEndOffset:
widget.control.attrDouble("crossAxisEndOffset", 0.0)!,
dismissThresholds: dismissThresholds ?? {},
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);
});
}
}
1 change: 1 addition & 0 deletions packages/flet/lib/src/controls/navigation_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class _NavigationBarControlState extends State<NavigationBarControl>
if (_selectedIndex != selectedIndex) {
_selectedIndex = selectedIndex;
}

var navBar = withControls(
widget.children
.where((c) => c.isVisible && c.name == null)
Expand Down
17 changes: 8 additions & 9 deletions packages/flet/lib/src/controls/scrollable_control.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import '../models/control.dart';
import '../utils/animations.dart';
import '../utils/numbers.dart';
import '../utils/others.dart';
import '../utils/time.dart';
import 'flet_store_mixin.dart';

class ScrollableControl extends StatefulWidget {
Expand Down Expand Up @@ -83,7 +84,8 @@ class _ScrollableControlState extends State<ScrollableControl>
var params = Map<String, dynamic>.from(mj["p"] as Map);

if (name == "scroll_to") {
var duration = parseInt(params["duration"], 0)!;
var duration = durationFromJSON(params["duration"]) ?? Duration.zero;

var curve = parseCurve(params["curve"], Curves.ease)!;
if (params["key"] != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Expand All @@ -92,10 +94,7 @@ class _ScrollableControlState extends State<ScrollableControl>
var ctx = key.currentContext;
if (ctx != null) {
Scrollable.ensureVisible(ctx,
duration: duration > 0
? Duration(milliseconds: duration)
: Duration.zero,
curve: curve);
duration: duration, curve: curve);
}
}
});
Expand All @@ -105,12 +104,12 @@ class _ScrollableControlState extends State<ScrollableControl>
if (offset < 0) {
offset = _controller.position.maxScrollExtent + offset + 1;
}
if (duration < 1) {
if (duration.compareTo(const Duration(milliseconds: 1)) < 0) {
_controller.jumpTo(offset);
} else {
_controller.animateTo(
offset,
duration: Duration(milliseconds: duration),
duration: duration,
curve: curve,
);
}
Expand All @@ -119,12 +118,12 @@ class _ScrollableControlState extends State<ScrollableControl>
WidgetsBinding.instance.addPostFrameCallback((_) {
var delta = parseDouble(params["delta"], 0)!;
var offset = _controller.position.pixels + delta;
if (duration < 1) {
if (duration.compareTo(const Duration(milliseconds: 1)) < 0) {
_controller.jumpTo(offset);
} else {
_controller.animateTo(
offset,
duration: Duration(milliseconds: duration),
duration: duration,
curve: curve,
);
}
Expand Down
Loading