Text selection in Flutter PDF Viewer (SfPdfViewer)

5 Jun 202422 minutes to read

On a touch device, the SfPdfViewer allows you to select a text in the PDF page by long pressing on it, which in turn displays the selection handles or bubbles at the top-left and bottom-right corners of its bounds. Then, you can use the left handle to select the text at the left and top, and the right handle to select the text at the right and bottom directions.

And on a desktop web browser, the text selection can also be performed using mouse dragging with the selection interaction mode enabled.

NOTE

The images in the document will not be selected and, the multiple-page text selection is not supported for now.

Enable or disable text selection

You can enable or disable the text selection in the PDF page using the enableTextSelection property. The following code example explains the same.

@override
Widget build(BuildContext context) {
  return Scaffold(
      body: SfPdfViewer.network(
              'https://cdn.syncfusion.com/content/PDFViewer/flutter-succinctly.pdf',
              enableTextSelection: false));
}

NOTE

On a desktop web browser, this enableTextSelection property will have no effect on the pan interaction mode.

Customize the text selection and its handle color

The SfPdfViewer allows you to customize the color used for text selection and its handle based on your requirements. The properties selectionColor and selectionHandleColor of the TextSelectionThemeData class can be used to customize them. The following code example explains the same.

void main() => runApp(MaterialApp(
      title: 'Syncfusion PDF Viewer Demo',
      theme: ThemeData(
        textSelectionTheme: TextSelectionThemeData(
            selectionColor: Colors.red, selectionHandleColor: Colors.blue),
      ),
      home: HomePage(),
    ));

/// Represents Homepage for Navigation
class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text('Syncfusion Flutter PDF Viewer'),
        ),
        body: Container(
            child: SfPdfViewer.network(
          'https://cdn.syncfusion.com/content/PDFViewer/flutter-succinctly.pdf',
        )));
  }
}

Customize the visibility of the text selection context menu

The canShowTextSelectionMenu property allows the user to customize the visibility of the built-in text selection context menu. You can assign false to this property to disable the text selection context menu. The following code example explains how to disable the built-in text selection context menu in the PDF viewer.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Container(
      child: SfPdfViewer.network(
        'https://cdn.syncfusion.com/content/PDFViewer/encrypted.pdf',
        canShowTextSelectionMenu: false,
      ),
    ),
  );
}

Callbacks

The SfPdfViewer text selection supports the PdfTextSelectionChangedCallback to notify the text selection changes.

Text selection changed callback

The onTextSelectionChanged callback triggers when the text is selected or deselected in the SfPdfViewer. The PdfTextSelectionChangedDetails will hold the globalSelectedRegion representing the global bounds information of the selected text region and the selectedText representing the selected text value. The following code example explains the same.

@override
Widget build(BuildContext context) {
  return Scaffold(
      appBar: AppBar(
        title: Text('Syncfusion Flutter PDF Viewer'),
      ),
      body: SfPdfViewer.network(
        'https://cdn.syncfusion.com/content/PDFViewer/flutter-succinctly.pdf',
        onTextSelectionChanged: (PdfTextSelectionChangedDetails details) {
          if (details.selectedText != null) {
            print(details.selectedText);
          }
        },
      ));
}

How to get the selected text lines in the PDF viewer?

Using the getSelectedTextLines method, you can get the selected text lines in the PDF viewer. The following code example explains the same.

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('Syncfusion Flutter PDF Viewer'),
      actions: <Widget>[
        IconButton(
          icon: const Icon(Icons.add),
          onPressed: () async {
            final List<PdfTextLine>? selectedTextLines =
                _pdfViewerKey.currentState?.getSelectedTextLines();

            if (selectedTextLines != null && selectedTextLines.isNotEmpty) {
              // Creates a highlight annotation with the selected text lines.
              final HighlightAnnotation highlightAnnotation =
                  HighlightAnnotation(
                textBoundsCollection: selectedTextLines,
              );
              // Adds the highlight annotation to the PDF document.
              _pdfViewerController.addAnnotation(highlightAnnotation);
            }
          },
        ),
      ],
    ),
    body: SfPdfViewer.asset(
      'assets/sample.pdf',
      key: _pdfViewerKey,
      controller: _pdfViewerController,
    ),
  );
}

How to create and display a customized text selection context menu with copy and text markup options to retrieve the selected text?

With the options available in SfPdfViewer text selection, you can easily create and display a customized text selection context menu with various options such as Copy, Highlight, Underline, Strikethrough, and Squiggly, and perform operations with them. The following code example explains how to implement this functionality.

In this example, we use the OverlayEntry widget to create the customized context menu and add buttons for various options, such as Copy, Highlight, Underline, Strikethrough, and Squiggly. We are calling the method to display the customized context menu in the onTextSelectionChanged callback. Whenever one of these options is pressed, the corresponding action is performed on the selected text.

  • For the copy operation, the selected text value is retrieved from the’selectedText` property of the onTextSelectionChanged callback details. We then copy the text to the clipboard using the Clipboard.setData method.
    The text selection is cleared after the Copy operation by calling the clearSelection controller method.

  • For the text markup options, we retrieve the selected text lines using the getSelectedTextLines method and add the respective annotations using the addAnnotation method on the PdfViewerController.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart';

void main() => runApp(const MaterialApp(
      title: 'Syncfusion PDF Viewer Demo',
      home: HomePage(),
    ));

/// Represents Homepage for Navigation
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  late PdfViewerController _pdfViewerController;
  OverlayEntry? _overlayEntry;
  final GlobalKey<SfPdfViewerState> _pdfViewerKey = GlobalKey();

  @override
  void initState() {
    _pdfViewerController = PdfViewerController();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Syncfusion Flutter PDF Viewer'),
      ),
      body: SfPdfViewer.network(
        'https://cdn.syncfusion.com/content/PDFViewer/flutter-succinctly.pdf',
        onTextSelectionChanged: (PdfTextSelectionChangedDetails details) {
          if (details.selectedText == null && _overlayEntry != null) {
            _overlayEntry!.remove();
            _overlayEntry = null;
          } else if (details.selectedText != null && _overlayEntry == null) {
            _showContextMenu(context, details);
          }
        },
        controller: _pdfViewerController,
        canShowTextSelectionMenu: false,
      ),
    );
  }

  void _showContextMenu(
      BuildContext context, PdfTextSelectionChangedDetails details) {
    const double height = 250;
    const double width = 150;
    final OverlayState overlayState = Overlay.of(context);
    final Size screenSize = MediaQuery.of(context).size;

    double top = details.globalSelectedRegion!.top >= screenSize.height / 2
        ? details.globalSelectedRegion!.top - height - 10
        : details.globalSelectedRegion!.bottom + 20;
    top = top < 0 ? 20 : top;
    top = top + height > screenSize.height
        ? screenSize.height - height - 10
        : top;

    double left = details.globalSelectedRegion!.bottomLeft.dx;
    left = left < 0 ? 10 : left;
    left =
        left + width > screenSize.width ? screenSize.width - width - 10 : left;
    _overlayEntry = OverlayEntry(
      builder: (BuildContext context) => Positioned(
        top: top,
        left: left,
        child: Container(
          decoration: BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.circular(4),
            boxShadow: const <BoxShadow>[
              BoxShadow(
                color: Colors.black26,
                blurRadius: 4,
                offset: Offset(2, 2),
              ),
            ],
          ),
          constraints:
              const BoxConstraints.tightFor(width: width, height: height),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: <Widget>[
              TextButton(
                onPressed: () {
                  if (details.selectedText != null) {
                    Clipboard.setData(
                        ClipboardData(text: details.selectedText!));
                    print('Text copied to clipboard: ${details.selectedText}');
                    _pdfViewerController.clearSelection();
                  }
                },
                child: const Text('Copy', style: TextStyle(fontSize: 15)),
              ),
              TextButton(
                onPressed: () {
                  final List<PdfTextLine>? textLines =
                      _pdfViewerKey.currentState?.getSelectedTextLines();
                  if (textLines != null && textLines.isNotEmpty) {
                    final HighlightAnnotation highlightAnnotation =
                        HighlightAnnotation(
                      textBoundsCollection: textLines,
                    );
                    _pdfViewerController.addAnnotation(highlightAnnotation);
                  }
                },
                child: const Text('Highlight', style: TextStyle(fontSize: 15)),
              ),
              TextButton(
                onPressed: () {
                  final List<PdfTextLine>? textLines =
                      _pdfViewerKey.currentState?.getSelectedTextLines();
                  if (textLines != null && textLines.isNotEmpty) {
                    final UnderlineAnnotation underLineAnnotation =
                        UnderlineAnnotation(
                      textBoundsCollection: textLines,
                    );
                    _pdfViewerController.addAnnotation(underLineAnnotation);
                  }
                },
                child: const Text('Underline', style: TextStyle(fontSize: 15)),
              ),
              TextButton(
                onPressed: () {
                  final List<PdfTextLine>? textLines =
                      _pdfViewerKey.currentState?.getSelectedTextLines();
                  if (textLines != null && textLines.isNotEmpty) {
                    final StrikethroughAnnotation strikethroughAnnotation =
                        StrikethroughAnnotation(
                      textBoundsCollection: textLines,
                    );
                    _pdfViewerController.addAnnotation(strikethroughAnnotation);
                  }
                },
                child:
                    const Text('Strikethrough', style: TextStyle(fontSize: 15)),
              ),
              TextButton(
                onPressed: () {
                  final List<PdfTextLine>? textLines =
                      _pdfViewerKey.currentState?.getSelectedTextLines();
                  if (textLines != null && textLines.isNotEmpty) {
                    final SquigglyAnnotation squigglyAnnotation =
                        SquigglyAnnotation(
                      textBoundsCollection: textLines,
                    );
                    _pdfViewerController.addAnnotation(squigglyAnnotation);
                  }
                },
                child: const Text('Squiggly', style: TextStyle(fontSize: 15)),
              ),
            ],
          ),
        ),
      ),
    );
    overlayState.insert(_overlayEntry!);
  }
}