import 'package:flutter/material.dart'; import '../../../../flutter_quill.dart'; import '../../../../translations.dart'; import '../../../utils/widgets.dart'; class QuillToolbarSelectHeaderStyleDropdownButton extends StatefulWidget { const QuillToolbarSelectHeaderStyleDropdownButton({ required this.controller, required this.options, super.key, }); /// Since we can't get the state from the instace of the widget for comparing /// in [didUpdateWidget] then we will have to store reference here final QuillController controller; final QuillToolbarSelectHeaderStyleDropdownButtonOptions options; @override State createState() => _QuillToolbarSelectHeaderStyleDropdownButtonState(); } class _QuillToolbarSelectHeaderStyleDropdownButtonState extends State { Attribute? _selectedAttribute; Style get _selectionStyle => controller.getSelectionStyle(); late final _valueToText = { Attribute.header: context.loc.normal, Attribute.h1: context.loc.heading1, Attribute.h2: context.loc.heading2, Attribute.h3: context.loc.heading3, Attribute.h4: context.loc.heading4, Attribute.h5: context.loc.heading5, Attribute.h6: context.loc.heading6, }; Map? _headerTextStyles; QuillToolbarSelectHeaderStyleDropdownButtonOptions get options { return widget.options; } QuillController get controller { return widget.controller; } double get iconSize { final baseFontSize = baseButtonExtraOptions.globalIconSize; final iconSize = options.iconSize; return iconSize ?? baseFontSize; } double get iconButtonFactor { final baseIconFactor = baseButtonExtraOptions.globalIconButtonFactor; final iconButtonFactor = options.iconButtonFactor; return iconButtonFactor ?? baseIconFactor; } VoidCallback? get afterButtonPressed { return options.afterButtonPressed ?? baseButtonExtraOptions.afterButtonPressed; } QuillIconTheme? get iconTheme { return options.iconTheme ?? baseButtonExtraOptions.iconTheme; } QuillToolbarBaseButtonOptions get baseButtonExtraOptions { return context.requireQuillToolbarBaseButtonOptions; } String get tooltip { return options.tooltip ?? baseButtonExtraOptions.tooltip ?? context.loc.headerStyle; } List get _attrbuites { return options.attributes ?? _valueToText.keys.toList(); } @override void dispose() { controller.removeListener(_didChangeEditingValue); super.dispose(); } @override void didUpdateWidget( covariant QuillToolbarSelectHeaderStyleDropdownButton oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.controller != controller) { oldWidget.controller.removeListener(_didChangeEditingValue); controller.addListener(_didChangeEditingValue); _selectedAttribute = _getHeaderValue(); } } @override void didChangeDependencies() { super.didChangeDependencies(); if (_headerTextStyles == null) { final defaultStyles = QuillStyles.getStyles(context, false); _headerTextStyles = { Attribute.h1: defaultStyles!.h1!.style, Attribute.h2: defaultStyles.h2!.style, Attribute.h3: defaultStyles.h3!.style, Attribute.h4: defaultStyles.h4!.style, Attribute.h5: defaultStyles.h5!.style, Attribute.h6: defaultStyles.h6!.style, }; } } @override void initState() { super.initState(); controller.addListener(_didChangeEditingValue); _selectedAttribute = _getHeaderValue(); } @override Widget build(BuildContext context) { assert(_attrbuites.every((element) => _valueToText.keys.contains(element))); final baseButtonConfigurations = context.requireQuillToolbarBaseButtonOptions; final childBuilder = options.childBuilder ?? baseButtonConfigurations.childBuilder; if (childBuilder != null) { return childBuilder( options.copyWith( iconSize: iconSize, iconTheme: iconTheme, tooltip: tooltip, afterButtonPressed: afterButtonPressed, ), QuillToolbarSelectHeaderStyleDropdownButtonExtraOptions( currentValue: _selectedAttribute!, controller: controller, context: context, onPressed: _onPressed, ), ); } return ConstrainedBox( constraints: BoxConstraints.tightFor( height: iconSize * 1.81, width: options.width, ), child: UtilityWidgets.maybeTooltip( message: tooltip, child: RawMaterialButton( visualDensity: VisualDensity.compact, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(iconTheme?.borderRadius ?? 2), ), fillColor: options.fillColor, elevation: 0, hoverElevation: options.hoverElevation, highlightElevation: options.hoverElevation, onPressed: _onPressed, child: _buildContent(context), ), ), ); } void _didChangeEditingValue() { setState(() { _selectedAttribute = _getHeaderValue(); }); } Attribute _getHeaderValue() { final attr = controller.toolbarButtonToggler[Attribute.header.key]; if (attr != null) { // checkbox tapping causes controller.selection to go to offset 0 controller.toolbarButtonToggler.remove(Attribute.header.key); return attr; } return _selectionStyle.attributes[Attribute.header.key] ?? Attribute.header; } Widget _buildContent(BuildContext context) { final theme = Theme.of(context); final hasFinalWidth = options.width != null; return Padding( padding: options.padding ?? const EdgeInsets.fromLTRB(10, 0, 0, 0), child: Row( mainAxisSize: !hasFinalWidth ? MainAxisSize.min : MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ UtilityWidgets.maybeWidget( enabled: hasFinalWidth, wrapper: (child) => Expanded(child: child), child: Text( _valueToText[_selectedAttribute]!, overflow: options.labelOverflow, style: options.style ?? TextStyle( fontSize: iconSize / 1.15, color: iconTheme?.iconUnselectedColor ?? theme.iconTheme.color, ), ), ), const SizedBox(width: 3), Icon( Icons.arrow_drop_down, size: iconSize / 1.15, color: iconTheme?.iconUnselectedColor ?? theme.iconTheme.color, ) ], ), ); } void _onPressed() { _showMenu(); options.afterButtonPressed?.call(); } Future _showMenu() async { final popupMenuTheme = PopupMenuTheme.of(context); final button = context.findRenderObject() as RenderBox; final overlay = Overlay.of(context).context.findRenderObject() as RenderBox; final position = RelativeRect.fromRect( Rect.fromPoints( button.localToGlobal(Offset.zero, ancestor: overlay), button.localToGlobal(button.size.bottomLeft(Offset.zero), ancestor: overlay), ), Offset.zero & overlay.size, ); final newValue = await showMenu( context: context, elevation: 4, items: [ for (final header in _valueToText.entries) PopupMenuItem( key: ValueKey(header.value), value: header.key, height: options.itemHeight ?? kMinInteractiveDimension, padding: options.itemPadding, child: Text( header.value, style: TextStyle( fontSize: options.renderItemTextStyle ? _headerTextStyles![header.key]!.fontSize ?? DefaultTextStyle.of(context).style.fontSize ?? 14 : null, color: header.key == _selectedAttribute ? options.defaultItemColor : null, ), ), ), ], position: position, shape: popupMenuTheme.shape, color: popupMenuTheme.color, ); if (newValue == null) { return; } final attribute0 = _selectedAttribute == newValue ? Attribute.header : newValue; controller.formatSelection(attribute0); afterButtonPressed?.call(); } }