diff --git a/modules/ensemble/lib/framework/ensemble_widget.dart b/modules/ensemble/lib/framework/ensemble_widget.dart index 0167d5128..0e1b13ad7 100644 --- a/modules/ensemble/lib/framework/ensemble_widget.dart +++ b/modules/ensemble/lib/framework/ensemble_widget.dart @@ -5,6 +5,7 @@ import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/scope.dart'; import 'package:ensemble/framework/studio/studio_debugger.dart'; import 'package:ensemble/framework/view/data_scope_widget.dart'; +import 'package:ensemble/util/utils.dart'; import 'package:ensemble/widget/helpers/controllers.dart'; import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; import 'package:flutter/cupertino.dart'; @@ -67,6 +68,18 @@ abstract class EnsembleWidgetState extends State { child: rtn); } + + // add tooltip handling if tooltip message is specified + // add tooltip handling if tooltip message is specified + if (widgetController.toolTip != null) { + rtn = Utils.getTooltipWidget( + context, + rtn, + widgetController.toolTip, + widgetController + ); + } + // in Web, capture the pointer if overlay on htmlelementview like Maps if (widgetController.captureWebPointer == true) { rtn = PointerInterceptor(child: rtn); diff --git a/modules/ensemble/lib/framework/widget/widget.dart b/modules/ensemble/lib/framework/widget/widget.dart index c724303a3..1350014bf 100644 --- a/modules/ensemble/lib/framework/widget/widget.dart +++ b/modules/ensemble/lib/framework/widget/widget.dart @@ -75,6 +75,16 @@ abstract class EWidgetState child: rtn); } + // add tooltip handling if tooltip message is specified + if (widgetController.toolTip != null) { + rtn = Utils.getTooltipWidget( + context, + rtn, + widgetController.toolTip, + widgetController + ); + } + // in Web, capture the pointer if overlay on htmlelementview like Maps if (widgetController.captureWebPointer == true) { rtn = PointerInterceptor(child: rtn); diff --git a/modules/ensemble/lib/util/utils.dart b/modules/ensemble/lib/util/utils.dart index 49fce5ece..bb9ffa2a8 100644 --- a/modules/ensemble/lib/util/utils.dart +++ b/modules/ensemble/lib/util/utils.dart @@ -3,9 +3,12 @@ import 'dart:math'; import 'dart:ui'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/ensemble_app.dart'; +import 'package:ensemble/framework/action.dart'; import 'package:ensemble/framework/ensemble_config_service.dart'; import 'package:ensemble/framework/stub/location_manager.dart'; import 'package:ensemble/framework/theme/theme_manager.dart'; +import 'package:ensemble/screen_controller.dart'; +import 'package:ensemble/widget/helpers/tooltip_composite.dart'; import 'package:ensemble_ts_interpreter/invokables/UserLocale.dart'; import 'package:path/path.dart' as p; @@ -489,6 +492,81 @@ class Utils { return null; } + // Creates tooltip composite from inputs + static TooltipStyleComposite? getTooltipStyleComposite( + ChangeNotifier controller, dynamic inputs) { + if (inputs is Map) { + return TooltipStyleComposite(controller, inputs: inputs); + } + return null; + } + + /// Creates tooltip widget with configured styles and behavior + static Widget getTooltipWidget( + BuildContext context, + Widget child, + Map? tooltipData, + ChangeNotifier controller +) { + if (tooltipData == null) return child; + + final tooltip = TooltipData.from(tooltipData, controller); + if (tooltip == null) return child; + + final tooltipKey = GlobalKey(); + // Start with the original child + Widget tooltipChild = child; + + if (kIsWeb && tooltip.styles?.triggerMode == null) { + tooltipChild = MouseRegion( + onEnter: (_) { + final dynamic tooltip = tooltipKey.currentState; + tooltip?.ensureTooltipVisible(); + }, + onExit: (_) { + final dynamic tooltip = tooltipKey.currentState; + tooltip?.deactivate(); + }, + child: tooltipChild, + ); + } + + return Tooltip( + key: tooltipKey, + message: tooltip.message, + textStyle: tooltip.styles?.textStyle, + padding: tooltip.styles?.padding, + margin: tooltip.styles?.margin, + verticalOffset: tooltip.styles?.verticalOffset, + preferBelow: tooltip.styles?.preferBelow, + waitDuration: + tooltip.styles?.waitDuration ?? const Duration(milliseconds: 0), + showDuration: + tooltip.styles?.showDuration ?? const Duration(milliseconds: 1500), + triggerMode: tooltip.styles?.triggerMode ?? TooltipTriggerMode.tap, + enableFeedback: true, + decoration: BoxDecoration( + color: tooltip.styles?.backgroundColor ?? Colors.grey[700], + borderRadius: tooltip.styles?.borderRadius, + border: (tooltip.styles?.borderColor != null || + tooltip.styles?.borderWidth != null) + ? Border.all( + color: tooltip.styles?.borderColor ?? + ThemeManager().getBorderColor(context), + width: (tooltip.styles?.borderWidth ?? + ThemeManager().getBorderThickness(context)) + .toDouble(), + ) + : null, + ), + onTriggered: tooltip.onTriggered != null + ? () => + ScreenController().executeAction(context, tooltip.onTriggered!) + : null, + child: tooltipChild, + ); + } + static BoxShadowComposite? getBoxShadowComposite( ChangeNotifier widgetController, dynamic inputs) { if (inputs is Map) { diff --git a/modules/ensemble/lib/widget/helpers/controllers.dart b/modules/ensemble/lib/widget/helpers/controllers.dart index cc4ae0da5..50e47c63f 100644 --- a/modules/ensemble/lib/widget/helpers/controllers.dart +++ b/modules/ensemble/lib/widget/helpers/controllers.dart @@ -8,9 +8,11 @@ import 'package:ensemble/model/transform_matrix.dart'; import 'package:ensemble/page_model.dart'; import 'package:ensemble/util/utils.dart'; import 'package:ensemble/widget/helpers/box_animation_composite.dart'; +import 'package:ensemble/widget/helpers/tooltip_composite.dart'; import 'package:ensemble_ts_interpreter/errors.dart'; import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import '../../model/capabilities.dart'; @@ -223,6 +225,9 @@ abstract class WidgetController extends Controller with HasStyles { // https://pub.dev/packages/pointer_interceptor bool? captureWebPointer; + // properties for tooltip + Map? toolTip; + // legacy used to show as the form label if used inside Form @Deprecated("don't use anymore") String? label; @@ -279,7 +284,8 @@ abstract class WidgetController extends Controller with HasStyles { 'textDirection': (value) => textDirection = Utils.getTextDirection(value), 'label': (value) => label = Utils.optionalString(value), 'classList': (value) => classList = value, - 'className': (value) => className = value + 'className': (value) => className = value, + 'tooltip': (value) => toolTip = Utils.getMap(value), }; } @@ -458,6 +464,10 @@ abstract class EnsembleWidgetController extends EnsembleController // https://pub.dev/packages/pointer_interceptor bool? captureWebPointer; + // properties for tooltip + Map? toolTip; + + @override Map getters() { return { @@ -499,8 +509,9 @@ abstract class EnsembleWidgetController extends EnsembleController 'captureWebPointer': (value) => captureWebPointer = Utils.optionalBool(value), 'classList': (value) => classList = value, - 'className': (value) => className = value - }; + 'className': (value) => className = value, + 'tooltip': (value) => toolTip = Utils.getMap(value), + }; } bool hasPositions() { diff --git a/modules/ensemble/lib/widget/helpers/tooltip_composite.dart b/modules/ensemble/lib/widget/helpers/tooltip_composite.dart new file mode 100644 index 000000000..9d9802322 --- /dev/null +++ b/modules/ensemble/lib/widget/helpers/tooltip_composite.dart @@ -0,0 +1,98 @@ +/// This class contains helper controllers for our widgets. +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/extensions.dart'; +import 'package:ensemble/util/utils.dart'; +import 'package:ensemble/widget/helpers/controllers.dart'; +import 'package:flutter/material.dart'; + +class TooltipData { + final String message; + final TooltipStyleComposite? styles; + final EnsembleAction? onTriggered; + + TooltipData({ + required this.message, + this.styles, + this.onTriggered, + }); + + static TooltipData? from(Map? data, ChangeNotifier controller) { + if (data == null) return null; + + return TooltipData( + message: Utils.getString(data['message'], fallback: ''), + styles: data['styles'] != null ? + TooltipStyleComposite(controller, inputs: data['styles']) : null, + onTriggered: data['onTriggered'] != null ? + EnsembleAction.from(data['onTriggered']) : null, + ); + } +} + +// Composite class to handle tooltip styling and behavior +class TooltipStyleComposite extends WidgetCompositeProperty { + TooltipStyleComposite(super.widgetController, {required Map inputs}) { + textStyle = Utils.getTextStyle(inputs['textStyle']); + verticalOffset = Utils.optionalDouble(inputs['verticalOffset']); + preferBelow = Utils.optionalBool(inputs['preferBelow']); + waitDuration = Utils.getDuration(inputs['waitDuration']); + showDuration = Utils.getDuration(inputs['showDuration']); + triggerMode = TooltipTriggerMode.values.from(inputs['triggerMode']); + backgroundColor = Utils.getColor(inputs['backgroundColor']); + borderRadius = Utils.getBorderRadius(inputs['borderRadius'])?.getValue(); + padding = Utils.optionalInsets(inputs['padding']); + margin = Utils.optionalInsets(inputs['margin']); + borderColor = Utils.getColor(inputs['borderColor']); + borderWidth = Utils.optionalInt(inputs['borderWidth']); + } + + TextStyle? textStyle; + double? verticalOffset; + bool? preferBelow; + Duration? waitDuration; + Duration? showDuration; + TooltipTriggerMode? triggerMode; + Color? backgroundColor; + BorderRadius? borderRadius; + EdgeInsets? padding; + EdgeInsets? margin; + Color? borderColor; + int? borderWidth; + + @override + Map setters() { + return { + 'textStyle': (value) => textStyle = Utils.getTextStyle(value), + 'verticalOffset': (value) => verticalOffset = Utils.optionalDouble(value), + 'preferBelow': (value) => preferBelow = Utils.optionalBool(value), + 'waitDuration': (value) => waitDuration = Utils.getDuration(value), + 'showDuration': (value) => showDuration = Utils.getDuration(value), + 'triggerMode': (value) => triggerMode = TooltipTriggerMode.values.from(value), + 'backgroundColor': (value) => backgroundColor = Utils.getColor(value), + 'borderRadius': (value) => borderRadius = Utils.getBorderRadius(value)?.getValue(), + 'padding': (value) => padding = Utils.optionalInsets(value), + 'margin': (value) => margin = Utils.optionalInsets(value), + 'borderColor': (value) => borderColor = Utils.getColor(value), + 'borderWidth': (value) => borderWidth = Utils.optionalInt(value), + }; + } + + @override + Map getters() => { + 'textStyle': () => textStyle, + 'verticalOffset': () => verticalOffset, + 'preferBelow': () => preferBelow, + 'waitDuration': () => waitDuration, + 'showDuration': () => showDuration, + 'triggerMode': () => triggerMode, + 'backgroundColor': () => backgroundColor, + 'borderRadius': () => borderRadius, + 'padding': () => padding, + 'margin': () => margin, + 'borderColor': () => borderColor, + 'borderWidth': () => borderWidth, + }; + + @override + Map methods() => {}; +} \ No newline at end of file diff --git a/modules/ensemble/lib/widget/tooltip.dart b/modules/ensemble/lib/widget/tooltip.dart deleted file mode 100644 index e93240a55..000000000 --- a/modules/ensemble/lib/widget/tooltip.dart +++ /dev/null @@ -1,163 +0,0 @@ -import 'package:ensemble/framework/event.dart'; -import 'package:ensemble/framework/extensions.dart'; -import 'package:ensemble/framework/theme/theme_manager.dart'; -import 'package:ensemble/widget/helpers/controllers.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:ensemble/framework/widget/widget.dart'; -import 'package:ensemble/framework/action.dart'; -import 'package:ensemble/screen_controller.dart'; -import 'package:ensemble/util/utils.dart'; -import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; -import 'package:ensemble/framework/widget/has_children.dart'; -import 'package:ensemble/framework/widget/view_util.dart'; - -class ToolTip extends StatefulWidget - with Invokable, HasController { - static const type = 'ToolTip'; - - ToolTip({Key? key}) : super(key: key); - - final ToolTipController _controller = ToolTipController(); - - @override - ToolTipController get controller => _controller; - - @override - State createState() => ToolTipState(); - - @override - Map getters() { - return { - 'isVisible': () => _controller.isVisible, - }; - } - - @override - Map setters() { - return { - 'message': (value) => _controller.message = Utils.optionalString(value), - 'widget': (value) => _controller.widget = value, - 'textStyle': (value) => _controller.textStyle = Utils.getTextStyle(value), - 'verticalOffset': (value) => - _controller.verticalOffset = Utils.optionalDouble(value), - 'preferBelow': (value) => - _controller.preferBelow = Utils.optionalBool(value), - 'waitDuration': (value) => - _controller.waitDuration = Utils.getDuration(value), - 'showDuration': (value) => - _controller.showDuration = Utils.getDuration(value), - 'triggerMode': (value) => - _controller.triggerMode = TooltipTriggerMode.values.from(value), - 'onTriggered': (definition) => _controller.onTriggered = - EnsembleAction.from(definition, initiator: this), - }; - } - - @override - Map methods() { - return { - 'show': () => _controller.show(), - }; - } -} - -class ToolTipController extends BoxController { - String? message; - dynamic widget; - TextStyle? textStyle; - double? verticalOffset; - bool? preferBelow; - Duration? waitDuration; - Duration? showDuration; - TooltipTriggerMode? triggerMode; - bool isVisible = false; - EnsembleAction? onTriggered; - - void show() { - isVisible = true; - notifyListeners(); - } - - void hide() { - isVisible = false; - notifyListeners(); - } -} - -class ToolTipState extends EWidgetState with HasChildren { - final GlobalKey _tooltipKey = GlobalKey(); - - @override - void initState() { - super.initState(); - widget.controller.addListener(() => setState(() {})); - } - - void _showTooltip() { - final dynamic tooltip = _tooltipKey.currentState; - tooltip?.ensureTooltipVisible(); - widget.controller.show(); - } - - void _hideTooltip() { - final dynamic tooltip = _tooltipKey.currentState; - tooltip?.deactivate(); - widget.controller.hide(); - } - - @override - Widget buildWidget(BuildContext context) { - Widget child = widget.controller.widget != null - ? buildChild(ViewUtil.buildModel(widget.controller.widget, null)) - : const SizedBox.shrink(); - - if (kIsWeb && widget.controller.triggerMode == null) { - child = MouseRegion( - onEnter: (_) => _showTooltip(), - onExit: (_) => _hideTooltip(), - child: child, - ); - } - - return Tooltip( - key: _tooltipKey, - message: widget.controller.message ?? '', - textStyle: widget.controller.textStyle, - height: widget.controller.height?.toDouble(), - padding: widget.controller.padding, - margin: widget.controller.margin, - verticalOffset: widget.controller.verticalOffset, - preferBelow: widget.controller.preferBelow, - waitDuration: - widget.controller.waitDuration ?? const Duration(milliseconds: 0), - showDuration: - widget.controller.showDuration ?? const Duration(milliseconds: 1500), - triggerMode: widget.controller.triggerMode ?? - (kIsWeb ? null : TooltipTriggerMode.tap), - enableFeedback: true, - decoration: BoxDecoration( - color: widget._controller.backgroundColor ?? Colors.grey[700], - borderRadius: widget._controller.borderRadius?.getValue(), - border: widget._controller.borderColor != null || - widget._controller.borderWidth != null - ? Border.all( - color: widget._controller.borderColor ?? - ThemeManager().getBorderColor(context), - width: widget._controller.borderWidth?.toDouble() ?? - ThemeManager().getBorderThickness(context), - ) - : null), - onTriggered: () { - if (widget.controller.onTriggered != null) { - ScreenController().executeAction( - context, - widget.controller.onTriggered!, - event: EnsembleEvent(widget), - ); - } - }, - child: child, - ); - } -} diff --git a/modules/ensemble/lib/widget/widget_registry.dart b/modules/ensemble/lib/widget/widget_registry.dart index bb364cf2a..62520967b 100644 --- a/modules/ensemble/lib/widget/widget_registry.dart +++ b/modules/ensemble/lib/widget/widget_registry.dart @@ -59,7 +59,6 @@ import 'package:ensemble/widget/stub_widgets.dart'; import 'package:ensemble/widget/switch.dart'; import 'package:ensemble/widget/text.dart'; import 'package:ensemble/widget/toggle_button.dart'; -import 'package:ensemble/widget/tooltip.dart'; import 'package:ensemble/widget/video.dart'; import 'package:ensemble/widget/slidable.dart'; import 'package:ensemble/widget/accordion.dart'; @@ -139,7 +138,6 @@ class WidgetRegistry { PopupMenu.type: () => PopupMenu(), EnsembleCalendar.type: () => EnsembleCalendar(), Countdown.type: () => Countdown(), - ToolTip.type: () => ToolTip(), EnsembleAccordion.type: () => EnsembleAccordion(), // form fields