Skip to content

Commit

Permalink
CupertinoTimerPicker and CupertinorPicker Controls (#2743)
Browse files Browse the repository at this point in the history
* initial commit

* switch to ConstrainedControl

* remove children in CupertinoTimerPickerControl

* CupertinoTimerPicker: update types and defaults

* CupertinoPicker: initial commit

* Fix python side

* Fix pickers

* Make time picker work in seconds

---------

Co-authored-by: Feodor Fitsner <[email protected]>
  • Loading branch information
ndonkoHenri and FeodorFitsner authored Mar 2, 2024
1 parent f3b8b33 commit e172698
Show file tree
Hide file tree
Showing 9 changed files with 685 additions and 25 deletions.
18 changes: 18 additions & 0 deletions packages/flet/lib/src/controls/create_control.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,12 @@ import 'cupertino_checkbox.dart';
import 'cupertino_dialog_action.dart';
import 'cupertino_list_tile.dart';
import 'cupertino_navigation_bar.dart';
import 'cupertino_picker.dart';
import 'cupertino_radio.dart';
import 'cupertino_slider.dart';
import 'cupertino_switch.dart';
import 'cupertino_textfield.dart';
import 'cupertino_timer_picker.dart';
import 'datatable.dart';
import 'date_picker.dart';
import 'dismissible.dart';
Expand Down Expand Up @@ -486,6 +488,22 @@ Widget createWidget(
children: controlView.children,
parentDisabled: parentDisabled,
backend: backend);
case "cupertinotimerpicker":
return CupertinoTimerPickerControl(
parent: parent,
control: controlView.control,
parentDisabled: parentDisabled,
nextChild: nextChild,
backend: backend);
case "cupertinopicker":
return CupertinoPickerControl(
parent: parent,
control: controlView.control,
children: controlView.children,
parentAdaptive: parentAdaptive,
parentDisabled: parentDisabled,
nextChild: nextChild,
backend: backend);
case "draggable":
return DraggableControl(
key: key,
Expand Down
147 changes: 147 additions & 0 deletions packages/flet/lib/src/controls/cupertino_picker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import '../flet_control_backend.dart';
import '../models/control.dart';
import '../utils/colors.dart';
import 'create_control.dart';
import 'error.dart';

const double _kItemExtent = 32.0;
const double _kDefaultDiameterRatio = 1.07;
const double _kSqueeze = 1.45;

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

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

@override
State<CupertinoPickerControl> createState() => _CupertinoPickerControlState();
}

class _CupertinoPickerControlState extends State<CupertinoPickerControl> {
Widget _createPicker() {
bool disabled = widget.control.isDisabled || widget.parentDisabled;

List<Widget> ctrls = widget.children.where((c) => c.isVisible).map((c) {
return Center(
child: createControl(widget.control, c.id, disabled,
parentAdaptive: widget.parentAdaptive));
}).toList();

double itemExtent = widget.control.attrDouble("itemExtent", _kItemExtent)!;
int? selectedIndex = widget.control.attrInt("selectedIndex");
double diameterRatio =
widget.control.attrDouble("diameterRatio", _kDefaultDiameterRatio)!;
double magnification = widget.control.attrDouble("magnification", 1.0)!;
double squeeze = widget.control.attrDouble("squeeze", _kSqueeze)!;
double offAxisFraction = widget.control.attrDouble("offAxisFraction", 0.0)!;
bool useMagnifier = widget.control.attrBool("useMagnifier", false)!;
bool looping = widget.control.attrBool("looping", false)!;
Color? backgroundColor = HexColor.fromString(
Theme.of(context), widget.control.attrString("bgColor", "")!);

Widget picker = CupertinoPicker(
backgroundColor: backgroundColor,
diameterRatio: diameterRatio,
magnification: magnification,
squeeze: squeeze,
offAxisFraction: offAxisFraction,
itemExtent: itemExtent,
useMagnifier: useMagnifier,
looping: looping,
onSelectedItemChanged: (int index) {
widget.backend.updateControlState(
widget.control.id, {"selectedIndex": index.toString()});
widget.backend
.triggerControlEvent(widget.control.id, "change", index.toString());
},
scrollController: selectedIndex != null
? FixedExtentScrollController(initialItem: selectedIndex)
: null,
children: ctrls,
);

return Container(
height: 216,
padding: const EdgeInsets.only(top: 6.0),
// The Bottom margin is provided to align the popup above the system navigation bar.
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
// Provide a background color for the popup.
color: CupertinoColors.systemBackground.resolveFrom(context),
// Use a SafeArea widget to avoid system overlaps.
child: SafeArea(
top: false,
child: picker,
),
);
}

@override
Widget build(BuildContext context) {
debugPrint("CupertinoPicker build: ${widget.control.id}");

bool lastOpen = widget.control.state["open"] ?? false;

var open = widget.control.attrBool("open", false)!;
var modal = widget.control.attrBool("modal", false)!;

debugPrint("Current open state: $lastOpen");
debugPrint("New open state: $open");

if (open && (open != lastOpen)) {
var dialog = _createPicker();
if (dialog is ErrorControl) {
return dialog;
}

// close previous dialog
if (ModalRoute.of(context)?.isCurrent != true) {
Navigator.of(context).pop();
}

widget.control.state["open"] = open;

WidgetsBinding.instance.addPostFrameCallback((_) {
showCupertinoModalPopup(
barrierDismissible: !modal,
useRootNavigator: false,
context: context,
builder: (context) => _createPicker()).then((value) {
lastOpen = widget.control.state["open"] ?? false;
debugPrint("Picker should be dismissed ($hashCode): $lastOpen");
bool shouldDismiss = lastOpen;
widget.control.state["open"] = false;

if (shouldDismiss) {
widget.backend
.updateControlState(widget.control.id, {"open": "false"});
widget.backend
.triggerControlEvent(widget.control.id, "dismiss", "");
}
});
});
} else if (open != lastOpen && lastOpen) {
Navigator.of(context).pop();
}

return widget.nextChild ?? const SizedBox.shrink();
}
}
132 changes: 132 additions & 0 deletions packages/flet/lib/src/controls/cupertino_timer_picker.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

import '../flet_control_backend.dart';
import '../models/control.dart';
import '../utils/alignment.dart';
import '../utils/colors.dart';
import 'error.dart';

class CupertinoTimerPickerControl extends StatefulWidget {
final Control? parent;
final Control control;
final bool parentDisabled;
final Widget? nextChild;

final FletControlBackend backend;

const CupertinoTimerPickerControl(
{super.key,
this.parent,
required this.control,
required this.parentDisabled,
required this.nextChild,
required this.backend});

@override
State<CupertinoTimerPickerControl> createState() =>
_CupertinoTimerPickerControlState();
}

class _CupertinoTimerPickerControlState
extends State<CupertinoTimerPickerControl> {
Widget _createPicker() {
int value = widget.control.attrInt("value", 0)!;
Duration initialTimerDuration = Duration(seconds: value);
int minuteInterval =
widget.control.attrDouble("minuteInterval", 1)!.toInt();
int secondInterval =
widget.control.attrDouble("secondInterval", 1)!.toInt();
CupertinoTimerPickerMode mode = CupertinoTimerPickerMode.values.firstWhere(
(a) =>
a.name.toLowerCase() ==
widget.control.attrString("mode", "")!.toLowerCase(),
orElse: () => CupertinoTimerPickerMode.hms);

Color? backgroundColor = HexColor.fromString(
Theme.of(context), widget.control.attrString("bgColor", "")!);

Widget picker = CupertinoTimerPicker(
mode: mode,
initialTimerDuration: initialTimerDuration,
minuteInterval: minuteInterval,
secondInterval: secondInterval,
alignment:
parseAlignment(widget.control, "alignment") ?? Alignment.center,
backgroundColor: backgroundColor,
onTimerDurationChanged: (Duration d) {
widget.backend.updateControlState(
widget.control.id, {"value": d.inSeconds.toString()});
widget.backend.triggerControlEvent(
widget.control.id, "change", d.inSeconds.toString());
},
);

return Container(
height: 216,
padding: const EdgeInsets.only(top: 6.0),
// The Bottom margin is provided to align the popup above the system navigation bar.
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
// Provide a background color for the popup.
color: CupertinoColors.systemBackground.resolveFrom(context),
// Use a SafeArea widget to avoid system overlaps.
child: SafeArea(
top: false,
child: picker,
),
);
}

@override
Widget build(BuildContext context) {
debugPrint("CupertinoTimerPicker build: ${widget.control.id}");

bool lastOpen = widget.control.state["open"] ?? false;

var open = widget.control.attrBool("open", false)!;
var modal = widget.control.attrBool("modal", false)!;

debugPrint("Current open state: $lastOpen");
debugPrint("New open state: $open");

if (open && (open != lastOpen)) {
var dialog = _createPicker();
if (dialog is ErrorControl) {
return dialog;
}

// close previous dialog
if (ModalRoute.of(context)?.isCurrent != true) {
Navigator.of(context).pop();
}

widget.control.state["open"] = open;

WidgetsBinding.instance.addPostFrameCallback((_) {
showCupertinoModalPopup(
barrierDismissible: !modal,
useRootNavigator: false,
context: context,
builder: (context) => _createPicker()).then((value) {
lastOpen = widget.control.state["open"] ?? false;
debugPrint("Picker should be dismissed ($hashCode): $lastOpen");
bool shouldDismiss = lastOpen;
widget.control.state["open"] = false;

if (shouldDismiss) {
widget.backend
.updateControlState(widget.control.id, {"open": "false"});
widget.backend
.triggerControlEvent(widget.control.id, "dismiss", "");
}
});
});
} else if (open != lastOpen && lastOpen) {
Navigator.of(context).pop();
}

return widget.nextChild ?? const SizedBox.shrink();
}
}
5 changes: 5 additions & 0 deletions sdk/python/packages/flet-core/src/flet_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,15 @@
from flet_core.cupertino_filled_button import CupertinoFilledButton
from flet_core.cupertino_list_tile import CupertinoListTile
from flet_core.cupertino_navigation_bar import CupertinoNavigationBar
from flet_core.cupertino_picker import CupertinoPicker
from flet_core.cupertino_radio import CupertinoRadio
from flet_core.cupertino_slider import CupertinoSlider
from flet_core.cupertino_switch import CupertinoSwitch
from flet_core.cupertino_textfield import CupertinoTextField, VisibilityMode
from flet_core.cupertino_timer_picker import (
CupertinoTimerPicker,
CupertinoTimerPickerMode,
)
from flet_core.datatable import (
DataCell,
DataColumn,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
from typing import Any, List, Optional, Union

from flet_core.adaptive_control import AdaptiveControl
from flet_core.control import Control, OptionalNumber
from flet_core.ref import Ref
from flet_core.types import (
AnimationValue,
OffsetValue,
ResponsiveNumber,
RotateValue,
ScaleValue,
)


class CupertinoActionSheet(AdaptiveControl):
class CupertinoActionSheet(Control):
"""
An iOS-style action sheet.
Expand All @@ -31,13 +23,12 @@ def __init__(
open: bool = False,
on_dismiss=None,
#
# ConstrainedControl and AdaptiveControl
# Control
#
ref: Optional[Ref] = None,
visible: Optional[bool] = None,
disabled: Optional[bool] = None,
data: Any = None,
adaptive: Optional[bool] = None,
):
Control.__init__(
self,
Expand All @@ -47,8 +38,6 @@ def __init__(
data=data,
)

AdaptiveControl.__init__(self, adaptive=adaptive)

self.cancel = cancel
self.title = title
self.message = message
Expand Down
Loading

0 comments on commit e172698

Please sign in to comment.