Skip to content

Commit 14d029c

Browse files
authored
v1: patches + improvements (#5265)
* permit `Overlay` controls to use `Positioned` for absolute positioning * `ShaderMask.blend_mode` default value * new: `ResponsiveRow.breakpoints` (user-defined-breakpoints: #2944) * refactor `StoragePaths` to use methods instead of props * fix `CupertinoTimerPicker` * fix `OutlinedButton` * fix `SearchBar` * fix `AutoComplete` * change typing of ref in `BaseControl` * fix `AutoComplete`: make selection_index part of AutoCompleteSelectEvent * reformat code
1 parent 4c8c2a1 commit 14d029c

20 files changed

+274
-131
lines changed

packages/flet/lib/src/controls/auto_complete.dart

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@ class AutoCompleteControl extends StatelessWidget {
2020
var autoComplete = Autocomplete(
2121
optionsMaxHeight: control.getDouble("suggestions_max_height", 200)!,
2222
onSelected: (AutoCompleteSuggestion selection) {
23-
control.updateProperties(
24-
{"selected_index": suggestions.indexOf(selection)});
25-
control.triggerEvent(
26-
"select",
27-
AutoCompleteSuggestion(key: selection.key, value: selection.value)
28-
.toMap());
23+
control.triggerEvent("select", {
24+
"selection": selection.toMap(),
25+
"selection_index": suggestions.indexOf(selection)
26+
});
2927
},
3028
// optionsViewBuilder: optionsViewBuilder,
3129
optionsBuilder: (TextEditingValue textEditingValue) {

packages/flet/lib/src/controls/base_controls.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ Widget _positionedControl(
220220
);
221221
} else if (left != null || top != null || right != null || bottom != null) {
222222
var parent = control.parent;
223-
if (parent?.type != "Stack" && parent?.type != "Page") {
223+
if (!["Stack", "Page", "Overlay"].contains(parent?.type)) {
224224
return ErrorControl("Error displaying ${control.type}",
225225
description:
226226
"Control can be positioned absolutely with \"left\", \"top\", \"right\" and \"bottom\" properties inside Stack control only.");

packages/flet/lib/src/controls/cupertino_timer_picker.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,19 @@ class _CupertinoTimerPickerControlState
2626
Widget picker = CupertinoTimerPicker(
2727
mode: widget.control
2828
.getCupertinoTimerPickerMode("mode", CupertinoTimerPickerMode.hms)!,
29-
initialTimerDuration: widget.control.getDuration("value", Duration.zero)!,
29+
initialTimerDuration: widget.control
30+
.getDuration("value", Duration.zero, DurationUnit.seconds)!,
3031
minuteInterval: widget.control.getInt("minute_interval", 1)!,
3132
secondInterval: widget.control.getInt("second_interval", 1)!,
3233
itemExtent: widget.control.getDouble("item_extent", 32.0)!,
3334
alignment: widget.control.getAlignment("alignment", Alignment.center)!,
3435
backgroundColor: widget.control.getColor("bgcolor", context),
3536
onTimerDurationChanged: (duration) {
36-
widget.control.updateProperties({"value": duration});
37-
widget.control.triggerEvent("change", duration);
37+
// preserve (original) value's type
38+
final d =
39+
widget.control.get("value") is int ? duration.inSeconds : duration;
40+
widget.control.updateProperties({"value": d});
41+
widget.control.triggerEvent("change", d);
3842
},
3943
);
4044

packages/flet/lib/src/controls/responsive_row.dart

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart';
22

33
import '../models/control.dart';
44
import '../utils/alignment.dart';
5+
import '../utils/numbers.dart';
56
import '../utils/responsive.dart';
67
import '../widgets/error.dart';
78
import '../widgets/flet_store_mixin.dart';
@@ -20,23 +21,30 @@ class ResponsiveRowControl extends StatelessWidget with FletStoreMixin {
2021
final columns = control.getResponsiveNumber("columns", 12)!;
2122
final spacing = control.getResponsiveNumber("spacing", 10)!;
2223
final runSpacing = control.getResponsiveNumber("run_spacing", 10)!;
24+
2325
return withPageSize((context, view) {
2426
var result = LayoutBuilder(
2527
builder: (BuildContext context, BoxConstraints constraints) {
26-
debugPrint(
27-
"ResponsiveRow constraints: maxWidth=${constraints.maxWidth}, maxHeight=${constraints.maxHeight}");
28-
28+
// breakpoints
29+
final rawBreakpoints =
30+
control.get<Map>("breakpoints", view.breakpoints)!;
31+
final breakpoints = <String, double>{};
32+
rawBreakpoints.forEach((k, v) {
33+
final val = parseDouble(v);
34+
if (val != null) {
35+
breakpoints[k.toString()] = val;
36+
}
37+
});
2938
var bpSpacing =
30-
getBreakpointNumber(spacing, view.size.width, view.breakpoints);
39+
getBreakpointNumber(spacing, view.size.width, breakpoints);
3140
var bpColumns =
32-
getBreakpointNumber(columns, view.size.width, view.breakpoints);
41+
getBreakpointNumber(columns, view.size.width, breakpoints);
3342

3443
double totalCols = 0;
3544
List<Widget> controls = [];
3645
for (var ctrl in control.children("controls")) {
3746
final col = ctrl.getResponsiveNumber("col", 12)!;
38-
var bpCol =
39-
getBreakpointNumber(col, view.size.width, view.breakpoints);
47+
var bpCol = getBreakpointNumber(col, view.size.width, breakpoints);
4048
totalCols += bpCol;
4149

4250
// calculate child width
@@ -45,10 +53,8 @@ class ResponsiveRowControl extends StatelessWidget with FletStoreMixin {
4553
var childWidth = colWidth * bpCol + bpSpacing * (bpCol - 1);
4654

4755
controls.add(ConstrainedBox(
48-
constraints: BoxConstraints(
49-
minWidth: childWidth,
50-
maxWidth: childWidth,
51-
),
56+
constraints:
57+
BoxConstraints(minWidth: childWidth, maxWidth: childWidth),
5258
child: ControlWidget(key: key, control: ctrl),
5359
));
5460
}
@@ -78,10 +84,8 @@ class ResponsiveRowControl extends StatelessWidget with FletStoreMixin {
7884
children: controls,
7985
);
8086
} catch (e) {
81-
return ErrorControl(
82-
"Error displaying ResponsiveRow",
83-
description: e.toString(),
84-
);
87+
return ErrorControl("Error displaying ResponsiveRow",
88+
description: e.toString());
8589
}
8690
});
8791

packages/flet/lib/src/controls/shader_mask.dart

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import '../models/control.dart';
55
import '../utils/borders.dart';
66
import '../utils/gradient.dart';
77
import '../utils/images.dart';
8-
import '../utils/numbers.dart';
98
import '../widgets/error.dart';
109
import 'base_controls.dart';
1110

@@ -22,12 +21,8 @@ class ShaderMaskControl extends StatelessWidget {
2221
return const ErrorControl("ShaderMask.shader must be provided");
2322
}
2423
final shaderMask = ShaderMask(
25-
shaderCallback: (bounds) {
26-
debugPrint("shaderCallback: $bounds, $gradient");
27-
return gradient.createShader(bounds);
28-
},
29-
blendMode: parseBlendMode(
30-
control.getString("blend_mode"), BlendMode.modulate)!,
24+
shaderCallback: (bounds) => gradient.createShader(bounds),
25+
blendMode: control.getBlendMode("blend_mode", BlendMode.modulate)!,
3126
child: control.buildWidget("content"));
3227
return ConstrainedControl(
3328
control: control,
@@ -37,10 +32,7 @@ class ShaderMaskControl extends StatelessWidget {
3732

3833
Widget _clipCorners(Widget widget, {BorderRadius? borderRadius}) {
3934
return borderRadius != null
40-
? ClipRRect(
41-
borderRadius: borderRadius,
42-
child: widget,
43-
)
35+
? ClipRRect(borderRadius: borderRadius, child: widget)
4436
: widget;
4537
}
4638
}

packages/flet/lib/src/services/storage_paths.dart

Lines changed: 45 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,55 @@ class StoragePaths extends FletService {
1212
void init() {
1313
super.init();
1414
debugPrint("StoragePaths(${control.id}).init: ${control.properties}");
15-
updateValues();
15+
control.addInvokeMethodListener(_invokeMethod);
1616
}
1717

18-
@override
19-
void update() {
20-
debugPrint("StoragePaths(${control.id}).update: ${control.properties}");
21-
updateValues();
22-
}
23-
24-
void updateValues() async {
18+
Future<dynamic> _invokeMethod(String name, dynamic args) async {
19+
debugPrint("StoragePaths.$name($args)");
2520
// path_provider doesn't support web
2621
if (!isWebPlatform()) {
27-
var applicationCacheDirectory =
28-
(await getApplicationCacheDirectory()).path;
29-
var consoleLogFilename =
30-
path.join(applicationCacheDirectory, "console.log");
31-
var applicationDocumentsDirectory =
32-
(await getApplicationDocumentsDirectory()).path;
33-
var applicationSupportDirectory =
34-
(await getApplicationSupportDirectory()).path;
35-
var downloadsDirectory = (await getDownloadsDirectory())?.path;
36-
var externalCacheDirectories = isAndroidMobile()
37-
? (await getExternalCacheDirectories())?.map((e) => e.path).toList()
38-
: null;
39-
var externalStorageDirectories = isAndroidMobile()
40-
? (await getExternalStorageDirectories())?.map((e) => e.path).toList()
41-
: null;
42-
var libraryDirectory =
43-
isApplePlatform() ? (await getLibraryDirectory()).path : null;
44-
var externalCacheDirectory = isAndroidMobile()
45-
? (await getExternalStorageDirectory())?.path
46-
: null;
47-
var temporaryDirectory = (await getTemporaryDirectory()).path;
48-
49-
control.updateProperties({
50-
"application_cache_directory": applicationCacheDirectory,
51-
"application_documents_directory": applicationDocumentsDirectory,
52-
"application_support_directory": applicationSupportDirectory,
53-
"downloads_directory": downloadsDirectory,
54-
"external_cache_directories": externalCacheDirectories,
55-
"external_storage_directories": externalStorageDirectories,
56-
"library_directory": libraryDirectory,
57-
"external_cache_directory": externalCacheDirectory,
58-
"temporary_directory": temporaryDirectory,
59-
"console_log_filename": consoleLogFilename,
60-
});
22+
switch (name) {
23+
case "get_application_cache_directory":
24+
return (await getApplicationCacheDirectory()).path;
25+
case "get_application_documents_directory":
26+
return (await getApplicationDocumentsDirectory()).path;
27+
case "get_application_support_directory":
28+
return (await getApplicationSupportDirectory()).path;
29+
case "get_downloads_directory":
30+
return (await getDownloadsDirectory())?.path;
31+
case "get_external_cache_directories":
32+
return isAndroidMobile()
33+
? (await getExternalCacheDirectories())
34+
?.map((e) => e.path)
35+
.toList()
36+
: null;
37+
case "get_external_storage_directories":
38+
return isAndroidMobile()
39+
? (await getExternalStorageDirectories())
40+
?.map((e) => e.path)
41+
.toList()
42+
: null;
43+
case "get_library_directory":
44+
return isApplePlatform() ? (await getLibraryDirectory()).path : null;
45+
case "get_external_cache_directory":
46+
return isAndroidMobile()
47+
? (await getExternalStorageDirectory())?.path
48+
: null;
49+
case "get_temporary_directory":
50+
return (await getTemporaryDirectory()).path;
51+
case "get_console_log_filename":
52+
return path.join(
53+
(await getApplicationCacheDirectory()).path, "console.log");
54+
default:
55+
throw Exception("Unknown StoragePaths method: $name");
56+
}
6157
}
6258
}
59+
60+
@override
61+
void dispose() {
62+
debugPrint("StoragePaths(${control.id}).dispose()");
63+
control.removeInvokeMethodListener(_invokeMethod);
64+
super.dispose();
65+
}
6366
}

packages/flet/lib/src/services/url_launcher.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:url_launcher/url_launcher.dart';
33

44
import '../flet_service.dart';
55
import '../utils/launch_url.dart';
6+
import '../utils/numbers.dart';
67

78
class UrlLauncherService extends FletService {
89
UrlLauncherService({required super.control});
@@ -26,9 +27,9 @@ class UrlLauncherService extends FletService {
2627
case "launch_url":
2728
return openWebBrowser(args["url"]!,
2829
webWindowName: args["web_window_name"],
29-
webPopupWindow: args["web_popup_window"],
30-
windowWidth: args["window_width"],
31-
windowHeight: args["window_height"]);
30+
webPopupWindow: parseBool(args["web_popup_window"]),
31+
windowWidth: parseInt(args["window_width"]),
32+
windowHeight: parseInt(args["window_height"]));
3233
case "can_launch_url":
3334
return canLaunchUrl(Uri.parse(args["url"]!));
3435
case "close_in_app_web_view":

packages/flet/lib/src/utils/responsive.dart

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'package:flutter/foundation.dart';
2-
31
import '../models/control.dart';
42
import '../utils/numbers.dart';
53

@@ -25,30 +23,29 @@ Map<String, double>? parseResponsiveNumber(dynamic value, double defaultValue) {
2523

2624
double getBreakpointNumber(
2725
Map<String, double> value, double width, Map<String, double> breakpoints) {
28-
// default value
29-
double? result = value[""];
26+
// Defaults
27+
double? selectedValue = value[""];
28+
double highestMatchedBreakpoint = 0;
3029

31-
debugPrint("getBreakpointNumber: $value, $width, $breakpoints");
30+
for (final entry in value.entries) {
31+
final bpName = entry.key;
32+
final v = entry.value;
3233

33-
double maxBpWidth = 0;
34-
value.forEach((bpName, respValue) {
35-
if (bpName == "") {
36-
return;
37-
}
38-
var bpWidth = breakpoints[bpName];
39-
if (bpWidth == null) {
40-
throw Exception("Unknown breakpoint: $bpName");
41-
}
42-
if (width >= bpWidth && bpWidth >= maxBpWidth) {
43-
maxBpWidth = bpWidth;
44-
result = respValue;
34+
if (bpName.isEmpty) continue;
35+
36+
final bpWidth = breakpoints[bpName];
37+
if (bpWidth == null) continue;
38+
39+
if (width >= bpWidth && bpWidth >= highestMatchedBreakpoint) {
40+
highestMatchedBreakpoint = bpWidth;
41+
selectedValue = v;
4542
}
46-
});
43+
}
4744

48-
if (result == null) {
45+
if (selectedValue == null) {
4946
throw Exception("Responsive number not found for width=$width: $value");
5047
}
51-
return result!;
48+
return selectedValue;
5249
}
5350

5451
extension ResponsiveParsers on Control {

packages/flet/lib/src/utils/time.dart

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,37 @@ import 'package:flutter/material.dart';
55
import '../models/control.dart';
66
import 'numbers.dart';
77

8-
Duration? parseDuration(dynamic value, [Duration? defaultValue]) {
8+
enum DurationUnit { microseconds, milliseconds, seconds, minutes, hours, days }
9+
10+
/// Parses a dynamic [value] into a [Duration] object.
11+
///
12+
/// Supported input types:
13+
/// - `null`: Returns [defaultValue].
14+
/// - `num` (int or double): Interpreted as a single time unit specified by [treatNumAs].
15+
/// - `Map`: Must contain one or more of the following keys with numeric values:
16+
/// `days`, `hours`, `minutes`, `seconds`, `milliseconds`, `microseconds`.
17+
///
18+
/// Parameters:
19+
/// - [value]: The input to parse. Can be `null`, `num`, or `Map<String, dynamic>`.
20+
/// - [defaultValue]: The value to return if [value] is `null`.
21+
/// - [treatNumAs]: Specifies the unit of time for numeric input. Defaults to `DurationUnit.milliseconds`.
22+
///
23+
/// Returns:
24+
/// A [Duration] constructed from the parsed input, or [defaultValue] if input is `null`.
25+
Duration? parseDuration(dynamic value,
26+
[Duration? defaultValue,
27+
DurationUnit treatNumAs = DurationUnit.milliseconds]) {
928
if (value == null) return defaultValue;
10-
if (value is int || value is double) {
11-
return Duration(milliseconds: parseInt(value, 0)!);
29+
if (value is num) {
30+
final v = parseInt(value, 0)!;
31+
return Duration(
32+
microseconds: treatNumAs == DurationUnit.microseconds ? v : 0,
33+
milliseconds: treatNumAs == DurationUnit.milliseconds ? v : 0,
34+
seconds: treatNumAs == DurationUnit.seconds ? v : 0,
35+
minutes: treatNumAs == DurationUnit.minutes ? v : 0,
36+
hours: treatNumAs == DurationUnit.hours ? v : 0,
37+
days: treatNumAs == DurationUnit.days ? v : 0,
38+
);
1239
}
1340
return Duration(
1441
days: parseInt(value["days"], 0)!,
@@ -68,8 +95,20 @@ DatePickerMode? parseDatePickerMode(String? value,
6895
}
6996

7097
extension TimeParsers on Control {
71-
Duration? getDuration(String propertyName, [Duration? defaultValue]) {
72-
return parseDuration(get(propertyName), defaultValue);
98+
/// Retrieves and parses a duration value from the control's properties.
99+
///
100+
/// Parameters:
101+
/// - [propertyName]: The name of the property to retrieve the value from.
102+
/// - [defaultValue]: The value to return if the property is not set or is `null`.
103+
/// - [treatNumAs]: Specifies the unit of time for numeric input. Defaults to `DurationUnit.milliseconds`.
104+
///
105+
///
106+
/// Returns:
107+
/// A [Duration] based on the property's value, or [defaultValue] if the value is `null`.
108+
Duration? getDuration(String propertyName,
109+
[Duration? defaultValue,
110+
DurationUnit treatNumAs = DurationUnit.milliseconds]) {
111+
return parseDuration(get(propertyName), defaultValue, treatNumAs);
73112
}
74113

75114
DatePickerDateOrder? getDatePickerDateOrder(String propertyName,

0 commit comments

Comments
 (0)