Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Block Attribute issue #1881

Closed
1 task done
CatHood0 opened this issue May 20, 2024 · 3 comments · Fixed by #1886
Closed
1 task done

Custom Block Attribute issue #1881

CatHood0 opened this issue May 20, 2024 · 3 comments · Fixed by #1886
Labels
bug Something isn't working

Comments

@CatHood0
Copy link

CatHood0 commented May 20, 2024

Is there an existing issue for this?

Flutter Quill version

9.3.13

Steps to reproduce

Write some lines with random text (apply the line-height that you want with the QuillLineHeightButton), go to another line, and then, try to change to another line height. The change won't be applied.

Expected results

I just expect it works as block attribute should be.

Actual results

The issue described in the steps. In certain situations line-height (a block attr) cannot be removed or updated to another. But also, if we use align (i don't test other blocks attrs like header at this moment) attr after use line height to a paragraph, the custom attr will be deleted and just will be applied Align

Code sample

Code sample
/// Attribute with issue

const String lineHeightKey = 'line-height';
const AttributeScope lineHeightScope = AttributeScope.block;

class LineHeightAttribute extends Attribute<String?> {
  const LineHeightAttribute({String? value = "1.0"})
      : super(
          lineHeightKey,
          lineHeightScope,
          value,
        );
}
///Custom quill toolbar button implementation

class QuillLineHeightButton extends QuillToolbarBaseValueButton<QuillLineHeightButtonOptions, QuillLineHeightButtonExtraOptions> {
  QuillLineHeightButton({
    required super.controller,
    @Deprecated('Please use the default display text from the options') this.defaultDisplayText,
    super.options = const QuillLineHeightButtonOptions(),
    super.key,
  })  : assert(options.rawItems?.isNotEmpty ?? true),
        assert(options.initialValue == null || (options.initialValue?.isNotEmpty ?? true));

  final String? defaultDisplayText;

  @override
  QuillLineHeightButtonState createState() => QuillLineHeightButtonState();
}

class QuillLineHeightButtonState extends QuillToolbarBaseValueButtonState<QuillLineHeightButton, QuillLineHeightButtonOptions,
    QuillLineHeightButtonExtraOptions, String> {
  Size? size;
  final MenuController _menuController = MenuController();

  List<String> get rawItemsMap {
    const List<String> spacings = ["1.0","1.15","1.5","2.0"];
    return spacings;
  }

  String get _defaultDisplayText {
    return options.initialValue ?? widget.options.defaultDisplayText ?? widget.defaultDisplayText ?? context.loc.fontSize;
  }

  @override
  String get currentStateValue {
    final Attribute<dynamic>? attribute = controller.getSelectionStyle().attributes[lineHeightKey];
    return attribute == null ? _defaultDisplayText : attribute.value ?? _defaultDisplayText;
  }

  @override
  String get defaultTooltip => context.loc.fontSize;

  void _onDropdownButtonPressed() {
    if (_menuController.isOpen) {
      _menuController.close();
    } else {
      _menuController.open();
    }
    afterButtonPressed?.call();
  }

  @override
  Widget build(BuildContext context) {
    size ??= MediaQuery.sizeOf(context);
    return MenuAnchor(
      controller: _menuController,
      menuChildren: rawItemsMap.map((String spacing) {
        return MenuItemButton(
          key: ValueKey<String>(spacing),
          onPressed: () {
            final String newValue = spacing;
            final attribute0 = currentValue == spacing ? const LineHeightAttribute(value: null) : LineHeightAttribute(value: newValue);
            controller.formatSelection(attribute0);
            setState(() {
              currentValue = newValue;
              options.onSelected?.call(newValue);
            });
          },
          child: SizedBox(
            height: 65,
            child: Row(
              children: [
                const Icon(Icons.format_line_spacing),
                const SizedBox(width: 5),
                Text.rich(
                  TextSpan(
                    children: [
                      TextSpan(
                          text: 'Spacing: ',
                          style: TextStyle(fontWeight: currentValue == spacing ? FontWeight.bold : FontWeight.w300)),
                      TextSpan(
                        text: spacing,
                        style: TextStyle(fontWeight: currentValue == spacing ? FontWeight.bold : FontWeight.w300),
                      ),
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
      }).toList(),
      child: Builder(
        builder: (BuildContext context) {
          final bool isMaterial3 = Theme.of(context).useMaterial3;
          if (!isMaterial3) {
            return RawMaterialButton(
              onPressed: _onDropdownButtonPressed,
              child: _buildContent(context),
            );
          }
          return QuillToolbarIconButton(
            tooltip: tooltip,
            isSelected: false,
            iconTheme: iconTheme,
            onPressed: _onDropdownButtonPressed,
            icon: _buildContent(context),
          );
        },
      ),
    );
  }

  Widget _buildContent(BuildContext context) {
    final bool 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: <Widget>[
          UtilityWidgets.maybeWidget(
            enabled: hasFinalWidth,
            wrapper: (Widget child) => Expanded(child: child),
            child: Text(
              currentValue,
              overflow: options.labelOverflow,
              style: options.style ??
                  TextStyle(
                    fontSize: iconSize / 1.15,
                  ),
            ),
          ),
          Icon(
            Icons.arrow_drop_down,
            size: iconSize * iconButtonFactor,
          )
        ],
      ),
    );
  }
}

/// The [T] is the options for the button
/// The [E] is the extra options for the button
abstract class QuillToolbarBaseValueButton<T extends QuillToolbarBaseButtonOptions<T, E>, E extends QuillToolbarBaseButtonExtraOptions>
    extends StatefulWidget {
  const QuillToolbarBaseValueButton({required this.controller, required this.options, super.key});

  final T options;

  final QuillController controller;
}

/// The [W] is the widget that creates this State
/// The [V] is the type of the currentValue
abstract class QuillToolbarBaseValueButtonState<W extends QuillToolbarBaseValueButton<T, E>, T extends QuillToolbarBaseButtonOptions<T, E>,
    E extends QuillToolbarBaseButtonExtraOptions, V> extends State<W> {
  T get options => widget.options;

  QuillController get controller => widget.controller;

  late V currentValue;

  /// Callback to query the widget's state for the value to be assigned to currentState
  V get currentStateValue;

  @override
  void initState() {
    super.initState();
    controller.addListener(didChangeEditingValue);
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    currentValue = currentStateValue;
  }

  void didChangeEditingValue() {
    setState(() {
      currentValue = currentStateValue;
    });
  }

  @override
  void dispose() {
    controller.removeListener(didChangeEditingValue);
    super.dispose();
  }

  @override
  void didUpdateWidget(covariant W oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.controller != controller) {
      oldWidget.controller.removeListener(didChangeEditingValue);
      controller.addListener(didChangeEditingValue);
      currentValue = currentStateValue;
    }
  }

  String get defaultTooltip;

  String get tooltip {
    return options.tooltip ?? context.quillToolbarBaseButtonOptions?.tooltip ?? defaultTooltip;
  }

  double get iconSize {
    final double? baseFontSize = baseButtonExtraOptions?.iconSize;
    final double? iconSize = options.iconSize;
    return iconSize ?? baseFontSize ?? kDefaultIconSize;
  }

  double get iconButtonFactor {
    final double? baseIconFactor = baseButtonExtraOptions?.iconButtonFactor;
    final double? iconButtonFactor = options.iconButtonFactor;
    return iconButtonFactor ?? baseIconFactor ?? kDefaultIconButtonFactor;
  }

  QuillIconTheme? get iconTheme {
    return options.iconTheme ?? baseButtonExtraOptions?.iconTheme;
  }

  QuillToolbarBaseButtonOptions? get baseButtonExtraOptions {
    return context.quillToolbarBaseButtonOptions;
  }

  VoidCallback? get afterButtonPressed {
    return options.afterButtonPressed ?? baseButtonExtraOptions?.afterButtonPressed;
  }
}

class QuillLineHeightButtonExtraOptions extends QuillToolbarBaseButtonExtraOptions {
  const QuillLineHeightButtonExtraOptions({
    required super.controller,
    required this.currentValue,
    required this.defaultDisplayText,
    required super.context,
    required super.onPressed,
  });

  final String currentValue;
  final String defaultDisplayText;
}

@immutable
class QuillLineHeightButtonOptions extends QuillToolbarBaseButtonOptions<QuillLineHeightButtonOptions, QuillLineHeightButtonExtraOptions> {
  const QuillLineHeightButtonOptions({
    super.iconSize,
    super.iconButtonFactor,
    this.rawItems,
    this.onSelected,
    this.attribute = const LineHeightAttribute(value: "1.0"),
    super.afterButtonPressed,
    super.tooltip,
    this.padding,
    this.style,
    @Deprecated('No longer used') this.width,
    this.initialValue,
    this.labelOverflow = TextOverflow.visible,
    this.itemHeight,
    this.itemPadding,
    this.defaultItemColor = Colors.red,
    super.childBuilder,
    this.shape,
    this.defaultDisplayText,
  });

  final ButtonStyle? shape;

  final List<String>? rawItems;
  final ValueChanged<String>? onSelected;
  final LineHeightAttribute attribute;
  final EdgeInsetsGeometry? padding;
  final TextStyle? style;
  final double? width;
  final String? initialValue;
  final TextOverflow labelOverflow;
  @Deprecated('No longer used')
  final double? itemHeight;
  @Deprecated('No longer used')
  final EdgeInsets? itemPadding;
  final Color? defaultItemColor;
  final String? defaultDisplayText;

  QuillLineHeightButtonOptions copyWith({
    double? iconSize,
    double? iconButtonFactor,
    double? hoverElevation,
    double? highlightElevation,
    List<PopupMenuEntry<String>>? items,
    List<String>? rawItems,
    ValueChanged<String>? onSelected,
    LineHeightAttribute? attribute,
    EdgeInsetsGeometry? padding,
    TextStyle? style,
    double? width,
    String? initialValue,
    TextOverflow? labelOverflow,
    double? itemHeight,
    EdgeInsets? itemPadding,
    Color? defaultItemColor,
    VoidCallback? afterButtonPressed,
    String? tooltip,
    OutlinedBorder? shape,
    String? defaultDisplayText,
  }) {
    return QuillLineHeightButtonOptions(
      iconSize: iconSize ?? this.iconSize,
      iconButtonFactor: iconButtonFactor ?? this.iconButtonFactor,
      rawItems: rawItems ?? this.rawItems,
      onSelected: onSelected ?? this.onSelected,
      attribute: attribute ?? this.attribute,
      padding: padding ?? this.padding,
      style: style ?? this.style,
      // ignore: deprecated_member_use_from_same_package
      width: width ?? this.width,
      initialValue: initialValue ?? this.initialValue,
      labelOverflow: labelOverflow ?? this.labelOverflow,
      // ignore: deprecated_member_use_from_same_package
      itemHeight: itemHeight ?? this.itemHeight,
      // ignore: deprecated_member_use_from_same_package
      itemPadding: itemPadding ?? this.itemPadding,
      defaultItemColor: defaultItemColor ?? this.defaultItemColor,
      tooltip: tooltip ?? super.tooltip,
      afterButtonPressed: afterButtonPressed ?? super.afterButtonPressed,
      defaultDisplayText: defaultDisplayText ?? this.defaultDisplayText,
    );
  }
}

//And then just add to the QuillEditorConfigurations
customStyleBuilder: (Attribute<dynamic> attribute) {
          if (attribute.key.equals('line-height')) {
            return TextStyle(
              height: double.parse(attribute.value),
            );
          }
return TextStyle();
},

Screenshots or Video

Example video of how works and the issue

screen-20240520-011733.mp4

Result delta and json

Screenshot_20240520-012329~2

Logs

Logs
[Paste your logs here]
@CatHood0 CatHood0 added the bug Something isn't working label May 20, 2024
@CatHood0
Copy link
Author

@AtlasAutocode could you please take a look if you're able to fix this

@AtlasAutocode
Copy link
Contributor

Wow, that looks really good. Line spacing is an important word processing function - have you considered contributing it?
I believe, my fault is using the pre-defined set of Attribute keys for the block scope without realizing that people could define their own attributes....
I have updated the code to take this into account - thank you - and will submit PR soon.
I assume you will be implementing the code to actually use your LineHeight settings in editor display. (I would be most interested in how you do this.)
I wonder if it would be a good idea to change to use 'LineSpacing' and pick a different tooltip/default text? The use of 'Font Size' conflicts with the current Font Size used for font point size.

@CatHood0
Copy link
Author

CatHood0 commented May 21, 2024

Wow, that looks really good. Line spacing is an important word processing function - have you considered contributing it?

I considered contribute with some ideas, but i'm developing on them first, in my app, and later i just will post as a feature here. One of them is transforming deltas to pdf but more complex and with capability to add custom widgets.

Focusing on my issue

i notice of this error when i record the last video:

assert(after.isPlain);

If your watch the video (minute 0:21), there's a moment where i delete a line, and try to put a new line at that same position, i can't do it. This is related with the insert at line 62. Idk what it means (i working just 1 year and know how works deltas, but rules, and formatting it's difficult to me to resolve this issue)

I assume you will be implementing the code to actually use your LineHeight settings in editor display. (I would be most interested in how you do this.)

The only thing that i didn't show, is a function that makes more easy read text to user:

 double resolveLineHeightEditor() {
    if (this == 2.0) return 2.0;
    if (this == 1.5) return 1.55;
    if (this == 1.15) return 1.30;
    if (this == 1.0) return 1.15;
    return this;
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants