Skip to content

ClipboardMonitor causes background lag when enabling clipboard buttons #2421

@Maksim-Nikolaev

Description

@Maksim-Nikolaev

Is there an existing issue for this?

Flutter Quill version

flutter_quill: ^11.0.0-dev.17 & flutter_quill_extensions: ^11.0.0-dev.7

Steps to reproduce

  1. Add QuillSimpleToolbar and pass QuillSimpleToolbarConfig with showClipboardCut, showClipboardCopy, or showClipboardPaste set to true
  2. Since it adds the button with Clipboard Service it causes performance issues:
    Source code for reference:
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

import '../../common/utils/widgets.dart';
import '../../editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart';
import '../../l10n/extensions/localizations_ext.dart';
import '../base_button/base_value_button.dart';
import '../simple_toolbar.dart';

enum ClipboardAction { cut, copy, paste }

class ClipboardMonitor {
  bool _canPaste = false;
  bool get canPaste => _canPaste;
  Timer? _timer;

  void monitorClipboard(bool add, void Function() listener) {
    if (kIsWeb) return;
    if (add) {
      _timer = Timer.periodic(
          const Duration(seconds: 1), (timer) => _update(listener));
    } else {
      _timer?.cancel();
    }
  }

  Future<void> _update(void Function() listener) async {
    final clipboardService = ClipboardServiceProvider.instance;
    if (await clipboardService.hasClipboardContent) {
      _canPaste = true;
      listener();
    }
  }
}

class QuillToolbarClipboardButton extends QuillToolbarToggleStyleBaseButton {
  QuillToolbarClipboardButton({
    required super.controller,
    required this.clipboardAction,
    super.options = const QuillToolbarToggleStyleButtonOptions(),

    /// Shares common options between all buttons, prefer the [options]
    /// over the [baseOptions].
    super.baseOptions,
    super.key,
  });

  final ClipboardAction clipboardAction;

  final ClipboardMonitor _monitor = ClipboardMonitor();

  @override
  State<StatefulWidget> createState() => QuillToolbarClipboardButtonState();
}

class QuillToolbarClipboardButtonState
    extends QuillToolbarToggleStyleBaseButtonState<
        QuillToolbarClipboardButton> {
  @override
  bool get currentStateValue {
    switch (widget.clipboardAction) {
      case ClipboardAction.cut:
        return !controller.readOnly && !controller.selection.isCollapsed;
      case ClipboardAction.copy:
        return !controller.selection.isCollapsed;
      case ClipboardAction.paste:
        return !controller.readOnly && (kIsWeb || widget._monitor.canPaste);
    }
  }

  void _listenClipboardStatus() => didChangeEditingValue();

  @override
  void addExtraListener() {
    if (widget.clipboardAction == ClipboardAction.paste) {
      widget._monitor.monitorClipboard(true, _listenClipboardStatus);
    }
  }

  @override
  void removeExtraListener(covariant QuillToolbarClipboardButton oldWidget) {
    if (widget.clipboardAction == ClipboardAction.paste) {
      oldWidget._monitor.monitorClipboard(false, _listenClipboardStatus);
    }
  }

  @override
  String get defaultTooltip => switch (widget.clipboardAction) {
        ClipboardAction.cut => context.loc.cut,
        ClipboardAction.copy => context.loc.copy,
        ClipboardAction.paste => context.loc.paste,
      };

  @override
  IconData get defaultIconData => switch (widget.clipboardAction) {
        ClipboardAction.cut => Icons.cut_outlined,
        ClipboardAction.copy => Icons.copy_outlined,
        ClipboardAction.paste => Icons.paste_outlined,
      };

  void _onPressed() {
    switch (widget.clipboardAction) {
      case ClipboardAction.cut:
        controller.clipboardSelection(false);
        break;
      case ClipboardAction.copy:
        controller.clipboardSelection(true);
        break;
      case ClipboardAction.paste:
        controller.clipboardPaste();
        break;
    }
    afterButtonPressed?.call();
  }

  @override
  Widget build(BuildContext context) {
    final childBuilder = this.childBuilder;
    if (childBuilder != null) {
      return childBuilder(
        options,
        QuillToolbarToggleStyleButtonExtraOptions(
          context: context,
          controller: controller,
          onPressed: _onPressed,
          isToggled: currentValue,
        ),
      );
    }

    return UtilityWidgets.maybeTooltip(
        message: tooltip,
        child: QuillToolbarIconButton(
          icon: Icon(
            iconData,
            size: iconSize * iconButtonFactor,
          ),
          isSelected: false,
          onPressed: currentValue ? _onPressed : null,
          afterPressed: afterButtonPressed,
          iconTheme: iconTheme,
        ));
  }
}
  1. Since the QuillToolbarClipboardButton was added to the Widget tree, it starts affecting the app performance & memory on the background, even if you are not actively copying or pasting with clipboard at the moment.
W/Looper  ( 4641): PerfMonitor longMsg : seq=69 plan=18:41:47.480 late=0ms wall=1570ms h=android.os.Handler c=io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0 procState=-1
W/Looper  ( 4641): PerfMonitor longMsg : seq=71 plan=18:41:48.479 late=572ms wall=1418ms h=android.os.Handler c=io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0 procState=-1
W/Looper  ( 4641): PerfMonitor longMsg : seq=73 plan=18:41:49.479 late=991ms wall=1410ms h=android.os.Handler c=io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0 procState=-1
W/Looper  ( 4641): PerfMonitor longMsg : seq=74 plan=18:41:50.559 late=1323ms wall=1427ms h=android.os.Handler c=io.flutter.embedding.engine.dart.DartMessenger$$ExternalSyntheticLambda0 procState=-1
...
...

Expected results

Expected Clipboard to not be as heavy for the application and to not cause performance issues.

Actual results

ClipboardService causes background lag when used.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions