diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index 1b969c3eb..7c11c0acc 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -75,6 +75,7 @@ import 'tooltip.dart'; import 'transparent_pointer.dart'; import 'vertical_divider.dart'; import 'window_drag_area.dart'; +import 'range_slider.dart'; Widget createControl(Control? parent, String id, bool parentDisabled, {Widget? nextChild}) { @@ -439,10 +440,20 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, dispatch: controlView.dispatch); case "slider": return SliderControl( - key: key, - parent: parent, - control: controlView.control, - parentDisabled: parentDisabled); + key: key, + parent: parent, + control: controlView.control, + parentDisabled: parentDisabled, + dispatch: controlView.dispatch, + ); + case "rangeslider": + return RangeSliderControl( + key: key, + parent: parent, + control: controlView.control, + parentDisabled: parentDisabled, + dispatch: controlView.dispatch, + ); case "radiogroup": return RadioGroupControl( key: key, diff --git a/package/lib/src/controls/date_picker.dart b/package/lib/src/controls/date_picker.dart index 87201df99..d65c07e14 100644 --- a/package/lib/src/controls/date_picker.dart +++ b/package/lib/src/controls/date_picker.dart @@ -80,7 +80,7 @@ class _DatePickerControlState extends State { value?.toIso8601String() ?? currentDate?.toIso8601String() ?? ""; eventName = "dismiss"; } else { - stringValue = dateValue?.toIso8601String() ?? ""; + stringValue = dateValue.toIso8601String(); eventName = "change"; } List> props = [ diff --git a/package/lib/src/controls/range_slider.dart b/package/lib/src/controls/range_slider.dart new file mode 100644 index 000000000..2c31f85ff --- /dev/null +++ b/package/lib/src/controls/range_slider.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import '../actions.dart'; +import '../flet_app_services.dart'; +import '../models/control.dart'; +import '../protocol/update_control_props_payload.dart'; +import '../utils/colors.dart'; +import '../utils/desktop.dart'; +import 'create_control.dart'; +import '../utils/buttons.dart'; +import '../utils/debouncer.dart'; + +class RangeSliderControl extends StatefulWidget { + final Control? parent; + final Control control; + final bool parentDisabled; + final dynamic dispatch; + + const RangeSliderControl({ + Key? key, + this.parent, + required this.control, + required this.parentDisabled, + required this.dispatch, + }) : super(key: key); + + @override + State createState() => _SliderControlState(); +} + +class _SliderControlState extends State { + final _debouncer = Debouncer(milliseconds: isDesktop() ? 10 : 100); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + _debouncer.dispose(); + super.dispose(); + } + + void onChange(double startValue, double endValue) { + var strStartValue = startValue.toString(); + var strEndValue = endValue.toString(); + + List> props = [ + { + "i": widget.control.id, + "startvalue": strStartValue, + "endvalue": strEndValue + } + ]; + widget.dispatch( + UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); + + _debouncer.run(() { + final server = FletAppServices.of(context).server; + server.updateControlProps(props: props); + server.sendPageEvent( + eventTarget: widget.control.id, eventName: "change", eventData: ''); + }); + } + + @override + Widget build(BuildContext context) { + debugPrint("RangeSliderControl build: ${widget.control.id}"); + + double startValue = widget.control.attrDouble("startvalue", 0)!; + double endValue = widget.control.attrDouble("endvalue", 0)!; + String? label = widget.control.attrString("label"); + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + double min = widget.control.attrDouble("min", 0)!; + double max = widget.control.attrDouble("max", 1)!; + + int? divisions = widget.control.attrInt("divisions"); + int round = widget.control.attrInt("round", 0)!; + + final server = FletAppServices.of(context).server; + + debugPrint("SliderControl StoreConnector build: ${widget.control.id}"); + + var rangeSlider = RangeSlider( + values: RangeValues(startValue, endValue), + labels: RangeLabels( + (label ?? "") + .replaceAll("{value}", startValue.toStringAsFixed(round)), + (label ?? "") + .replaceAll("{value}", endValue.toStringAsFixed(round))), + min: min, + max: max, + divisions: divisions, + activeColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("activeColor", "")!), + inactiveColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("inactiveColor", "")!), + overlayColor: parseMaterialStateColor( + Theme.of(context), widget.control, "overlayColor"), + onChanged: !disabled + ? (RangeValues newValues) { + onChange(newValues.start, newValues.end); + } + : null, + onChangeStart: !disabled + ? (RangeValues newValues) { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "change_start", + eventData: ''); + } + : null, + onChangeEnd: !disabled + ? (RangeValues newValues) { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "change_end", + eventData: ''); + } + : null); + + return constrainedControl( + context, rangeSlider, widget.parent, widget.control); + } +} diff --git a/package/lib/src/controls/slider.dart b/package/lib/src/controls/slider.dart index 2a31ddd7b..b5d527647 100644 --- a/package/lib/src/controls/slider.dart +++ b/package/lib/src/controls/slider.dart @@ -1,27 +1,25 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; -import 'package:flutter_redux/flutter_redux.dart'; - import '../actions.dart'; import '../flet_app_services.dart'; -import '../models/app_state.dart'; import '../models/control.dart'; import '../protocol/update_control_props_payload.dart'; import '../utils/colors.dart'; import '../utils/desktop.dart'; +import '../utils/debouncer.dart'; import 'create_control.dart'; class SliderControl extends StatefulWidget { final Control? parent; final Control control; final bool parentDisabled; + final dynamic dispatch; const SliderControl( {Key? key, this.parent, required this.control, - required this.parentDisabled}) + required this.parentDisabled, + required this.dispatch}) : super(key: key); @override @@ -30,7 +28,7 @@ class SliderControl extends StatefulWidget { class _SliderControlState extends State { double _value = 0; - Timer? _debounce; + final _debouncer = Debouncer(milliseconds: isDesktop() ? 10 : 100); late final FocusNode _focusNode; @override @@ -42,7 +40,7 @@ class _SliderControlState extends State { @override void dispose() { - _debounce?.cancel(); + _debouncer.dispose(); _focusNode.removeListener(_onFocusChange); _focusNode.dispose(); super.dispose(); @@ -55,26 +53,24 @@ class _SliderControlState extends State { eventData: ""); } - void onChange(double value, Function dispatch) { + void onChange(double value) { var svalue = value.toString(); debugPrint(svalue); setState(() { _value = value; }); - if (_debounce?.isActive ?? false) _debounce!.cancel(); List> props = [ {"i": widget.control.id, "value": svalue} ]; - dispatch(UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); + widget.dispatch( + UpdateControlPropsAction(UpdateControlPropsPayload(props: props))); - _debounce = Timer(Duration(milliseconds: isDesktop() ? 10 : 100), () { + _debouncer.run(() { final server = FletAppServices.of(context).server; server.updateControlProps(props: props); server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change", - eventData: svalue); + eventTarget: widget.control.id, eventName: "change", eventData: ''); }); } @@ -93,57 +89,49 @@ class _SliderControlState extends State { final server = FletAppServices.of(context).server; - return StoreConnector( - distinct: true, - converter: (store) => store.dispatch, - builder: (context, dispatch) { - debugPrint( - "SliderControl StoreConnector build: ${widget.control.id}"); - - double value = widget.control.attrDouble("value", 0)!; - if (_value != value) { - _value = value; - } - - var slider = Slider( - autofocus: autofocus, - focusNode: _focusNode, - value: _value, - min: min, - max: max, - divisions: divisions, - label: - label?.replaceAll("{value}", _value.toStringAsFixed(round)), - activeColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("activeColor", "")!), - inactiveColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("inactiveColor", "")!), - thumbColor: HexColor.fromString(Theme.of(context), - widget.control.attrString("thumbColor", "")!), - onChanged: !disabled - ? (double value) { - onChange(value, dispatch); - } - : null, - onChangeStart: !disabled - ? (double value) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change_start", - eventData: value.toString()); - } - : null, - onChangeEnd: !disabled - ? (double value) { - server.sendPageEvent( - eventTarget: widget.control.id, - eventName: "change_end", - eventData: value.toString()); - } - : null); - - return constrainedControl( - context, slider, widget.parent, widget.control); - }); + debugPrint("SliderControl StoreConnector build: ${widget.control.id}"); + + double value = widget.control.attrDouble("value", 0)!; + if (_value != value) { + _value = value; + } + + var slider = Slider( + autofocus: autofocus, + focusNode: _focusNode, + value: _value, + min: min, + max: max, + divisions: divisions, + label: label?.replaceAll("{value}", _value.toStringAsFixed(round)), + activeColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("activeColor", "")!), + inactiveColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("inactiveColor", "")!), + thumbColor: HexColor.fromString( + Theme.of(context), widget.control.attrString("thumbColor", "")!), + onChanged: !disabled + ? (double value) { + onChange(value); + } + : null, + onChangeStart: !disabled + ? (double value) { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "change_start", + eventData: value.toString()); + } + : null, + onChangeEnd: !disabled + ? (double value) { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "change_end", + eventData: value.toString()); + } + : null); + + return constrainedControl(context, slider, widget.parent, widget.control); } } diff --git a/package/lib/src/utils/debouncer.dart b/package/lib/src/utils/debouncer.dart new file mode 100644 index 000000000..759726997 --- /dev/null +++ b/package/lib/src/utils/debouncer.dart @@ -0,0 +1,18 @@ +import 'package:flutter/widgets.dart'; +import 'dart:async'; + +class Debouncer { + final int milliseconds; + Timer? _timer; + + Debouncer({required this.milliseconds}); + + void run(VoidCallback action) { + if (_timer?.isActive ?? false) _timer!.cancel(); + _timer = Timer(Duration(milliseconds: milliseconds), action); + } + + void dispose() { + _timer?.cancel(); + } +} diff --git a/sdk/python/packages/flet-core/src/flet_core/__init__.py b/sdk/python/packages/flet-core/src/flet_core/__init__.py index 7b12b1720..44cfdc762 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -204,3 +204,4 @@ from flet_core.vertical_divider import VerticalDivider from flet_core.view import View from flet_core.window_drag_area import WindowDragArea +from flet_core.range_slider import RangeSlider diff --git a/sdk/python/packages/flet-core/src/flet_core/range_slider.py b/sdk/python/packages/flet-core/src/flet_core/range_slider.py new file mode 100644 index 000000000..7b41f1621 --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/range_slider.py @@ -0,0 +1,286 @@ +from typing import Any, Optional, Union, Dict +from flet_core.constrained_control import ConstrainedControl +from flet_core.control import OptionalNumber +from flet_core.ref import Ref +from flet_core.types import ( + AnimationValue, + OffsetValue, + ResponsiveNumber, + RotateValue, + ScaleValue, + MaterialState, +) + + +class RangeSlider(ConstrainedControl): + """ + A Material Design range slider. Used to select a range from a range of values. + A range slider can be used to select from either a continuous or a discrete set of values. + The default is to use a continuous range of values from min to max. + + Example: + ``` + import flet as ft + + + def range_slider_changed(e): + print(f"On change! Values are ({e.control.start_value}, {e.control.end_value})") + + + def range_slider_started_change(e): + print( + f"On change start! Values are ({e.control.start_value}, {e.control.end_value})" + ) + + + def range_slider_ended_change(e): + print(f"On change end! Values are ({e.control.start_value}, {e.control.end_value})") + + + def main(page: ft.Page): + range_slider = ft.RangeSlider( + min=0, + max=50, + start_value=10, + divisions=10, + end_value=20, + inactive_color=ft.colors.GREEN_300, + active_color=ft.colors.GREEN_700, + overlay_color=ft.colors.GREEN_100, + on_change=range_slider_changed, + on_change_start=range_slider_started_change, + on_change_end=range_slider_ended_change, + label="{value}%", + ) + + page.add( + ft.Column( + horizontal_alignment=ft.CrossAxisAlignment.CENTER, + controls=[ + ft.Text("Range slider", size=20, weight=ft.FontWeight.BOLD), + range_slider, + ], + ) + ) + + + ft.app(target=main) + ``` + + ----- + + Online docs: https://flet.dev/docs/controls/rangeslider + """ + + def __init__( + self, + start_value: [float], + end_value: [float], + ref: Optional[Ref] = None, + key: Optional[str] = None, + width: OptionalNumber = None, + height: OptionalNumber = None, + left: OptionalNumber = None, + top: OptionalNumber = None, + right: OptionalNumber = None, + bottom: OptionalNumber = None, + expand: Union[None, bool, int] = None, + col: Optional[ResponsiveNumber] = None, + opacity: OptionalNumber = None, + rotate: RotateValue = None, + scale: ScaleValue = None, + offset: OffsetValue = None, + aspect_ratio: OptionalNumber = None, + animate_opacity: AnimationValue = None, + animate_size: AnimationValue = None, + animate_position: AnimationValue = None, + animate_rotation: AnimationValue = None, + animate_scale: AnimationValue = None, + animate_offset: AnimationValue = None, + on_animation_end=None, + tooltip: Optional[str] = None, + visible: Optional[bool] = None, + disabled: Optional[bool] = None, + data: Any = None, + # + # Specific + # + label: Optional[str] = None, + min: OptionalNumber = None, + max: OptionalNumber = None, + divisions: Optional[int] = None, + round: Optional[int] = None, + active_color: Optional[str] = None, + inactive_color: Optional[str] = None, + overlay_color: Union[None, str, Dict[MaterialState, str]] = None, + on_change=None, + on_change_start=None, + on_change_end=None, + ): + ConstrainedControl.__init__( + self, + ref=ref, + key=key, + width=width, + height=height, + left=left, + top=top, + right=right, + bottom=bottom, + expand=expand, + col=col, + opacity=opacity, + rotate=rotate, + scale=scale, + offset=offset, + aspect_ratio=aspect_ratio, + animate_opacity=animate_opacity, + animate_size=animate_size, + animate_position=animate_position, + animate_rotation=animate_rotation, + animate_scale=animate_scale, + animate_offset=animate_offset, + on_animation_end=on_animation_end, + tooltip=tooltip, + visible=visible, + disabled=disabled, + data=data, + ) + self.start_value = start_value + self.end_value = end_value + self.label = label + + self.min = min + self.max = max + self.divisions = divisions + self.round = round + self.active_color = active_color + self.inactive_color = inactive_color + self.overlay_color = overlay_color + self.on_change = on_change + self.on_change_start = on_change_start + self.on_change_end = on_change_end + + def _get_control_name(self): + return "rangeslider" + + def _before_build_command(self): + super()._before_build_command() + self._set_attr_json("overlayColor", self.__overlay_color) + + # start_value + @property + def start_value(self) -> float: + return self._get_attr("startvalue") + + @start_value.setter + def start_value(self, value: float): + self._set_attr("startvalue", value) + + # end_value + @property + def end_value(self) -> float: + return self._get_attr("endvalue") + + @end_value.setter + def end_value(self, value: float): + self._set_attr("endvalue", value) + + # label + @property + def label(self) -> str: + return self._get_attr("label") + + @label.setter + def label(self, value: str): + self._set_attr("label", value) + + # min + @property + def min(self) -> OptionalNumber: + return self._get_attr("min") + + @min.setter + def min(self, value: OptionalNumber): + self._set_attr("min", value) + + # max + @property + def max(self) -> OptionalNumber: + return self._get_attr("max") + + @max.setter + def max(self, value: OptionalNumber): + self._set_attr("max", value) + + # divisions + @property + def divisions(self) -> Optional[int]: + return self._get_attr("divisions") + + @divisions.setter + def divisions(self, value: Optional[int]): + self._set_attr("divisions", value) + + # round + @property + def round(self) -> Optional[int]: + return self._get_attr("round") + + @round.setter + def round(self, value: Optional[int]): + self._set_attr("round", value) + + # active_color + @property + def active_color(self): + return self._get_attr("activeColor") + + @active_color.setter + def active_color(self, value): + self._set_attr("activeColor", value) + + # inactive_color + @property + def inactive_color(self): + return self._get_attr("inactiveColor") + + @inactive_color.setter + def inactive_color(self, value): + self._set_attr("inactiveColor", value) + + # overlay_color + @property + def overlay_color(self) -> Union[None, str, Dict[MaterialState, str]]: + return self.__overlay_color + + @overlay_color.setter + def overlay_color(self, value: Union[None, str, Dict[MaterialState, str]]): + self.__overlay_color = value + + # on_change + @property + def on_change(self): + return self._get_event_handler("change") + + @on_change.setter + def on_change(self, handler): + self._add_event_handler("change", handler) + + # on_change_start + @property + def on_change_start(self): + return self._get_event_handler("change_start") + + @on_change_start.setter + def on_change_start(self, handler): + self._add_event_handler("change_start", handler) + + # on_change_end + @property + def on_change_end(self): + return self._get_event_handler("change_end") + + @on_change_end.setter + def on_change_end(self, handler): + self._add_event_handler("change_end", handler)