Skip to content

v1: patches + improvements #5265

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

Merged
merged 11 commits into from
May 3, 2025
10 changes: 4 additions & 6 deletions packages/flet/lib/src/controls/auto_complete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ class AutoCompleteControl extends StatelessWidget {
var autoComplete = Autocomplete(
optionsMaxHeight: control.getDouble("suggestions_max_height", 200)!,
onSelected: (AutoCompleteSuggestion selection) {
control.updateProperties(
{"selected_index": suggestions.indexOf(selection)});
control.triggerEvent(
"select",
AutoCompleteSuggestion(key: selection.key, value: selection.value)
.toMap());
control.triggerEvent("select", {
"selection": selection.toMap(),
"selection_index": suggestions.indexOf(selection)
});
},
// optionsViewBuilder: optionsViewBuilder,
optionsBuilder: (TextEditingValue textEditingValue) {
Expand Down
2 changes: 1 addition & 1 deletion packages/flet/lib/src/controls/base_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ Widget _positionedControl(
);
} else if (left != null || top != null || right != null || bottom != null) {
var parent = control.parent;
if (parent?.type != "Stack" && parent?.type != "Page") {
if (!["Stack", "Page", "Overlay"].contains(parent?.type)) {
return ErrorControl("Error displaying ${control.type}",
description:
"Control can be positioned absolutely with \"left\", \"top\", \"right\" and \"bottom\" properties inside Stack control only.");
Expand Down
10 changes: 7 additions & 3 deletions packages/flet/lib/src/controls/cupertino_timer_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ class _CupertinoTimerPickerControlState
Widget picker = CupertinoTimerPicker(
mode: widget.control
.getCupertinoTimerPickerMode("mode", CupertinoTimerPickerMode.hms)!,
initialTimerDuration: widget.control.getDuration("value", Duration.zero)!,
initialTimerDuration: widget.control
.getDuration("value", Duration.zero, DurationUnit.seconds)!,
minuteInterval: widget.control.getInt("minute_interval", 1)!,
secondInterval: widget.control.getInt("second_interval", 1)!,
itemExtent: widget.control.getDouble("item_extent", 32.0)!,
alignment: widget.control.getAlignment("alignment", Alignment.center)!,
backgroundColor: widget.control.getColor("bgcolor", context),
onTimerDurationChanged: (duration) {
widget.control.updateProperties({"value": duration});
widget.control.triggerEvent("change", duration);
// preserve (original) value's type
final d =
widget.control.get("value") is int ? duration.inSeconds : duration;
widget.control.updateProperties({"value": d});
widget.control.triggerEvent("change", d);
},
);

Expand Down
34 changes: 19 additions & 15 deletions packages/flet/lib/src/controls/responsive_row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/widgets.dart';

import '../models/control.dart';
import '../utils/alignment.dart';
import '../utils/numbers.dart';
import '../utils/responsive.dart';
import '../widgets/error.dart';
import '../widgets/flet_store_mixin.dart';
Expand All @@ -20,23 +21,30 @@ class ResponsiveRowControl extends StatelessWidget with FletStoreMixin {
final columns = control.getResponsiveNumber("columns", 12)!;
final spacing = control.getResponsiveNumber("spacing", 10)!;
final runSpacing = control.getResponsiveNumber("run_spacing", 10)!;

return withPageSize((context, view) {
var result = LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
debugPrint(
"ResponsiveRow constraints: maxWidth=${constraints.maxWidth}, maxHeight=${constraints.maxHeight}");

// breakpoints
final rawBreakpoints =
control.get<Map>("breakpoints", view.breakpoints)!;
final breakpoints = <String, double>{};
rawBreakpoints.forEach((k, v) {
final val = parseDouble(v);
if (val != null) {
breakpoints[k.toString()] = val;
}
});
var bpSpacing =
getBreakpointNumber(spacing, view.size.width, view.breakpoints);
getBreakpointNumber(spacing, view.size.width, breakpoints);
var bpColumns =
getBreakpointNumber(columns, view.size.width, view.breakpoints);
getBreakpointNumber(columns, view.size.width, breakpoints);

double totalCols = 0;
List<Widget> controls = [];
for (var ctrl in control.children("controls")) {
final col = ctrl.getResponsiveNumber("col", 12)!;
var bpCol =
getBreakpointNumber(col, view.size.width, view.breakpoints);
var bpCol = getBreakpointNumber(col, view.size.width, breakpoints);
totalCols += bpCol;

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

controls.add(ConstrainedBox(
constraints: BoxConstraints(
minWidth: childWidth,
maxWidth: childWidth,
),
constraints:
BoxConstraints(minWidth: childWidth, maxWidth: childWidth),
child: ControlWidget(key: key, control: ctrl),
));
}
Expand Down Expand Up @@ -78,10 +84,8 @@ class ResponsiveRowControl extends StatelessWidget with FletStoreMixin {
children: controls,
);
} catch (e) {
return ErrorControl(
"Error displaying ResponsiveRow",
description: e.toString(),
);
return ErrorControl("Error displaying ResponsiveRow",
description: e.toString());
}
});

Expand Down
14 changes: 3 additions & 11 deletions packages/flet/lib/src/controls/shader_mask.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import '../models/control.dart';
import '../utils/borders.dart';
import '../utils/gradient.dart';
import '../utils/images.dart';
import '../utils/numbers.dart';
import '../widgets/error.dart';
import 'base_controls.dart';

Expand All @@ -22,12 +21,8 @@ class ShaderMaskControl extends StatelessWidget {
return const ErrorControl("ShaderMask.shader must be provided");
}
final shaderMask = ShaderMask(
shaderCallback: (bounds) {
debugPrint("shaderCallback: $bounds, $gradient");
return gradient.createShader(bounds);
},
blendMode: parseBlendMode(
control.getString("blend_mode"), BlendMode.modulate)!,
shaderCallback: (bounds) => gradient.createShader(bounds),
blendMode: control.getBlendMode("blend_mode", BlendMode.modulate)!,
child: control.buildWidget("content"));
return ConstrainedControl(
control: control,
Expand All @@ -37,10 +32,7 @@ class ShaderMaskControl extends StatelessWidget {

Widget _clipCorners(Widget widget, {BorderRadius? borderRadius}) {
return borderRadius != null
? ClipRRect(
borderRadius: borderRadius,
child: widget,
)
? ClipRRect(borderRadius: borderRadius, child: widget)
: widget;
}
}
87 changes: 45 additions & 42 deletions packages/flet/lib/src/services/storage_paths.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,52 +12,55 @@ class StoragePaths extends FletService {
void init() {
super.init();
debugPrint("StoragePaths(${control.id}).init: ${control.properties}");
updateValues();
control.addInvokeMethodListener(_invokeMethod);
}

@override
void update() {
debugPrint("StoragePaths(${control.id}).update: ${control.properties}");
updateValues();
}

void updateValues() async {
Future<dynamic> _invokeMethod(String name, dynamic args) async {
debugPrint("StoragePaths.$name($args)");
// path_provider doesn't support web
if (!isWebPlatform()) {
var applicationCacheDirectory =
(await getApplicationCacheDirectory()).path;
var consoleLogFilename =
path.join(applicationCacheDirectory, "console.log");
var applicationDocumentsDirectory =
(await getApplicationDocumentsDirectory()).path;
var applicationSupportDirectory =
(await getApplicationSupportDirectory()).path;
var downloadsDirectory = (await getDownloadsDirectory())?.path;
var externalCacheDirectories = isAndroidMobile()
? (await getExternalCacheDirectories())?.map((e) => e.path).toList()
: null;
var externalStorageDirectories = isAndroidMobile()
? (await getExternalStorageDirectories())?.map((e) => e.path).toList()
: null;
var libraryDirectory =
isApplePlatform() ? (await getLibraryDirectory()).path : null;
var externalCacheDirectory = isAndroidMobile()
? (await getExternalStorageDirectory())?.path
: null;
var temporaryDirectory = (await getTemporaryDirectory()).path;

control.updateProperties({
"application_cache_directory": applicationCacheDirectory,
"application_documents_directory": applicationDocumentsDirectory,
"application_support_directory": applicationSupportDirectory,
"downloads_directory": downloadsDirectory,
"external_cache_directories": externalCacheDirectories,
"external_storage_directories": externalStorageDirectories,
"library_directory": libraryDirectory,
"external_cache_directory": externalCacheDirectory,
"temporary_directory": temporaryDirectory,
"console_log_filename": consoleLogFilename,
});
switch (name) {
case "get_application_cache_directory":
return (await getApplicationCacheDirectory()).path;
case "get_application_documents_directory":
return (await getApplicationDocumentsDirectory()).path;
case "get_application_support_directory":
return (await getApplicationSupportDirectory()).path;
case "get_downloads_directory":
return (await getDownloadsDirectory())?.path;
case "get_external_cache_directories":
return isAndroidMobile()
? (await getExternalCacheDirectories())
?.map((e) => e.path)
.toList()
: null;
case "get_external_storage_directories":
return isAndroidMobile()
? (await getExternalStorageDirectories())
?.map((e) => e.path)
.toList()
: null;
case "get_library_directory":
return isApplePlatform() ? (await getLibraryDirectory()).path : null;
case "get_external_cache_directory":
return isAndroidMobile()
? (await getExternalStorageDirectory())?.path
: null;
case "get_temporary_directory":
return (await getTemporaryDirectory()).path;
case "get_console_log_filename":
return path.join(
(await getApplicationCacheDirectory()).path, "console.log");
default:
throw Exception("Unknown StoragePaths method: $name");
}
}
}

@override
void dispose() {
debugPrint("StoragePaths(${control.id}).dispose()");
control.removeInvokeMethodListener(_invokeMethod);
super.dispose();
}
}
7 changes: 4 additions & 3 deletions packages/flet/lib/src/services/url_launcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:url_launcher/url_launcher.dart';

import '../flet_service.dart';
import '../utils/launch_url.dart';
import '../utils/numbers.dart';

class UrlLauncherService extends FletService {
UrlLauncherService({required super.control});
Expand All @@ -26,9 +27,9 @@ class UrlLauncherService extends FletService {
case "launch_url":
return openWebBrowser(args["url"]!,
webWindowName: args["web_window_name"],
webPopupWindow: args["web_popup_window"],
windowWidth: args["window_width"],
windowHeight: args["window_height"]);
webPopupWindow: parseBool(args["web_popup_window"]),
windowWidth: parseInt(args["window_width"]),
windowHeight: parseInt(args["window_height"]));
case "can_launch_url":
return canLaunchUrl(Uri.parse(args["url"]!));
case "close_in_app_web_view":
Expand Down
37 changes: 17 additions & 20 deletions packages/flet/lib/src/utils/responsive.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'package:flutter/foundation.dart';

import '../models/control.dart';
import '../utils/numbers.dart';

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

double getBreakpointNumber(
Map<String, double> value, double width, Map<String, double> breakpoints) {
// default value
double? result = value[""];
// Defaults
double? selectedValue = value[""];
double highestMatchedBreakpoint = 0;

debugPrint("getBreakpointNumber: $value, $width, $breakpoints");
for (final entry in value.entries) {
final bpName = entry.key;
final v = entry.value;

double maxBpWidth = 0;
value.forEach((bpName, respValue) {
if (bpName == "") {
return;
}
var bpWidth = breakpoints[bpName];
if (bpWidth == null) {
throw Exception("Unknown breakpoint: $bpName");
}
if (width >= bpWidth && bpWidth >= maxBpWidth) {
maxBpWidth = bpWidth;
result = respValue;
if (bpName.isEmpty) continue;

final bpWidth = breakpoints[bpName];
if (bpWidth == null) continue;

if (width >= bpWidth && bpWidth >= highestMatchedBreakpoint) {
highestMatchedBreakpoint = bpWidth;
selectedValue = v;
}
});
}

if (result == null) {
if (selectedValue == null) {
throw Exception("Responsive number not found for width=$width: $value");
}
return result!;
return selectedValue;
}

extension ResponsiveParsers on Control {
Expand Down
49 changes: 44 additions & 5 deletions packages/flet/lib/src/utils/time.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,37 @@ import 'package:flutter/material.dart';
import '../models/control.dart';
import 'numbers.dart';

Duration? parseDuration(dynamic value, [Duration? defaultValue]) {
enum DurationUnit { microseconds, milliseconds, seconds, minutes, hours, days }

/// Parses a dynamic [value] into a [Duration] object.
///
/// Supported input types:
/// - `null`: Returns [defaultValue].
/// - `num` (int or double): Interpreted as a single time unit specified by [treatNumAs].
/// - `Map`: Must contain one or more of the following keys with numeric values:
/// `days`, `hours`, `minutes`, `seconds`, `milliseconds`, `microseconds`.
///
/// Parameters:
/// - [value]: The input to parse. Can be `null`, `num`, or `Map<String, dynamic>`.
/// - [defaultValue]: The value to return if [value] is `null`.
/// - [treatNumAs]: Specifies the unit of time for numeric input. Defaults to `DurationUnit.milliseconds`.
///
/// Returns:
/// A [Duration] constructed from the parsed input, or [defaultValue] if input is `null`.
Duration? parseDuration(dynamic value,
[Duration? defaultValue,
DurationUnit treatNumAs = DurationUnit.milliseconds]) {
if (value == null) return defaultValue;
if (value is int || value is double) {
return Duration(milliseconds: parseInt(value, 0)!);
if (value is num) {
final v = parseInt(value, 0)!;
return Duration(
microseconds: treatNumAs == DurationUnit.microseconds ? v : 0,
milliseconds: treatNumAs == DurationUnit.milliseconds ? v : 0,
seconds: treatNumAs == DurationUnit.seconds ? v : 0,
minutes: treatNumAs == DurationUnit.minutes ? v : 0,
hours: treatNumAs == DurationUnit.hours ? v : 0,
days: treatNumAs == DurationUnit.days ? v : 0,
);
}
return Duration(
days: parseInt(value["days"], 0)!,
Expand Down Expand Up @@ -68,8 +95,20 @@ DatePickerMode? parseDatePickerMode(String? value,
}

extension TimeParsers on Control {
Duration? getDuration(String propertyName, [Duration? defaultValue]) {
return parseDuration(get(propertyName), defaultValue);
/// Retrieves and parses a duration value from the control's properties.
///
/// Parameters:
/// - [propertyName]: The name of the property to retrieve the value from.
/// - [defaultValue]: The value to return if the property is not set or is `null`.
/// - [treatNumAs]: Specifies the unit of time for numeric input. Defaults to `DurationUnit.milliseconds`.
///
///
/// Returns:
/// A [Duration] based on the property's value, or [defaultValue] if the value is `null`.
Duration? getDuration(String propertyName,
[Duration? defaultValue,
DurationUnit treatNumAs = DurationUnit.milliseconds]) {
return parseDuration(get(propertyName), defaultValue, treatNumAs);
}

DatePickerDateOrder? getDatePickerDateOrder(String propertyName,
Expand Down
Loading