diff --git a/package/lib/src/controls/create_control.dart b/package/lib/src/controls/create_control.dart index f2994244f..19be5e404 100644 --- a/package/lib/src/controls/create_control.dart +++ b/package/lib/src/controls/create_control.dart @@ -1,9 +1,8 @@ import 'dart:math'; -import 'package:collection/collection.dart'; -import 'package:flet/src/controls/search_anchor.dart'; -import 'package:flet/src/controls/segmented_button.dart'; import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; import 'package:flutter_redux/flutter_redux.dart'; import '../flet_app_services.dart'; @@ -14,6 +13,7 @@ import '../models/page_media_view_model.dart'; import '../utils/animations.dart'; import '../utils/theme.dart'; import '../utils/transforms.dart'; + import 'alert_dialog.dart'; import 'animated_switcher.dart'; import 'audio.dart'; @@ -59,6 +59,8 @@ import 'linechart.dart'; import 'list_tile.dart'; import 'list_view.dart'; import 'markdown.dart'; +import 'menu_bar.dart'; +import 'menu_item_button.dart'; import 'navigation_bar.dart'; import 'navigation_rail.dart'; import 'outlined_button.dart'; @@ -73,6 +75,8 @@ import 'range_slider.dart'; import 'responsive_row.dart'; import 'row.dart'; import 'safe_area.dart'; +import 'search_anchor.dart'; +import 'segmented_button.dart'; import 'selection_area.dart'; import 'semantics.dart'; import 'shader_mask.dart'; @@ -80,6 +84,7 @@ import 'shake_detector.dart'; import 'slider.dart'; import 'snack_bar.dart'; import 'stack.dart'; +import 'submenu_button.dart'; import 'switch.dart'; import 'tabs.dart'; import 'text.dart'; @@ -324,6 +329,27 @@ Widget createWidget(Key? key, ControlViewModel controlView, Control? parent, control: controlView.control, children: controlView.children, parentDisabled: parentDisabled); + case "menubar": + return MenuBarControl( + key: key, + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled); + case "submenubutton": + return SubMenuButtonControl( + key: key, + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled); + case "menuitembutton": + return MenuItemButtonControl( + key: key, + parent: parent, + control: controlView.control, + children: controlView.children, + parentDisabled: parentDisabled); case "segmentedbutton": return SegmentedButtonControl( key: key, diff --git a/package/lib/src/controls/cupertino_navigation_bar.dart b/package/lib/src/controls/cupertino_navigation_bar.dart index b7d2d4205..75ac5721a 100644 --- a/package/lib/src/controls/cupertino_navigation_bar.dart +++ b/package/lib/src/controls/cupertino_navigation_bar.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; @@ -22,13 +21,12 @@ class CupertinoNavigationBarControl extends StatefulWidget { final dynamic dispatch; const CupertinoNavigationBarControl( - {Key? key, + {super.key, this.parent, required this.control, required this.children, required this.parentDisabled, - required this.dispatch}) - : super(key: key); + required this.dispatch}); @override State createState() => diff --git a/package/lib/src/controls/gesture_detector.dart b/package/lib/src/controls/gesture_detector.dart index 1c642fd2d..0d98a3cf5 100644 --- a/package/lib/src/controls/gesture_detector.dart +++ b/package/lib/src/controls/gesture_detector.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import '../flet_app_services.dart'; import '../models/control.dart'; +import '../utils/mouse.dart'; import 'create_control.dart'; import 'error.dart'; @@ -584,85 +585,6 @@ class _GestureDetectorControlState extends State { return constrainedControl(context, result, widget.parent, widget.control); } - - MouseCursor parseMouseCursor(String? cursor) { - switch (cursor) { - case "alias": - return SystemMouseCursors.alias; - case "allScroll": - return SystemMouseCursors.allScroll; - case "basic": - return SystemMouseCursors.basic; - case "cell": - return SystemMouseCursors.cell; - case "click": - return SystemMouseCursors.click; - case "contextMenu": - return SystemMouseCursors.contextMenu; - case "copy": - return SystemMouseCursors.copy; - case "disappearing": - return SystemMouseCursors.disappearing; - case "forbidden": - return SystemMouseCursors.forbidden; - case "grab": - return SystemMouseCursors.grab; - case "grabbing": - return SystemMouseCursors.grabbing; - case "help": - return SystemMouseCursors.help; - case "move": - return SystemMouseCursors.move; - case "noDrop": - return SystemMouseCursors.noDrop; - case "none": - return SystemMouseCursors.none; - case "precise": - return SystemMouseCursors.precise; - case "progress": - return SystemMouseCursors.progress; - case "resizeColumn": - return SystemMouseCursors.resizeColumn; - case "resizeDown": - return SystemMouseCursors.resizeDown; - case "resizeDownLeft": - return SystemMouseCursors.resizeDownLeft; - case "resizeDownRight": - return SystemMouseCursors.resizeDownRight; - case "resizeLeft": - return SystemMouseCursors.resizeLeft; - case "resizeLeftRight": - return SystemMouseCursors.resizeLeftRight; - case "resizeRight": - return SystemMouseCursors.resizeRight; - case "resizeRow": - return SystemMouseCursors.resizeRow; - case "resizeUp": - return SystemMouseCursors.resizeUp; - case "resizeUpDown": - return SystemMouseCursors.resizeUpDown; - case "resizeUpLeft": - return SystemMouseCursors.resizeUpLeft; - case "resizeUpLeftDownRight": - return SystemMouseCursors.resizeUpLeftDownRight; - case "resizeUpRight": - return SystemMouseCursors.resizeUpRight; - case "resizeUpRightDownLeft": - return SystemMouseCursors.resizeUpRightDownLeft; - case "text": - return SystemMouseCursors.text; - case "verticalText": - return SystemMouseCursors.verticalText; - case "wait": - return SystemMouseCursors.wait; - case "zoomIn": - return SystemMouseCursors.zoomIn; - case "zoomOut": - return SystemMouseCursors.zoomOut; - default: - return MouseCursor.defer; - } - } } class MultiTouchGestureRecognizer extends MultiTapGestureRecognizer { diff --git a/package/lib/src/controls/menu_bar.dart b/package/lib/src/controls/menu_bar.dart new file mode 100644 index 000000000..7c8328b8a --- /dev/null +++ b/package/lib/src/controls/menu_bar.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import '../models/control.dart'; +import '../utils/menu.dart'; +import 'create_control.dart'; +import 'error.dart'; + +class MenuBarControl extends StatefulWidget { + final Control? parent; + final Control control; + final List children; + final bool parentDisabled; + + const MenuBarControl( + {super.key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled}); + + @override + State createState() => _MenuBarControlState(); +} + +class _MenuBarControlState extends State { + @override + Widget build(BuildContext context) { + debugPrint("MenuBar build: ${widget.control.id}"); + + var ctrls = widget.children.where((c) => c.isVisible).toList(); + if (ctrls.isEmpty) { + return const ErrorControl( + "MenuBar must have at least one child control", + ); + } + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + var clipBehavior = Clip.values.firstWhere( + (e) => + e.name.toLowerCase() == + widget.control.attrString("clipBehavior", "")!.toLowerCase(), + orElse: () => Clip.none); + + // var theme = Theme.of(context); + + var style = parseMenuStyle(Theme.of(context), widget.control, "style"); + + MenuBar? menuBar = MenuBar( + style: style, + clipBehavior: clipBehavior, + children: ctrls.map((c) => createControl(widget.control, c.id, disabled)).toList(), + ); + + return constrainedControl(context, menuBar, widget.parent, widget.control); + } +} diff --git a/package/lib/src/controls/menu_item_button.dart b/package/lib/src/controls/menu_item_button.dart new file mode 100644 index 000000000..9796c8994 --- /dev/null +++ b/package/lib/src/controls/menu_item_button.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; + +import '../flet_app_services.dart'; +import '../models/control.dart'; +import '../utils/buttons.dart'; +import 'create_control.dart'; + +class MenuItemButtonControl extends StatefulWidget { + final Control? parent; + final Control control; + final List children; + final bool parentDisabled; + + const MenuItemButtonControl( + {super.key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled}); + + @override + State createState() => _MenuItemButtonControlState(); +} + +class _MenuItemButtonControlState extends State { + late final FocusNode _focusNode; + String? _lastFocusValue; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _focusNode.addListener(_onFocusChange); + } + + @override + void dispose() { + _focusNode.removeListener(_onFocusChange); + _focusNode.dispose(); + super.dispose(); + } + + void _onFocusChange() { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: widget.control.id, + eventName: _focusNode.hasFocus ? "focus" : "blur", + eventData: ""); + } + + @override + Widget build(BuildContext context) { + debugPrint("MenuItemButton build: ${widget.control.id}"); + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + var content = + widget.children.where((c) => c.name == "content" && c.isVisible); + var leading = + widget.children.where((c) => c.name == "leading" && c.isVisible); + var trailing = + widget.children.where((c) => c.name == "trailing" && c.isVisible); + + var clipBehavior = Clip.values.firstWhere( + (e) => + e.name.toLowerCase() == + widget.control.attrString("clipBehavior", "")!.toLowerCase(), + orElse: () => Clip.none); + + var theme = Theme.of(context); + var style = parseButtonStyle(Theme.of(context), widget.control, "style", + defaultForegroundColor: theme.colorScheme.primary, + defaultBackgroundColor: Colors.transparent, + defaultOverlayColor: Colors.transparent, + defaultShadowColor: Colors.transparent, + defaultSurfaceTintColor: Colors.transparent, + defaultElevation: 0, + defaultPadding: const EdgeInsets.all(8), + defaultBorderSide: BorderSide.none, + defaultShape: theme.useMaterial3 + ? const StadiumBorder() + : RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))); + + bool onClick = widget.control.attrBool("onClick", false)!; + bool onHover = widget.control.attrBool("onHover", false)!; + + var server = FletAppServices.of(context).server; + + var menuItem = MenuItemButton( + focusNode: _focusNode, + clipBehavior: clipBehavior, + style: style, + closeOnActivate: widget.control.attrBool("closeOnClick", true)!, + requestFocusOnHover: widget.control.attrBool("focusOnHover", true)!, + onHover: onHover && !disabled + ? (bool value) { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "hover", + eventData: "$value"); + } + : null, + onPressed: onClick && !disabled + ? () { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "click", + eventData: ""); + } + : null, + leadingIcon: leading.isNotEmpty + ? leading + .map((c) => createControl(widget.control, c.id, disabled)) + .first + : null, + trailingIcon: trailing.isNotEmpty + ? trailing + .map((c) => createControl(widget.control, c.id, disabled)) + .first + : null, + child: content.isNotEmpty + ? content + .map((c) => createControl(widget.control, c.id, disabled)) + .first + : null, + ); + + var focusValue = widget.control.attrString("focus"); + if (focusValue != null && focusValue != _lastFocusValue) { + _lastFocusValue = focusValue; + _focusNode.requestFocus(); + } + + return constrainedControl(context, menuItem, widget.parent, widget.control); + } +} diff --git a/package/lib/src/controls/submenu_button.dart b/package/lib/src/controls/submenu_button.dart new file mode 100644 index 000000000..ff2048667 --- /dev/null +++ b/package/lib/src/controls/submenu_button.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; + +import '../flet_app_services.dart'; +import '../models/control.dart'; +import '../utils/buttons.dart'; +import '../utils/menu.dart'; +import '../utils/transforms.dart'; + +import 'create_control.dart'; + + +class SubMenuButtonControl extends StatefulWidget { + final Control? parent; + final Control control; + final List children; + final bool parentDisabled; + + const SubMenuButtonControl( + {super.key, + this.parent, + required this.control, + required this.children, + required this.parentDisabled}); + + @override + State createState() => _SubMenuButtonControlState(); +} + +class _SubMenuButtonControlState extends State { + late final FocusNode _focusNode; + String? _lastFocusValue; + + @override + void initState() { + super.initState(); + _focusNode = FocusNode(); + _focusNode.addListener(_onFocusChange); + } + + @override + void dispose() { + _focusNode.removeListener(_onFocusChange); + _focusNode.dispose(); + super.dispose(); + } + + void _onFocusChange() { + FletAppServices.of(context).server.sendPageEvent( + eventTarget: widget.control.id, + eventName: _focusNode.hasFocus ? "focus" : "blur", + eventData: ""); + } + + @override + Widget build(BuildContext context) { + debugPrint("SubMenuButton build: ${widget.control.id}"); + bool disabled = widget.control.isDisabled || widget.parentDisabled; + + var content = + widget.children.where((c) => c.name == "content" && c.isVisible); + var ctrls = + widget.children.where((c) => c.name == "controls" && c.isVisible); + var leading = + widget.children.where((c) => c.name == "leading" && c.isVisible); + var trailing = + widget.children.where((c) => c.name == "trailing" && c.isVisible); + + var clipBehavior = Clip.values.firstWhere( + (e) => + e.name.toLowerCase() == + widget.control.attrString("clipBehavior", "")!.toLowerCase(), + orElse: () => Clip.hardEdge); + + var offsetDetails = parseOffset(widget.control, "alignmentOffset"); + + var theme = Theme.of(context); + var style = parseButtonStyle(Theme.of(context), widget.control, "style", + defaultForegroundColor: theme.colorScheme.primary, + defaultBackgroundColor: Colors.transparent, + defaultOverlayColor: Colors.transparent, + defaultShadowColor: Colors.transparent, + defaultSurfaceTintColor: Colors.transparent, + defaultElevation: 0, + defaultPadding: const EdgeInsets.all(8), + defaultBorderSide: BorderSide.none, + defaultShape: theme.useMaterial3 + ? const StadiumBorder() + : RoundedRectangleBorder(borderRadius: BorderRadius.circular(4))); + + var menuStyle = + parseMenuStyle(Theme.of(context), widget.control, "menuStyle"); + + bool onOpen = widget.control.attrBool("onOpen", false)!; + bool onClose = widget.control.attrBool("onClose", false)!; + bool onHover = widget.control.attrBool("onHover", false)!; + + var server = FletAppServices.of(context).server; + + var subMenu = SubmenuButton( + focusNode: _focusNode, + clipBehavior: clipBehavior, + style: style, + menuStyle: menuStyle, + alignmentOffset: offsetDetails != null + ? Offset(offsetDetails.x, offsetDetails.y) + : null, + onClose: onClose && !disabled + ? () { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "close", + eventData: ""); + } + : null, + onHover: onHover && !disabled + ? (bool value) { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "hover", + eventData: "$value"); + } + : null, + onOpen: onOpen && !disabled + ? () { + server.sendPageEvent( + eventTarget: widget.control.id, + eventName: "open", + eventData: ""); + } + : null, + leadingIcon: leading.isNotEmpty + ? leading + .map((c) => createControl(widget.control, c.id, disabled)) + .toList() + .first + : null, + trailingIcon: trailing.isNotEmpty + ? trailing + .map((c) => createControl(widget.control, c.id, disabled)) + .toList() + .first + : null, + menuChildren: ctrls.isNotEmpty + ? ctrls.map((c) { + return createControl(widget.control, c.id, disabled); + }).toList() + : [], + child: content.isNotEmpty + ? content + .map((c) => createControl(widget.control, c.id, disabled)) + .toList() + .first + : null, + ); + + var focusValue = widget.control.attrString("focus"); + if (focusValue != null && focusValue != _lastFocusValue) { + _lastFocusValue = focusValue; + _focusNode.requestFocus(); + } + + return constrainedControl(context, subMenu, widget.parent, widget.control); + } +} diff --git a/package/lib/src/utils/menu.dart b/package/lib/src/utils/menu.dart new file mode 100644 index 000000000..8d9478bf3 --- /dev/null +++ b/package/lib/src/utils/menu.dart @@ -0,0 +1,95 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +import '../models/control.dart'; +import 'alignment.dart'; +import 'borders.dart'; +import 'colors.dart'; +import 'edge_insets.dart'; +import 'material_state.dart'; +import 'mouse.dart'; +import 'numbers.dart'; + +MenuStyle? parseMenuStyle(ThemeData theme, Control control, String propName, + {Color? defaultBackgroundColor, + Color? defaultShadowColor, + Color? defaultSurfaceTintColor, + double? defaultElevation, + Alignment? defaultAlignment, + MouseCursor? defaultMouseCursor, + EdgeInsets? defaultPadding, + BorderSide? defaultBorderSide, + OutlinedBorder? defaultShape}) { + var v = control.attrString(propName, null); + if (v == null) { + return null; + } + + final j1 = json.decode(v); + return menuStyleFromJSON( + theme, + j1, + defaultBackgroundColor, + defaultShadowColor, + defaultSurfaceTintColor, + defaultElevation, + defaultAlignment, + defaultMouseCursor, + defaultPadding, + defaultBorderSide, + defaultShape); +} + +MaterialStateProperty? parseMaterialStateColor( + ThemeData theme, Control control, String propName) { + var v = control.attrString(propName, null); + if (v == null) { + return null; + } + + final j1 = json.decode(v); + return getMaterialStateProperty( + j1, (jv) => HexColor.fromString(theme, jv as String), null); +} + +MenuStyle? menuStyleFromJSON( + ThemeData theme, + Map json, + Color? defaultBackgroundColor, + Color? defaultShadowColor, + Color? defaultSurfaceTintColor, + double? defaultElevation, + Alignment? defaultAlignment, + MouseCursor? defaultMouseCursor, + EdgeInsets? defaultPadding, + BorderSide? defaultBorderSide, + OutlinedBorder? defaultShape) { + return MenuStyle( + alignment: json["alignment"] != null + ? alignmentFromJson(json["alignment"]) + : defaultAlignment, + backgroundColor: getMaterialStateProperty( + json["bgcolor"], + (jv) => HexColor.fromString(theme, jv as String), + defaultBackgroundColor), + shadowColor: getMaterialStateProperty(json["shadow_color"], + (jv) => HexColor.fromString(theme, jv as String), defaultShadowColor), + surfaceTintColor: getMaterialStateProperty( + json["surface_tint_color"], + (jv) => HexColor.fromString(theme, jv as String), + defaultSurfaceTintColor), + elevation: getMaterialStateProperty( + json["elevation"], (jv) => parseDouble(jv), defaultElevation), + padding: getMaterialStateProperty( + json["padding"], (jv) => edgeInsetsFromJson(jv), defaultPadding), + side: getMaterialStateProperty( + json["side"], + (jv) => borderSideFromJSON(theme, jv, theme.colorScheme.outline), + defaultBorderSide), + shape: getMaterialStateProperty( + json["shape"], (jv) => outlinedBorderFromJSON(jv), defaultShape), + mouseCursor: getMaterialStateProperty(json["mouse_cursor"], + (jv) => parseMouseCursor(jv as String), defaultMouseCursor), + ); +} diff --git a/package/lib/src/utils/mouse.dart b/package/lib/src/utils/mouse.dart new file mode 100644 index 000000000..7b3c8762e --- /dev/null +++ b/package/lib/src/utils/mouse.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +MouseCursor parseMouseCursor(String? cursor) { + switch (cursor) { + case "alias": + return SystemMouseCursors.alias; + case "allScroll": + return SystemMouseCursors.allScroll; + case "basic": + return SystemMouseCursors.basic; + case "cell": + return SystemMouseCursors.cell; + case "click": + return SystemMouseCursors.click; + case "contextMenu": + return SystemMouseCursors.contextMenu; + case "copy": + return SystemMouseCursors.copy; + case "disappearing": + return SystemMouseCursors.disappearing; + case "forbidden": + return SystemMouseCursors.forbidden; + case "grab": + return SystemMouseCursors.grab; + case "grabbing": + return SystemMouseCursors.grabbing; + case "help": + return SystemMouseCursors.help; + case "move": + return SystemMouseCursors.move; + case "noDrop": + return SystemMouseCursors.noDrop; + case "none": + return SystemMouseCursors.none; + case "precise": + return SystemMouseCursors.precise; + case "progress": + return SystemMouseCursors.progress; + case "resizeColumn": + return SystemMouseCursors.resizeColumn; + case "resizeDown": + return SystemMouseCursors.resizeDown; + case "resizeDownLeft": + return SystemMouseCursors.resizeDownLeft; + case "resizeDownRight": + return SystemMouseCursors.resizeDownRight; + case "resizeLeft": + return SystemMouseCursors.resizeLeft; + case "resizeLeftRight": + return SystemMouseCursors.resizeLeftRight; + case "resizeRight": + return SystemMouseCursors.resizeRight; + case "resizeRow": + return SystemMouseCursors.resizeRow; + case "resizeUp": + return SystemMouseCursors.resizeUp; + case "resizeUpDown": + return SystemMouseCursors.resizeUpDown; + case "resizeUpLeft": + return SystemMouseCursors.resizeUpLeft; + case "resizeUpLeftDownRight": + return SystemMouseCursors.resizeUpLeftDownRight; + case "resizeUpRight": + return SystemMouseCursors.resizeUpRight; + case "resizeUpRightDownLeft": + return SystemMouseCursors.resizeUpRightDownLeft; + case "text": + return SystemMouseCursors.text; + case "verticalText": + return SystemMouseCursors.verticalText; + case "wait": + return SystemMouseCursors.wait; + case "zoomIn": + return SystemMouseCursors.zoomIn; + case "zoomOut": + return SystemMouseCursors.zoomOut; + default: + return MouseCursor.defer; + } + } \ No newline at end of file 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 3e11bedc1..7dab0c885 100644 --- a/sdk/python/packages/flet-core/src/flet_core/__init__.py +++ b/sdk/python/packages/flet-core/src/flet_core/__init__.py @@ -21,6 +21,9 @@ from flet_core.audio import Audio from flet_core.badge import Badge from flet_core.banner import Banner +from flet_core.menu_bar import MenuBar, MenuStyle +from flet_core.submenu_button import SubmenuButton +from flet_core.menu_item_button import MenuItemButton from flet_core.blur import Blur, BlurTileMode from flet_core.border import Border, BorderSide from flet_core.border_radius import BorderRadius @@ -93,10 +96,7 @@ from flet_core.filled_button import FilledButton from flet_core.filled_tonal_button import FilledTonalButton from flet_core.flet_app import FletApp -from flet_core.floating_action_button import ( - FloatingActionButton, - FloatingActionButtonLocation, -) +from flet_core.floating_action_button import FloatingActionButton from flet_core.form_field_control import InputBorder from flet_core.gesture_detector import ( DragEndEvent, @@ -214,6 +214,7 @@ BoxShape, ClipBehavior, CrossAxisAlignment, + FloatingActionButtonLocation, FontWeight, ImageFit, ImageRepeat, diff --git a/sdk/python/packages/flet-core/src/flet_core/floating_action_button.py b/sdk/python/packages/flet-core/src/flet_core/floating_action_button.py index 2c66dc632..40afbd3ea 100644 --- a/sdk/python/packages/flet-core/src/flet_core/floating_action_button.py +++ b/sdk/python/packages/flet-core/src/flet_core/floating_action_button.py @@ -1,4 +1,3 @@ -from enum import Enum from typing import Any, Optional, Union from flet_core.buttons import OutlinedBorder @@ -14,28 +13,6 @@ ) -class FloatingActionButtonLocation(Enum): - CENTER_DOCKED = "centerDocked" - CENTER_FLOAT = "centerFloat" - CENTER_TOP = "centerTop" - END_CONTAINED = "endContained" - END_DOCKED = "endDocked" - END_FLOAT = "endFloat" - END_TOP = "endTop" - MINI_CENTER_DOCKED = "miniCenterDocked" - MINI_CENTER_FLOAT = "miniCenterFloat" - MINI_CENTER_TOP = "miniCenterTop" - MINI_END_DOCKED = "miniEndDocked" - MINI_END_FLOAT = "miniEndFloat" - MINI_END_TOP = "miniEndTop" - MINI_START_DOCKED = "miniStartDocked" - MINI_START_FLOAT = "miniStartFloat" - MINI_START_TOP = "miniStartTop" - START_DOCKED = "startDocked" - START_FLOAT = "startFloat" - START_TOP = "startTop" - - class FloatingActionButton(ConstrainedControl): """ A floating action button is a circular icon button that hovers over content to promote a primary action in the application. Floating action button is usually set to `page.floating_action_button`, but can also be added as a regular control at any place on a page. diff --git a/sdk/python/packages/flet-core/src/flet_core/menu_bar.py b/sdk/python/packages/flet-core/src/flet_core/menu_bar.py new file mode 100644 index 000000000..c09dd5d05 --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/menu_bar.py @@ -0,0 +1,134 @@ +import dataclasses +from dataclasses import field +from typing import Any, Dict, List, Optional, Union + +from flet_core.alignment import Alignment +from flet_core.border import BorderSide +from flet_core.buttons import OutlinedBorder +from flet_core.control import Control, OptionalNumber +from flet_core.gesture_detector import MouseCursor +from flet_core.ref import Ref +from flet_core.types import ClipBehavior, MaterialState, PaddingValue, ResponsiveNumber + + +@dataclasses.dataclass +class MenuStyle: + alignment: Optional[Alignment] = field(default=None) + bgcolor: Union[None, str, Dict[Union[str, MaterialState], str]] = field( + default=None + ) + shadow_color: Union[None, str, Dict[Union[str, MaterialState], str]] = field( + default=None + ) + surface_tint_color: Union[None, str, Dict[Union[str, MaterialState], str]] = field( + default=None + ) + elevation: Union[ + None, float, int, Dict[Union[str, MaterialState], Union[float, int]] + ] = field(default=None) + padding: Union[PaddingValue, Dict[Union[str, MaterialState], PaddingValue]] = field( + default=None + ) + side: Union[None, BorderSide, Dict[Union[str, MaterialState], BorderSide]] = field( + default=None + ) + shape: Union[ + None, OutlinedBorder, Dict[Union[str, MaterialState], OutlinedBorder] + ] = field(default=None) + mouse_cursor: Union[ + None, MouseCursor, Dict[Union[str, MaterialState], MouseCursor] + ] = field(default=None) + + +class MenuBar(Control): + """ + A menu bar that manages cascading child menus. + + It could be placed anywhere but typically resides above the main body of the application + and defines a menu system for invoking callbacks in response to user selection of a menu item. + + ----- + + Online docs: https://flet.dev/docs/controls/menubar + """ + + def __init__( + self, + controls: Optional[List[Control]] = None, + ref: Optional[Ref] = None, + expand: Union[None, bool, int] = None, + col: Optional[ResponsiveNumber] = None, + opacity: OptionalNumber = None, + visible: Optional[bool] = None, + disabled: Optional[bool] = None, + data: Any = None, + # + # Specific + # + clip_behavior: Optional[ClipBehavior] = None, + style: Optional[MenuStyle] = None, + ): + Control.__init__( + self, + ref=ref, + expand=expand, + col=col, + opacity=opacity, + visible=visible, + disabled=disabled, + data=data, + ) + + self.__controls: List[Control] = [] + self.controls = controls + self.clip_behavior = clip_behavior + self.style = style + + def _get_control_name(self): + return "menubar" + + def _before_build_command(self): + super()._before_build_command() + if self.__style is not None: + self.__style.side = self._wrap_attr_dict(self.__style.side) + self.__style.shape = self._wrap_attr_dict(self.__style.shape) + self.__style.mouse_cursor = self._wrap_attr_dict(self.__style.mouse_cursor) + if self.__style.mouse_cursor: + for k, v in self.__style.mouse_cursor.items(): + self.__style.mouse_cursor[k] = ( + v.value if isinstance(v, MouseCursor) else str(v) + ) + self._set_attr_json("style", self.__style) + + def _get_children(self): + return self.__controls + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value): + self.__controls = value if value is not None else [] + + # clip_behavior + @property + def clip_behavior(self) -> Optional[ClipBehavior]: + return self.__clip_behavior + + @clip_behavior.setter + def clip_behavior(self, value: Optional[ClipBehavior]): + self.__clip_behavior = value + self._set_attr( + "clipBehavior", value.value if isinstance(value, ClipBehavior) else value + ) + + # style + @property + def style(self) -> Optional[MenuStyle]: + return self.__style + + @style.setter + def style(self, value: Optional[MenuStyle]): + self.__style = value diff --git a/sdk/python/packages/flet-core/src/flet_core/menu_item_button.py b/sdk/python/packages/flet-core/src/flet_core/menu_item_button.py new file mode 100644 index 000000000..33c72af3b --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/menu_item_button.py @@ -0,0 +1,245 @@ +import time +from typing import Any, Optional, Union + +from flet_core.buttons import ButtonStyle +from flet_core.constrained_control import ConstrainedControl +from flet_core.control import Control, OptionalNumber +from flet_core.ref import Ref +from flet_core.types import ( + AnimationValue, + ClipBehavior, + OffsetValue, + ResponsiveNumber, + RotateValue, + ScaleValue, +) + + +class MenuItemButton(ConstrainedControl): + """ + A button for use in a MenuBar or on its own, that can be activated by click or keyboard navigation. + + ----- + + Online docs: https://flet.dev/docs/controls/menuitembutton + """ + + def __init__( + self, + content: Optional[Control] = None, + 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 + # + close_on_click: Optional[bool] = None, + focus_on_hover: Optional[bool] = None, + leading: Optional[Control] = None, + trailing: Optional[Control] = None, + clip_behavior: Optional[ClipBehavior] = None, + style: Optional[ButtonStyle] = None, + on_click=None, + on_hover=None, + on_focus=None, + on_blur=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.content = content + self.leading = leading + self.trailing = trailing + self.clip_behavior = clip_behavior + self.style = style + self.close_on_click = close_on_click + self.focus_on_hover = focus_on_hover + self.on_click = on_click + self.on_hover = on_hover + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "menuitembutton" + + def _before_build_command(self): + super()._before_build_command() + if self.__style is not None: + self.__style.side = self._wrap_attr_dict(self.__style.side) + self.__style.shape = self._wrap_attr_dict(self.__style.shape) + self._set_attr_json("style", self.__style) + + def _get_children(self): + children = [] + if self.__leading: + self.__leading._set_attr_internal("n", "leading") + children.append(self.__leading) + if self.__trailing: + self.__trailing._set_attr_internal("n", "trailing") + children.append(self.__trailing) + if self.__content: + self.__content._set_attr_internal("n", "content") + children.append(self.__content) + return children + + def focus(self): + self._set_attr_json("focus", str(time.time())) + self.update() + + async def focus_async(self): + self._set_attr_json("focus", str(time.time())) + await self.update_async() + + # focus_on_hover + @property + def focus_on_hover(self) -> Optional[bool]: + return self._get_attr("focusOnHover", data_type="bool", def_value=True) + + @focus_on_hover.setter + def focus_on_hover(self, value: Optional[bool]): + self._set_attr("focusOnHover", value) + + # close_on_click + @property + def close_on_click(self) -> Optional[bool]: + return self._get_attr("closeOnClick", data_type="bool", def_value=True) + + @close_on_click.setter + def close_on_click(self, value: Optional[bool]): + self._set_attr("closeOnClick", value) + + # leading + @property + def leading(self) -> Optional[Control]: + return self.__leading + + @leading.setter + def leading(self, value: Optional[Control]): + self.__leading = value + + # trailing + @property + def trailing(self) -> Optional[Control]: + return self.__trailing + + @trailing.setter + def trailing(self, value: Optional[Control]): + self.__trailing = value + + # content + @property + def content(self) -> Optional[Control]: + return self.__content + + @content.setter + def content(self, value: Optional[Control]): + self.__content = value + + # style + @property + def style(self) -> Optional[ButtonStyle]: + return self.__style + + @style.setter + def style(self, value: Optional[ButtonStyle]): + self.__style = value + + # clip_behavior + @property + def clip_behavior(self) -> Optional[ClipBehavior]: + return self.__clip_behavior + + @clip_behavior.setter + def clip_behavior(self, value: Optional[ClipBehavior]): + self.__clip_behavior = value + self._set_attr( + "clipBehavior", value.value if isinstance(value, ClipBehavior) else value + ) + + # on_click + @property + def on_click(self): + return self._get_event_handler("click") + + @on_click.setter + def on_click(self, handler): + self._add_event_handler("click", handler) + self._set_attr("onClick", True if handler is not None else None) + + # on_hover + @property + def on_hover(self): + return self._get_event_handler("hover") + + @on_hover.setter + def on_hover(self, handler): + self._add_event_handler("hover", handler) + self._set_attr("onHover", True if handler is not None else None) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) diff --git a/sdk/python/packages/flet-core/src/flet_core/page.py b/sdk/python/packages/flet-core/src/flet_core/page.py index 9813bb995..26b35184e 100644 --- a/sdk/python/packages/flet-core/src/flet_core/page.py +++ b/sdk/python/packages/flet-core/src/flet_core/page.py @@ -23,10 +23,7 @@ from flet_core.control_event import ControlEvent from flet_core.event import Event from flet_core.event_handler import EventHandler -from flet_core.floating_action_button import ( - FloatingActionButton, - FloatingActionButtonLocation, -) +from flet_core.floating_action_button import FloatingActionButton from flet_core.locks import AsyncNopeLock, NopeLock from flet_core.navigation_bar import NavigationBar from flet_core.cupertino_navigation_bar import CupertinoNavigationBar @@ -38,6 +35,7 @@ from flet_core.theme import Theme from flet_core.types import ( CrossAxisAlignment, + FloatingActionButtonLocation, MainAxisAlignment, PaddingValue, PageDesignLanguage, diff --git a/sdk/python/packages/flet-core/src/flet_core/submenu_button.py b/sdk/python/packages/flet-core/src/flet_core/submenu_button.py new file mode 100644 index 000000000..2a9cdfa6b --- /dev/null +++ b/sdk/python/packages/flet-core/src/flet_core/submenu_button.py @@ -0,0 +1,277 @@ +import time +from typing import Any, List, Optional, Union + +from flet_core.buttons import ButtonStyle +from flet_core.constrained_control import ConstrainedControl +from flet_core.control import Control, OptionalNumber +from flet_core.menu_bar import MenuStyle +from flet_core.ref import Ref +from flet_core.types import ( + AnimationValue, + ClipBehavior, + OffsetValue, + ResponsiveNumber, + RotateValue, + ScaleValue, +) + + +class SubmenuButton(ConstrainedControl): + """ + A menu button that displays a cascading menu. It can be used as part of a MenuBar, or as a standalone control. + + ----- + + Online docs: https://flet.dev/docs/controls/submenubutton + """ + + def __init__( + self, + content: Optional[Control] = None, + controls: Optional[List[Control]] = None, + 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 + # + leading: Optional[Control] = None, + trailing: Optional[Control] = None, + clip_behavior: Optional[ClipBehavior] = None, + menu_style: Optional[MenuStyle] = None, + style: Optional[ButtonStyle] = None, + alignment_offset: OffsetValue = None, + on_open=None, + on_close=None, + on_hover=None, + on_focus=None, + on_blur=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.content = content + self.controls = controls + self.leading = leading + self.trailing = trailing + self.clip_behavior = clip_behavior + self.style = style + self.menu_style = menu_style + self.alignment_offset = alignment_offset + self.on_open = on_open + self.on_close = on_close + self.on_hover = on_hover + self.on_focus = on_focus + self.on_blur = on_blur + + def _get_control_name(self): + return "submenubutton" + + def _before_build_command(self): + super()._before_build_command() + if self.__style is not None: + self.__style.side = self._wrap_attr_dict(self.__style.side) + self.__style.shape = self._wrap_attr_dict(self.__style.shape) + if self.__menu_style is not None: + self.__menu_style.side = self._wrap_attr_dict(self.__menu_style.side) + self.__menu_style.shape = self._wrap_attr_dict(self.__menu_style.shape) + self._set_attr_json("style", self.__style) + self._set_attr_json("menuStyle", self.__menu_style) + + def _get_children(self): + children = [] + if self.__controls: + for c in self.__controls: + c._set_attr_internal("n", "controls") + children.append(c) + if self.__leading: + self.__leading._set_attr_internal("n", "leading") + children.append(self.__leading) + if self.__trailing: + self.__trailing._set_attr_internal("n", "trailing") + children.append(self.__trailing) + if self.__content: + self.__content._set_attr_internal("n", "content") + children.append(self.__content) + return children + + def focus(self): + self._set_attr_json("focus", str(time.time())) + self.update() + + async def focus_async(self): + self._set_attr_json("focus", str(time.time())) + await self.update_async() + + # controls + @property + def controls(self): + return self.__controls + + @controls.setter + def controls(self, value: Optional[List[Control]]): + self.__controls = value if value is not None else [] + + # leading + @property + def leading(self) -> Optional[Control]: + return self.__leading + + @leading.setter + def leading(self, value: Optional[Control]): + self.__leading = value + + # trailing + @property + def trailing(self) -> Optional[Control]: + return self.__trailing + + @trailing.setter + def trailing(self, value: Optional[Control]): + self.__trailing = value + + # content + @property + def content(self) -> Optional[Control]: + return self.__content + + @content.setter + def content(self, value: Optional[Control]): + self.__content = value + + # style + @property + def style(self) -> Optional[ButtonStyle]: + return self.__style + + @style.setter + def style(self, value: Optional[ButtonStyle]): + self.__style = value + + # menu_style + @property + def menu_style(self) -> Optional[MenuStyle]: + return self.__menu_style + + @menu_style.setter + def menu_style(self, value: Optional[MenuStyle]): + self.__menu_style = value + + # clip_behavior + @property + def clip_behavior(self) -> Optional[ClipBehavior]: + return self.__clip_behavior + + @clip_behavior.setter + def clip_behavior(self, value: Optional[ClipBehavior]): + self.__clip_behavior = value + self._set_attr( + "clipBehavior", value.value if isinstance(value, ClipBehavior) else value + ) + + # alignment_offset + @property + def alignment_offset(self) -> OffsetValue: + return self.__alignment_offset + + @alignment_offset.setter + def alignment_offset(self, value: OffsetValue): + self.__alignment_offset = value + + # on_open + @property + def on_open(self): + return self._get_event_handler("open") + + @on_open.setter + def on_open(self, handler): + self._add_event_handler("open", handler) + self._set_attr("onOpen", True if handler is not None else None) + + # on_close + @property + def on_close(self): + return self._get_event_handler("close") + + @on_close.setter + def on_close(self, handler): + self._add_event_handler("close", handler) + self._set_attr("onClose", True if handler is not None else None) + + # on_hover + @property + def on_hover(self): + return self._get_event_handler("hover") + + @on_hover.setter + def on_hover(self, handler): + self._add_event_handler("hover", handler) + self._set_attr("onHover", True if handler is not None else None) + + # on_focus + @property + def on_focus(self): + return self._get_event_handler("focus") + + @on_focus.setter + def on_focus(self, handler): + self._add_event_handler("focus", handler) + + # on_blur + @property + def on_blur(self): + return self._get_event_handler("blur") + + @on_blur.setter + def on_blur(self, handler): + self._add_event_handler("blur", handler) diff --git a/sdk/python/packages/flet-core/src/flet_core/types.py b/sdk/python/packages/flet-core/src/flet_core/types.py index 6364a0761..c07f9b4e9 100644 --- a/sdk/python/packages/flet-core/src/flet_core/types.py +++ b/sdk/python/packages/flet-core/src/flet_core/types.py @@ -304,3 +304,25 @@ class ThemeMode(Enum): SYSTEM = "system" LIGHT = "light" DARK = "dark" + + +class FloatingActionButtonLocation(Enum): + CENTER_DOCKED = "centerDocked" + CENTER_FLOAT = "centerFloat" + CENTER_TOP = "centerTop" + END_CONTAINED = "endContained" + END_DOCKED = "endDocked" + END_FLOAT = "endFloat" + END_TOP = "endTop" + MINI_CENTER_DOCKED = "miniCenterDocked" + MINI_CENTER_FLOAT = "miniCenterFloat" + MINI_CENTER_TOP = "miniCenterTop" + MINI_END_DOCKED = "miniEndDocked" + MINI_END_FLOAT = "miniEndFloat" + MINI_END_TOP = "miniEndTop" + MINI_START_DOCKED = "miniStartDocked" + MINI_START_FLOAT = "miniStartFloat" + MINI_START_TOP = "miniStartTop" + START_DOCKED = "startDocked" + START_FLOAT = "startFloat" + START_TOP = "startTop" diff --git a/sdk/python/packages/flet-core/src/flet_core/view.py b/sdk/python/packages/flet-core/src/flet_core/view.py index 4ec987e0a..46ff93972 100644 --- a/sdk/python/packages/flet-core/src/flet_core/view.py +++ b/sdk/python/packages/flet-core/src/flet_core/view.py @@ -4,10 +4,7 @@ from flet_core.app_bar import AppBar from flet_core.bottom_app_bar import BottomAppBar from flet_core.control import OptionalNumber -from flet_core.floating_action_button import ( - FloatingActionButton, - FloatingActionButtonLocation, -) +from flet_core.floating_action_button import FloatingActionButton from flet_core.navigation_bar import NavigationBar from flet_core.cupertino_navigation_bar import CupertinoNavigationBar from flet_core.navigation_drawer import NavigationDrawer @@ -15,6 +12,7 @@ from flet_core.types import ( CrossAxisAlignment, CrossAxisAlignmentString, + FloatingActionButtonLocation, MainAxisAlignment, MainAxisAlignmentString, PaddingValue,