From 9263a7a5aad8678c99b83a5827bfe30b977c1d79 Mon Sep 17 00:00:00 2001 From: Julius Kato Mutumba Date: Wed, 20 Aug 2025 22:47:10 +0300 Subject: [PATCH 1/4] hide delete button based on annotation custom data --- .../flutter/pspdfkit/FlutterPdfUiFragment.kt | 68 ++++- example/lib/examples.dart | 14 + .../lib/hide_delete_annotation_example.dart | 273 ++++++++++++++++++ ios/Classes/PspdfkitPlatformViewImpl.swift | 87 ++++++ 4 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 example/lib/hide_delete_annotation_example.dart diff --git a/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt b/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt index 3db24e24..12300fea 100644 --- a/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt +++ b/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt @@ -30,14 +30,20 @@ import androidx.lifecycle.Lifecycle import com.pspdfkit.document.PdfDocument import com.pspdfkit.flutter.pspdfkit.api.CustomToolbarCallbacks import com.pspdfkit.ui.PdfUiFragment +import com.pspdfkit.ui.toolbar.ContextualToolbar +import com.pspdfkit.ui.toolbar.ToolbarCoordinatorLayout +import com.pspdfkit.annotations.Annotation import com.pspdfkit.R -class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider { +class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLayout.OnContextualToolbarLifecycleListener { // Maps identifier strings to menu item IDs to track custom toolbar items private val customToolbarItemIds = HashMap() private var customToolbarCallbacks: CustomToolbarCallbacks? = null private var customToolbarItems: List>? = null + + // Store current contextual toolbar for annotation access + private var currentContextualToolbar: ContextualToolbar<*>? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -60,6 +66,11 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider { return super.onCreateView(inflater, container, savedInstanceState) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setOnContextualToolbarLifecycleListener(this) + } + /** * Called when the document is loaded. Notifies Flutter that the document has been loaded. * @@ -300,4 +311,59 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider { } } } + + /** + * Helper method to determine if the delete button should be hidden based on annotation custom data. + * + * @return True if delete button should be hidden, false otherwise + */ + private fun shouldHideDeleteButton(): Boolean { + try { + if (document != null) { + Log.d("FlutterPdfUiFragment", "Checking annotations for hideDelete custom data") + + val annotations: List = pdfFragment?.selectedAnnotations ?: emptyList() + + for (annotation in annotations) { + val customData = annotation.customData + if (customData != null && customData.has("hideDelete")) { + Log.d("FlutterPdfUiFragment", "Annotation ${annotation.type} has hideDelete custom data") + return true + } + } + } + return false + } catch (e: Exception) { + Log.e("FlutterPdfUiFragment", "Error checking annotation custom data", e) + return false + } + } + + override fun onPrepareContextualToolbar(toolbar: ContextualToolbar<*>) { + currentContextualToolbar = toolbar + val shouldHideDelete = shouldHideDeleteButton() + + if (shouldHideDelete) { + toolbar.setMenuItemVisibility( + R.id.pspdf__annotation_editing_toolbar_item_delete, + View.GONE + ) + } + } + + override fun onDisplayContextualToolbar(toolbar: ContextualToolbar<*>) { + currentContextualToolbar = toolbar + val shouldHideDelete = shouldHideDeleteButton() + + if (shouldHideDelete) { + toolbar.setMenuItemVisibility( + R.id.pspdf__annotation_editing_toolbar_item_delete, + View.GONE + ) + } + } + + override fun onRemoveContextualToolbar(toolbar: ContextualToolbar<*>) { + currentContextualToolbar = null + } } \ No newline at end of file diff --git a/example/lib/examples.dart b/example/lib/examples.dart index 98bc79eb..6b474d7c 100644 --- a/example/lib/examples.dart +++ b/example/lib/examples.dart @@ -42,6 +42,7 @@ import 'manual_save_example.dart'; import 'annotation_processing_example.dart'; import 'password_example.dart'; import 'nutrient_annotation_creation_mode_example.dart'; +import 'hide_delete_annotation_example.dart'; const String _documentPath = 'PDFs/PSPDFKit.pdf'; const String _measurementsDocs = 'PDFs/Measurements.pdf'; @@ -110,6 +111,12 @@ List examples(BuildContext context) => [ 'Programmatically adds and removes annotations using a custom Widget.', onTap: () => annotationsExample(context), ), + NutrientExampleItem( + title: 'Hide Delete Button on Annotations', + description: + 'Demonstrates how to use custom data to conditionally hide delete buttons on annotations.', + onTap: () => hideDeleteAnnotationExample(context), + ), NutrientExampleItem( title: 'Annotation Flags Example', description: 'Shows how to click an annotation and modify its flags.', @@ -614,3 +621,10 @@ void annotationFlagsExample(BuildContext context) { context, ); } + +void hideDeleteAnnotationExample(BuildContext context) async { + final extractedDocument = await extractAsset(context, _documentPath); + await Navigator.of(context).push(MaterialPageRoute( + builder: (_) => HideDeleteAnnotationExampleWidget( + documentPath: extractedDocument.path))); +} diff --git a/example/lib/hide_delete_annotation_example.dart b/example/lib/hide_delete_annotation_example.dart new file mode 100644 index 00000000..8a25dbb4 --- /dev/null +++ b/example/lib/hide_delete_annotation_example.dart @@ -0,0 +1,273 @@ +/// +/// Copyright @ 2018-2025 PSPDFKit GmbH. All rights reserved. +/// +/// THIS SOURCE CODE AND ANY ACCOMPANYING DOCUMENTATION ARE PROTECTED BY INTERNATIONAL COPYRIGHT LAW +/// AND MAY NOT BE RESOLD OR REDISTRIBUTED. USAGE IS BOUND TO THE PSPDFKIT LICENSE AGREEMENT. +/// UNAUTHORIZED REPRODUCTION OR DISTRIBUTION IS SUBJECT TO CIVIL AND CRIMINAL PENALTIES. +/// This notice may not be removed from this file. +/// +/// ignore_for_file: use_build_context_synchronously + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:nutrient_flutter/nutrient_flutter.dart'; +import 'utils/platform_utils.dart'; +import 'widgets/pdf_viewer_scaffold.dart'; + +/// Example demonstrating how to use the hideDelete custom data to conditionally +/// hide delete buttons on annotations in the PDF viewer. +class HideDeleteAnnotationExampleWidget extends StatefulWidget { + final String documentPath; + final PdfConfiguration? configuration; + + const HideDeleteAnnotationExampleWidget({ + Key? key, + required this.documentPath, + this.configuration, + }) : super(key: key); + + @override + State createState() => + _HideDeleteAnnotationExampleWidgetState(); +} + +class _HideDeleteAnnotationExampleWidgetState + extends State { + late NutrientViewController view; + late PdfDocument? document; + + @override + Widget build(BuildContext context) { + if (PlatformUtils.isCurrentPlatformSupported()) { + return Scaffold( + appBar: AppBar( + title: const Text('Hide Delete Button Example'), + ), + body: Column( + children: [ + // Control buttons at the top + Container( + padding: const EdgeInsets.all(16.0), + color: Colors.grey[100], + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton( + onPressed: _addProtectedAnnotations, + child: const Text('Add Protected'), + ), + ), + const SizedBox(width: 4), + Expanded( + child: ElevatedButton( + onPressed: _addEditableAnnotations, + child: const Text('Add Editable'), + ), + ), + const SizedBox(width: 4), + Expanded( + child: ElevatedButton( + onPressed: _clearAllAnnotations, + child: const Text('Clear All'), + ), + ), + ], + ), + ), + // PDF viewer + Expanded( + child: PdfViewerScaffold( + documentPath: widget.documentPath, + configuration: widget.configuration, + onPdfDocumentLoaded: (document) { + setState(() { + this.document = document; + }); + }, + onNutrientWidgetCreated: (controller) { + view = controller; + }, + ), + ), + ], + ), + ); + } else { + return Scaffold( + appBar: AppBar(title: const Text('Hide Delete Button Example')), + body: Center( + child: Text( + '$defaultTargetPlatform is not yet supported by PSPDFKit for Flutter.', + ), + ), + ); + } + } + + /// Adds annotations with hideDelete: true custom data + /// These annotations will not show a delete button in their contextual menu + Future _addProtectedAnnotations() async { + final protectedAnnotations = [ + // Protected highlight annotation - delete button will be hidden + HighlightAnnotation( + id: 'protected-highlight-1', + name: 'Protected Highlight', + bbox: [50.0, 400.0, 300.0, 20.0], + createdAt: DateTime.now().toIso8601String(), + color: const Color(0xFFFFEB3B), + rects: [ + [50.0, 400.0, 350.0, 420.0], + ], + opacity: 0.7, + pageIndex: 0, + creatorName: 'System', + customData: { + 'hideDelete': 'true', // This will hide the delete button + 'protected': 'true', + 'reason': 'System generated annotation', + }, + ), + + // Protected note annotation - delete button will be hidden + NoteAnnotation( + id: 'protected-note-1', + name: 'Protected Note', + bbox: [400.0, 400.0, 32.0, 32.0], + createdAt: DateTime.now().toIso8601String(), + text: TextContent( + value: 'This is a protected note that cannot be deleted', + format: TextFormat.plain, + ), + color: const Color(0xFFFF5722), + pageIndex: 0, + creatorName: 'Administrator', + customData: { + 'hideDelete': true, // Boolean value also works + 'protected': true, + 'adminGenerated': true, + }, + ), + + // Protected free text annotation - delete button will be hidden + FreeTextAnnotation( + id: 'protected-freetext-1', + name: 'Protected Free Text', + bbox: [50.0, 500.0, 200.0, 50.0], + createdAt: DateTime.now().toIso8601String(), + text: TextContent( + format: TextFormat.plain, + value: 'PROTECTED CONTENT', + ), + fontColor: const Color(0xFFFFFFFF), + fontSize: 16, + font: 'sans-serif', + pageIndex: 0, + creatorName: 'System', + backgroundColor: const Color(0xFFFF5722), + horizontalTextAlign: HorizontalTextAlignment.center, + verticalAlign: VerticalAlignment.center, + customData: { + 'hideDelete': 'true', + 'systemGenerated': true, + 'importance': 'high', + }, + ), + ]; + + await document?.addAnnotations(protectedAnnotations); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Protected annotations added! Try to select them - no delete button will appear.'), + backgroundColor: Colors.orange, + ), + ); + } + } + + /// Adds normal annotations without hideDelete custom data + /// These annotations will show the delete button normally + Future _addEditableAnnotations() async { + final editableAnnotations = [ + // Normal highlight annotation - delete button will be visible + HighlightAnnotation( + id: 'editable-highlight-1', + name: 'Editable Highlight', + bbox: [50.0, 300.0, 300.0, 20.0], + createdAt: DateTime.now().toIso8601String(), + color: const Color(0xFF4CAF50), + rects: [ + [50.0, 300.0, 350.0, 320.0], + ], + opacity: 0.7, + pageIndex: 0, + creatorName: 'User', + customData: { + 'editable': true, + 'userGenerated': true, + // Note: no hideDelete property, so delete button will be visible + }, + ), + + // Normal note annotation - delete button will be visible + NoteAnnotation( + id: 'editable-note-1', + name: 'Editable Note', + bbox: [400.0, 300.0, 32.0, 32.0], + createdAt: DateTime.now().toIso8601String(), + text: TextContent( + value: 'This note can be deleted normally', + format: TextFormat.plain, + ), + color: const Color(0xFF2196F3), + pageIndex: 0, + creatorName: 'User', + customData: { + 'editable': true, + 'userGenerated': true, + // hideDelete is not set, so delete button will appear + }, + ), + ]; + + await document?.addAnnotations(editableAnnotations); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Editable annotations added! These will show the delete button.'), + backgroundColor: Colors.green, + ), + ); + } + } + + /// Removes all annotations from the page + Future _clearAllAnnotations() async { + try { + final annotations = await document?.getAnnotations(0, AnnotationType.all); + if (annotations == null) return; + + for (var annotation in annotations) { + await document?.removeAnnotation(annotation); + } + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('All annotations cleared!'), + backgroundColor: Colors.grey, + ), + ); + } + } catch (e) { + if (kDebugMode) { + print('Error clearing annotations: $e'); + } + } + } +} diff --git a/ios/Classes/PspdfkitPlatformViewImpl.swift b/ios/Classes/PspdfkitPlatformViewImpl.swift index 05c170ae..767825df 100644 --- a/ios/Classes/PspdfkitPlatformViewImpl.swift +++ b/ios/Classes/PspdfkitPlatformViewImpl.swift @@ -114,6 +114,93 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV // This ensures our tap recognizer doesn't block PSPDFKit's built-in gestures return true } + + // MARK: - Menu Filtering Delegate Methods + + public func pdfViewController(_ pdfController: PDFViewController, + menuForAnnotations annotations: [Annotation], + onPageView pageView: PDFPageView, + appearance: EditMenuAppearance, + suggestedMenu: UIMenu) -> UIMenu { + print("*** menuForAnnotations delegate method called! Annotations count: \(annotations.count)") + + if suggestedMenu.children.isEmpty { + print("*** No suggested menu or empty menu, returning original") + return suggestedMenu + } + + print("*** Original menu has \(suggestedMenu.children.count) children") + + // Check if delete should be hidden based on annotation custom data + let shouldHideDelete = shouldHideDeleteButton(for: annotations) + + // Filter out delete actions conditionally based on custom data + let filteredMenu = filterActionsFromMenu(suggestedMenu, shouldHideDelete: shouldHideDelete) + + print("*** Final menu has \(filteredMenu.children.count) children") + + return filteredMenu + } + + // MARK: - Menu Filtering Helper + + /** + * Helper method to determine if the delete button should be hidden based on annotation custom data. + * + * @param annotations The annotations to check + * @return True if delete button should be hidden, false otherwise + */ + private func shouldHideDeleteButton(for annotations: [Annotation]) -> Bool { + // Check each annotation's custom data + for annotation in annotations { + if let customData = annotation.customData, + let hideDelete = customData["hideDelete"] { + // Check if hideDelete is set to true (as string or boolean) + if (hideDelete as? String) == "true" || (hideDelete as? Bool) == true { + print("*** Hiding delete button for annotation with hideDelete custom data") + return true + } + } + } + + return false + } + + private func filterActionsFromMenu(_ menu: UIMenu, shouldHideDelete: Bool = true) -> UIMenu { + var filteredChildren: [UIMenuElement] = [] + + for element in menu.children { + if let action = element as? UIAction { + print("Found action - identifier: '\(action.identifier.rawValue)', title: '\(action.title)'") + + // Only filter delete actions if shouldHideDelete is true + let isDeleteAction = action.identifier.rawValue == "com.pspdfkit.action.delete" || + action.identifier.rawValue.hasSuffix(".delete") == true || + action.identifier.rawValue.lowercased().contains("delete") == true || + action.title.lowercased().contains("delete") + + let shouldFilter = shouldHideDelete && isDeleteAction + + if !shouldFilter { + filteredChildren.append(element) + print("Kept action: \(action.identifier.rawValue)") + } else { + print("*** FILTERED OUT delete action: '\(action.identifier.rawValue)' - '\(action.title)'") + } + } else if let submenu = element as? UIMenu { + // Recursively filter submenus + let filteredSubmenu = filterActionsFromMenu(submenu, shouldHideDelete: shouldHideDelete) + if !filteredSubmenu.children.isEmpty { + filteredChildren.append(filteredSubmenu) + } + } else { + // Keep other elements (like UICommand) + filteredChildren.append(element) + } + } + + return menu.replacingChildren(filteredChildren) + } func setFormFieldValue(value: String, fullyQualifiedName: String, completion: @escaping (Result) -> Void) { do { From 251037e051a92ae189f09be8474e6d35d463c587 Mon Sep 17 00:00:00 2001 From: Julius Kato Mutumba Date: Wed, 27 Aug 2025 23:30:36 +0300 Subject: [PATCH 2/4] disable all edit functionality --- .../flutter/pspdfkit/FlutterPdfUiFragment.kt | 46 ++++++++--- example/lib/examples.dart | 4 +- .../lib/hide_delete_annotation_example.dart | 54 ++++++++---- ios/Classes/PspdfkitPlatformViewImpl.swift | 82 +++++++++++++++++-- 4 files changed, 153 insertions(+), 33 deletions(-) diff --git a/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt b/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt index 12300fea..0c0b5fd1 100644 --- a/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt +++ b/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt @@ -33,7 +33,7 @@ import com.pspdfkit.ui.PdfUiFragment import com.pspdfkit.ui.toolbar.ContextualToolbar import com.pspdfkit.ui.toolbar.ToolbarCoordinatorLayout import com.pspdfkit.annotations.Annotation -import com.pspdfkit.R + class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLayout.OnContextualToolbarLifecycleListener { @@ -313,9 +313,17 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa } /** - * Helper method to determine if the delete button should be hidden based on annotation custom data. + * Helper method to determine if annotation modification actions should be hidden based on annotation custom data. + * + * When annotations have 'hideDelete': true in their customData, this will hide all modification options + * including delete, edit, copy, cut, style picker, inspector, etc. + * + * References: + * - Android Contextual Toolbars: https://www.nutrient.io/guides/android/customizing-the-interface/customizing-the-toolbar/ + * - Annotation Custom Data: https://www.nutrient.io/guides/android/annotations/annotation-json/ + * - ContextualToolbar API: https://www.nutrient.io/api/android/kdoc/pspdfkit/com.pspdfkit.ui.toolbar/-contextual-toolbar/ * - * @return True if delete button should be hidden, false otherwise + * @return True if modification actions should be hidden, false otherwise */ private fun shouldHideDeleteButton(): Boolean { try { @@ -344,10 +352,18 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa val shouldHideDelete = shouldHideDeleteButton() if (shouldHideDelete) { - toolbar.setMenuItemVisibility( - R.id.pspdf__annotation_editing_toolbar_item_delete, - View.GONE - ) + // Hide all modification actions for protected annotations + // Reference: Complete list of annotation editing menu item IDs can be found in Android resources + // See: https://www.nutrient.io/guides/android/customizing-the-interface/customizing-the-toolbar/ + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_delete, View.GONE) // Delete annotation + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_cut, View.GONE) // Cut annotation to clipboard + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_copy, View.GONE) // Copy annotation to clipboard + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_edit, View.GONE) // Edit annotation content + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_picker, View.GONE) // Style/appearance picker + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_annotation_note, View.GONE) // Edit annotation notes + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_inspector, View.GONE) // Properties inspector + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_group, View.GONE) // Group annotations + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_ungroup, View.GONE) // Ungroup annotations } } @@ -356,10 +372,18 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa val shouldHideDelete = shouldHideDeleteButton() if (shouldHideDelete) { - toolbar.setMenuItemVisibility( - R.id.pspdf__annotation_editing_toolbar_item_delete, - View.GONE - ) + // Hide all modification actions for protected annotations + // Reference: Complete list of annotation editing menu item IDs can be found in Android resources + // See: https://www.nutrient.io/guides/android/customizing-the-interface/customizing-the-toolbar/ + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_delete, View.GONE) // Delete annotation + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_cut, View.GONE) // Cut annotation to clipboard + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_copy, View.GONE) // Copy annotation to clipboard + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_edit, View.GONE) // Edit annotation content + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_picker, View.GONE) // Style/appearance picker + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_annotation_note, View.GONE) // Edit annotation notes + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_inspector, View.GONE) // Properties inspector + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_group, View.GONE) // Group annotations + toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_ungroup, View.GONE) // Ungroup annotations } } diff --git a/example/lib/examples.dart b/example/lib/examples.dart index 6b474d7c..74612b76 100644 --- a/example/lib/examples.dart +++ b/example/lib/examples.dart @@ -112,9 +112,9 @@ List examples(BuildContext context) => [ onTap: () => annotationsExample(context), ), NutrientExampleItem( - title: 'Hide Delete Button on Annotations', + title: 'Hide Annotation Modification Options', description: - 'Demonstrates how to use custom data to conditionally hide delete buttons on annotations.', + 'Demonstrates how to use custom data to conditionally hide all modification options (delete, edit, copy, cut, style picker, etc.) on annotations.', onTap: () => hideDeleteAnnotationExample(context), ), NutrientExampleItem( diff --git a/example/lib/hide_delete_annotation_example.dart b/example/lib/hide_delete_annotation_example.dart index 8a25dbb4..485d8a7c 100644 --- a/example/lib/hide_delete_annotation_example.dart +++ b/example/lib/hide_delete_annotation_example.dart @@ -15,7 +15,16 @@ import 'utils/platform_utils.dart'; import 'widgets/pdf_viewer_scaffold.dart'; /// Example demonstrating how to use the hideDelete custom data to conditionally -/// hide delete buttons on annotations in the PDF viewer. +/// hide all annotation modification options in the PDF viewer. +/// +/// This example shows how to create "protected" annotations that cannot be modified +/// by users. When annotations have 'hideDelete': true in their customData, all +/// modification actions are hidden from contextual menus. +/// +/// References: +/// - Annotation Custom Data: https://www.nutrient.io/guides/flutter/annotations/annotation-json/ +/// - Android Menu Customization: https://www.nutrient.io/guides/android/customizing-the-interface/customizing-the-toolbar/ +/// - iOS Menu Customization: https://www.nutrient.io/guides/ios/customizing-the-interface/customizing-menus/ class HideDeleteAnnotationExampleWidget extends StatefulWidget { final String documentPath; final PdfConfiguration? configuration; @@ -41,7 +50,7 @@ class _HideDeleteAnnotationExampleWidgetState if (PlatformUtils.isCurrentPlatformSupported()) { return Scaffold( appBar: AppBar( - title: const Text('Hide Delete Button Example'), + title: const Text('Hide Annotation Modification Example'), ), body: Column( children: [ @@ -95,7 +104,7 @@ class _HideDeleteAnnotationExampleWidgetState ); } else { return Scaffold( - appBar: AppBar(title: const Text('Hide Delete Button Example')), + appBar: AppBar(title: const Text('Hide Annotation Modification Example')), body: Center( child: Text( '$defaultTargetPlatform is not yet supported by PSPDFKit for Flutter.', @@ -106,7 +115,13 @@ class _HideDeleteAnnotationExampleWidgetState } /// Adds annotations with hideDelete: true custom data - /// These annotations will not show a delete button in their contextual menu + /// These annotations will not show any modification options (delete, edit, copy, cut, style picker, etc.) in their contextual menu + /// + /// The hideDelete property can be set as: + /// - String: 'hideDelete': 'true' + /// - Boolean: 'hideDelete': true + /// + /// Reference: https://www.nutrient.io/guides/flutter/annotations/annotation-json/ Future _addProtectedAnnotations() async { final protectedAnnotations = [ // Protected highlight annotation - delete button will be hidden @@ -123,9 +138,9 @@ class _HideDeleteAnnotationExampleWidgetState pageIndex: 0, creatorName: 'System', customData: { - 'hideDelete': 'true', // This will hide the delete button - 'protected': 'true', - 'reason': 'System generated annotation', + 'hideDelete': 'true', // This will hide all modification options (delete, edit, copy, cut, style picker, etc.) + 'protected': 'true', // Additional custom property for application logic + 'reason': 'System generated annotation', // Additional metadata }, ), @@ -143,9 +158,9 @@ class _HideDeleteAnnotationExampleWidgetState pageIndex: 0, creatorName: 'Administrator', customData: { - 'hideDelete': true, // Boolean value also works - 'protected': true, - 'adminGenerated': true, + 'hideDelete': true, // Boolean value also works - hides all modification options + 'protected': true, // Additional custom property for application logic + 'adminGenerated': true, // Additional metadata }, ), @@ -181,7 +196,7 @@ class _HideDeleteAnnotationExampleWidgetState ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( - 'Protected annotations added! Try to select them - no delete button will appear.'), + 'Protected annotations added! Try to select them - no modification options will appear.'), backgroundColor: Colors.orange, ), ); @@ -189,7 +204,16 @@ class _HideDeleteAnnotationExampleWidgetState } /// Adds normal annotations without hideDelete custom data - /// These annotations will show the delete button normally + /// These annotations will show all modification options normally in their contextual menu + /// + /// When hideDelete is not present or set to false, users can: + /// - Delete annotations + /// - Edit annotation content and properties + /// - Copy/cut annotations + /// - Access style picker and inspector + /// - Group/ungroup annotations + /// + /// Reference: https://www.nutrient.io/guides/flutter/annotations/annotation-json/ Future _addEditableAnnotations() async { final editableAnnotations = [ // Normal highlight annotation - delete button will be visible @@ -208,7 +232,7 @@ class _HideDeleteAnnotationExampleWidgetState customData: { 'editable': true, 'userGenerated': true, - // Note: no hideDelete property, so delete button will be visible + // Note: no hideDelete property, so all modification options will be visible }, ), @@ -228,7 +252,7 @@ class _HideDeleteAnnotationExampleWidgetState customData: { 'editable': true, 'userGenerated': true, - // hideDelete is not set, so delete button will appear + // hideDelete is not set, so all modification options will appear }, ), ]; @@ -239,7 +263,7 @@ class _HideDeleteAnnotationExampleWidgetState ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( - 'Editable annotations added! These will show the delete button.'), + 'Editable annotations added! These will show all modification options.'), backgroundColor: Colors.green, ), ); diff --git a/ios/Classes/PspdfkitPlatformViewImpl.swift b/ios/Classes/PspdfkitPlatformViewImpl.swift index 767825df..90b10d72 100644 --- a/ios/Classes/PspdfkitPlatformViewImpl.swift +++ b/ios/Classes/PspdfkitPlatformViewImpl.swift @@ -117,6 +117,18 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV // MARK: - Menu Filtering Delegate Methods + /** + * Customizes the annotation context menu by filtering out modification actions for protected annotations. + * + * This delegate method is called when the user selects annotations and a context menu is displayed. + * For annotations with 'hideDelete': true in customData, all modification actions are filtered out. + * + * References: + * - iOS Menu Customization: https://www.nutrient.io/guides/ios/customizing-the-interface/customizing-menus/ + * - PDFViewController Delegate: https://www.nutrient.io/api/ios/documentation/pspdfkit/pdfviewcontrollerdelegate/ + * - UIMenu API: https://developer.apple.com/documentation/uikit/uimenu + * - Annotation Custom Data: https://www.nutrient.io/guides/ios/annotations/annotation-json/ + */ public func pdfViewController(_ pdfController: PDFViewController, menuForAnnotations annotations: [Annotation], onPageView pageView: PDFPageView, @@ -145,10 +157,17 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV // MARK: - Menu Filtering Helper /** - * Helper method to determine if the delete button should be hidden based on annotation custom data. + * Helper method to determine if annotation modification actions should be hidden based on custom data. + * + * Checks if any of the selected annotations have 'hideDelete' set to true in their customData. + * When true, all modification actions (delete, edit, copy, cut, style, etc.) will be hidden. + * + * References: + * - Annotation Custom Data: https://www.nutrient.io/guides/ios/annotations/annotation-json/ + * - Annotation Class: https://www.nutrient.io/api/ios/documentation/pspdfkit/annotation/ * * @param annotations The annotations to check - * @return True if delete button should be hidden, false otherwise + * @return True if modification actions should be hidden, false otherwise */ private func shouldHideDeleteButton(for annotations: [Annotation]) -> Bool { // Check each annotation's custom data @@ -166,6 +185,21 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV return false } + /** + * Recursively filters UIMenu actions to remove modification-related actions when shouldHideDelete is true. + * + * This method identifies actions by checking both their identifier.rawValue and title for keywords + * related to modification operations (delete, copy, cut, edit, style, inspector, note, group). + * + * References: + * - UIMenu API: https://developer.apple.com/documentation/uikit/uimenu + * - UIAction API: https://developer.apple.com/documentation/uikit/uiaction + * - PSPDFKit Action Identifiers: Actions typically follow "com.pspdfkit.action.{actionName}" pattern + * + * @param menu The menu to filter + * @param shouldHideDelete Whether to hide modification actions + * @return A new UIMenu with filtered actions + */ private func filterActionsFromMenu(_ menu: UIMenu, shouldHideDelete: Bool = true) -> UIMenu { var filteredChildren: [UIMenuElement] = [] @@ -173,19 +207,57 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV if let action = element as? UIAction { print("Found action - identifier: '\(action.identifier.rawValue)', title: '\(action.title)'") - // Only filter delete actions if shouldHideDelete is true + // Filter all modification actions if shouldHideDelete is true let isDeleteAction = action.identifier.rawValue == "com.pspdfkit.action.delete" || action.identifier.rawValue.hasSuffix(".delete") == true || action.identifier.rawValue.lowercased().contains("delete") == true || action.title.lowercased().contains("delete") - let shouldFilter = shouldHideDelete && isDeleteAction + let isInspectorAction = action.identifier.rawValue == "com.pspdfkit.action.inspector" || + action.identifier.rawValue.hasSuffix(".inspector") == true || + action.identifier.rawValue.lowercased().contains("inspector") == true || + action.title.lowercased().contains("inspector") + + let isCopyAction = action.identifier.rawValue == "com.pspdfkit.action.copy" || + action.identifier.rawValue.hasSuffix(".copy") == true || + action.identifier.rawValue.lowercased().contains("copy") == true || + action.title.lowercased().contains("copy") + + let isCutAction = action.identifier.rawValue == "com.pspdfkit.action.cut" || + action.identifier.rawValue.hasSuffix(".cut") == true || + action.identifier.rawValue.lowercased().contains("cut") == true || + action.title.lowercased().contains("cut") + + let isEditAction = action.identifier.rawValue == "com.pspdfkit.action.edit" || + action.identifier.rawValue.hasSuffix(".edit") == true || + action.identifier.rawValue.lowercased().contains("edit") == true || + action.title.lowercased().contains("edit") + + let isStyleAction = action.identifier.rawValue == "com.pspdfkit.action.style" || + action.identifier.rawValue.hasSuffix(".style") == true || + action.identifier.rawValue.lowercased().contains("style") == true || + action.identifier.rawValue.lowercased().contains("picker") == true || + action.title.lowercased().contains("style") || + action.title.lowercased().contains("picker") + + let isNoteAction = action.identifier.rawValue == "com.pspdfkit.action.note" || + action.identifier.rawValue.hasSuffix(".note") == true || + action.identifier.rawValue.lowercased().contains("note") == true || + action.title.lowercased().contains("note") + + let isGroupAction = action.identifier.rawValue == "com.pspdfkit.action.group" || + action.identifier.rawValue.hasSuffix(".group") == true || + action.identifier.rawValue.lowercased().contains("group") == true || + action.title.lowercased().contains("group") + + let shouldFilter = shouldHideDelete && (isDeleteAction || isInspectorAction || isCopyAction || + isCutAction || isEditAction || isStyleAction || isNoteAction || isGroupAction) if !shouldFilter { filteredChildren.append(element) print("Kept action: \(action.identifier.rawValue)") } else { - print("*** FILTERED OUT delete action: '\(action.identifier.rawValue)' - '\(action.title)'") + print("*** FILTERED OUT modification action: '\(action.identifier.rawValue)' - '\(action.title)'") } } else if let submenu = element as? UIMenu { // Recursively filter submenus From ab3f7964b516e3b846cf0e38d3f94730f645b249 Mon Sep 17 00:00:00 2001 From: Julius Kato Mutumba Date: Fri, 29 Aug 2025 23:07:19 +0300 Subject: [PATCH 3/4] disable contextual menu --- .../flutter/pspdfkit/FlutterPdfUiFragment.kt | 63 ++++--- example/lib/examples.dart | 4 +- .../lib/hide_delete_annotation_example.dart | 52 +++--- ios/Classes/PspdfkitPlatformViewImpl.swift | 154 ++++++------------ 4 files changed, 115 insertions(+), 158 deletions(-) diff --git a/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt b/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt index 0c0b5fd1..fbc976d6 100644 --- a/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt +++ b/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt @@ -313,19 +313,19 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa } /** - * Helper method to determine if annotation modification actions should be hidden based on annotation custom data. + * Helper method to determine if annotation editing should be disabled based on annotation custom data. * - * When annotations have 'hideDelete': true in their customData, this will hide all modification options - * including delete, edit, copy, cut, style picker, inspector, etc. + * When annotations have 'hideDelete': true in their customData, this will disable the entire + * contextual toolbar and annotation resizing, but still allow moving the annotation. * * References: * - Android Contextual Toolbars: https://www.nutrient.io/guides/android/customizing-the-interface/customizing-the-toolbar/ * - Annotation Custom Data: https://www.nutrient.io/guides/android/annotations/annotation-json/ * - ContextualToolbar API: https://www.nutrient.io/api/android/kdoc/pspdfkit/com.pspdfkit.ui.toolbar/-contextual-toolbar/ * - * @return True if modification actions should be hidden, false otherwise + * @return True if editing should be disabled (contextual toolbar hidden, resizing disabled), false otherwise */ - private fun shouldHideDeleteButton(): Boolean { + private fun shouldDisableEditing(): Boolean { try { if (document != null) { Log.d("FlutterPdfUiFragment", "Checking annotations for hideDelete custom data") @@ -335,8 +335,12 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa for (annotation in annotations) { val customData = annotation.customData if (customData != null && customData.has("hideDelete")) { - Log.d("FlutterPdfUiFragment", "Annotation ${annotation.type} has hideDelete custom data") - return true + val hideDeleteValue = customData.get("hideDelete") + val shouldHide = hideDeleteValue == "true" || hideDeleteValue == true + if (shouldHide) { + Log.d("FlutterPdfUiFragment", "Annotation ${annotation.type} has hideDelete=true, disabling editing") + return true + } } } } @@ -349,41 +353,30 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa override fun onPrepareContextualToolbar(toolbar: ContextualToolbar<*>) { currentContextualToolbar = toolbar - val shouldHideDelete = shouldHideDeleteButton() + val shouldDisableEditing = shouldDisableEditing() - if (shouldHideDelete) { - // Hide all modification actions for protected annotations - // Reference: Complete list of annotation editing menu item IDs can be found in Android resources - // See: https://www.nutrient.io/guides/android/customizing-the-interface/customizing-the-toolbar/ - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_delete, View.GONE) // Delete annotation - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_cut, View.GONE) // Cut annotation to clipboard - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_copy, View.GONE) // Copy annotation to clipboard - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_edit, View.GONE) // Edit annotation content - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_picker, View.GONE) // Style/appearance picker - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_annotation_note, View.GONE) // Edit annotation notes - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_inspector, View.GONE) // Properties inspector - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_group, View.GONE) // Group annotations - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_ungroup, View.GONE) // Ungroup annotations + if (shouldDisableEditing) { + // Completely hide the contextual toolbar for protected annotations + // This removes access to resize handles and all editing actions + Log.d("FlutterPdfUiFragment", "Hiding contextual toolbar for protected annotation") + toolbar.visibility = View.GONE + + // This is the most direct approach available in PSPDFKit Android + // to prevent users from accessing resize handles for protected annotations } } override fun onDisplayContextualToolbar(toolbar: ContextualToolbar<*>) { currentContextualToolbar = toolbar - val shouldHideDelete = shouldHideDeleteButton() + val shouldDisableEditing = shouldDisableEditing() - if (shouldHideDelete) { - // Hide all modification actions for protected annotations - // Reference: Complete list of annotation editing menu item IDs can be found in Android resources - // See: https://www.nutrient.io/guides/android/customizing-the-interface/customizing-the-toolbar/ - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_delete, View.GONE) // Delete annotation - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_cut, View.GONE) // Cut annotation to clipboard - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_copy, View.GONE) // Copy annotation to clipboard - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_edit, View.GONE) // Edit annotation content - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_picker, View.GONE) // Style/appearance picker - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_annotation_note, View.GONE) // Edit annotation notes - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_inspector, View.GONE) // Properties inspector - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_group, View.GONE) // Group annotations - toolbar.setMenuItemVisibility(R.id.pspdf__annotation_editing_toolbar_item_ungroup, View.GONE) // Ungroup annotations + if (shouldDisableEditing) { + // Completely hide the contextual toolbar for protected annotations + // This removes access to resize handles and all editing actions + Log.d("FlutterPdfUiFragment", "Hiding contextual toolbar for protected annotation") + toolbar.visibility = View.GONE + + // This effectively prevents access to resize handles and editing controls } } diff --git a/example/lib/examples.dart b/example/lib/examples.dart index 74612b76..6da54e8f 100644 --- a/example/lib/examples.dart +++ b/example/lib/examples.dart @@ -112,9 +112,9 @@ List examples(BuildContext context) => [ onTap: () => annotationsExample(context), ), NutrientExampleItem( - title: 'Hide Annotation Modification Options', + title: 'Protected Annotations (Move Only)', description: - 'Demonstrates how to use custom data to conditionally hide all modification options (delete, edit, copy, cut, style picker, etc.) on annotations.', + 'Demonstrates how to create protected annotations that can be moved but not edited or resized. Uses custom data to disable contextual menus while preserving movement functionality.', onTap: () => hideDeleteAnnotationExample(context), ), NutrientExampleItem( diff --git a/example/lib/hide_delete_annotation_example.dart b/example/lib/hide_delete_annotation_example.dart index 485d8a7c..0fb85c21 100644 --- a/example/lib/hide_delete_annotation_example.dart +++ b/example/lib/hide_delete_annotation_example.dart @@ -14,12 +14,15 @@ import 'package:nutrient_flutter/nutrient_flutter.dart'; import 'utils/platform_utils.dart'; import 'widgets/pdf_viewer_scaffold.dart'; -/// Example demonstrating how to use the hideDelete custom data to conditionally -/// hide all annotation modification options in the PDF viewer. +/// Example demonstrating how to use the hideDelete custom data to disable +/// annotation editing while still allowing movement. /// -/// This example shows how to create "protected" annotations that cannot be modified -/// by users. When annotations have 'hideDelete': true in their customData, all -/// modification actions are hidden from contextual menus. +/// This example shows how to create "protected" annotations that can be moved but not +/// edited or resized. When annotations have 'hideDelete': true in their customData: +/// - The contextual toolbar/menu is completely disabled +/// - Annotation resizing is blocked +/// - Annotation movement is still allowed +/// - All modification actions (delete, edit, copy, cut, style picker, etc.) are prevented /// /// References: /// - Annotation Custom Data: https://www.nutrient.io/guides/flutter/annotations/annotation-json/ @@ -50,7 +53,7 @@ class _HideDeleteAnnotationExampleWidgetState if (PlatformUtils.isCurrentPlatformSupported()) { return Scaffold( appBar: AppBar( - title: const Text('Hide Annotation Modification Example'), + title: const Text('Protected Annotations (Move Only)'), ), body: Column( children: [ @@ -115,16 +118,23 @@ class _HideDeleteAnnotationExampleWidgetState } /// Adds annotations with hideDelete: true custom data - /// These annotations will not show any modification options (delete, edit, copy, cut, style picker, etc.) in their contextual menu + /// These annotations will have their contextual toolbar/menu completely disabled and cannot be resized, + /// but can still be moved around the page. + /// + /// Behavior for protected annotations: + /// - No contextual toolbar/menu appears when selected + /// - Cannot be resized (handles are disabled) + /// - Can still be moved by dragging + /// - Cannot be deleted, edited, copied, cut, styled, etc. /// /// The hideDelete property can be set as: - /// - String: 'hideDelete': 'true' + /// - String: 'hideDelete': 'true' /// - Boolean: 'hideDelete': true /// /// Reference: https://www.nutrient.io/guides/flutter/annotations/annotation-json/ Future _addProtectedAnnotations() async { final protectedAnnotations = [ - // Protected highlight annotation - delete button will be hidden + // Protected highlight annotation - contextual menu disabled, resizing blocked, movement allowed HighlightAnnotation( id: 'protected-highlight-1', name: 'Protected Highlight', @@ -138,13 +148,13 @@ class _HideDeleteAnnotationExampleWidgetState pageIndex: 0, creatorName: 'System', customData: { - 'hideDelete': 'true', // This will hide all modification options (delete, edit, copy, cut, style picker, etc.) + 'hideDelete': 'true', // This will disable contextual menu and resizing, but allow movement 'protected': 'true', // Additional custom property for application logic 'reason': 'System generated annotation', // Additional metadata }, ), - // Protected note annotation - delete button will be hidden + // Protected note annotation - contextual menu disabled, resizing blocked, movement allowed NoteAnnotation( id: 'protected-note-1', name: 'Protected Note', @@ -158,13 +168,13 @@ class _HideDeleteAnnotationExampleWidgetState pageIndex: 0, creatorName: 'Administrator', customData: { - 'hideDelete': true, // Boolean value also works - hides all modification options + 'hideDelete': true, // Boolean value also works - disables contextual menu and resizing 'protected': true, // Additional custom property for application logic 'adminGenerated': true, // Additional metadata }, ), - // Protected free text annotation - delete button will be hidden + // Protected free text annotation - contextual menu disabled, resizing blocked, movement allowed FreeTextAnnotation( id: 'protected-freetext-1', name: 'Protected Free Text', @@ -188,6 +198,7 @@ class _HideDeleteAnnotationExampleWidgetState 'importance': 'high', }, ), + ]; await document?.addAnnotations(protectedAnnotations); @@ -196,7 +207,7 @@ class _HideDeleteAnnotationExampleWidgetState ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( - 'Protected annotations added! Try to select them - no modification options will appear.'), + 'Protected annotations added! Try selecting them - no contextual menu will appear, but you can still move them.'), backgroundColor: Colors.orange, ), ); @@ -204,7 +215,7 @@ class _HideDeleteAnnotationExampleWidgetState } /// Adds normal annotations without hideDelete custom data - /// These annotations will show all modification options normally in their contextual menu + /// These annotations will show the full contextual toolbar/menu with all modification options /// /// When hideDelete is not present or set to false, users can: /// - Delete annotations @@ -212,11 +223,13 @@ class _HideDeleteAnnotationExampleWidgetState /// - Copy/cut annotations /// - Access style picker and inspector /// - Group/ungroup annotations + /// - Resize annotations using handles + /// - Move annotations by dragging /// /// Reference: https://www.nutrient.io/guides/flutter/annotations/annotation-json/ Future _addEditableAnnotations() async { final editableAnnotations = [ - // Normal highlight annotation - delete button will be visible + // Normal highlight annotation - full contextual menu available HighlightAnnotation( id: 'editable-highlight-1', name: 'Editable Highlight', @@ -236,7 +249,7 @@ class _HideDeleteAnnotationExampleWidgetState }, ), - // Normal note annotation - delete button will be visible + // Normal note annotation - full contextual menu available NoteAnnotation( id: 'editable-note-1', name: 'Editable Note', @@ -252,9 +265,10 @@ class _HideDeleteAnnotationExampleWidgetState customData: { 'editable': true, 'userGenerated': true, - // hideDelete is not set, so all modification options will appear + // hideDelete is not set, so full contextual menu will appear }, ), + ]; await document?.addAnnotations(editableAnnotations); @@ -263,7 +277,7 @@ class _HideDeleteAnnotationExampleWidgetState ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( - 'Editable annotations added! These will show all modification options.'), + 'Editable annotations added! These will show the full contextual menu with all editing options.'), backgroundColor: Colors.green, ), ); diff --git a/ios/Classes/PspdfkitPlatformViewImpl.swift b/ios/Classes/PspdfkitPlatformViewImpl.swift index 90b10d72..96974dab 100644 --- a/ios/Classes/PspdfkitPlatformViewImpl.swift +++ b/ios/Classes/PspdfkitPlatformViewImpl.swift @@ -25,6 +25,7 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV self.pdfViewController = controller self.pdfViewController?.delegate = self + // Set the host view for the annotation toolbar controller controller.annotationToolbarController?.updateHostView(nil, container: nil, viewController: controller) CustomToolbarHelper.setupCustomToolbarItems(for: pdfViewController!, customToolbarItems:customToolbarItems, callbacks: customToolbarCallbacks) @@ -44,6 +45,7 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV public func pdfViewController(_ pdfController: PDFViewController, didSelect annotations: [Annotation], on pageView: PDFPageView) { // Call the event helper to notify the listeners. eventsHelper?.annotationSelected(annotations: annotations) + } public func pdfViewController(_ pdfController: PDFViewController, didDeselect annotations: [Annotation], on pageView: PDFPageView) { @@ -118,10 +120,11 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV // MARK: - Menu Filtering Delegate Methods /** - * Customizes the annotation context menu by filtering out modification actions for protected annotations. + * Customizes the annotation context menu by completely disabling it for protected annotations. * * This delegate method is called when the user selects annotations and a context menu is displayed. - * For annotations with 'hideDelete': true in customData, all modification actions are filtered out. + * For annotations with 'hideDelete': true in customData, the entire context menu is disabled + * to prevent all editing actions while still allowing annotation movement. * * References: * - iOS Menu Customization: https://www.nutrient.io/guides/ios/customizing-the-interface/customizing-menus/ @@ -136,47 +139,44 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV suggestedMenu: UIMenu) -> UIMenu { print("*** menuForAnnotations delegate method called! Annotations count: \(annotations.count)") - if suggestedMenu.children.isEmpty { - print("*** No suggested menu or empty menu, returning original") - return suggestedMenu - } - - print("*** Original menu has \(suggestedMenu.children.count) children") - - // Check if delete should be hidden based on annotation custom data - let shouldHideDelete = shouldHideDeleteButton(for: annotations) + // Check if editing should be disabled based on annotation custom data + let shouldDisableEditing = shouldDisableEditing(for: annotations) - // Filter out delete actions conditionally based on custom data - let filteredMenu = filterActionsFromMenu(suggestedMenu, shouldHideDelete: shouldHideDelete) - - print("*** Final menu has \(filteredMenu.children.count) children") + if shouldDisableEditing { + print("*** Disabling context menu entirely for protected annotations") + // Return an empty menu to completely disable the context menu + // This allows annotations to remain selectable and movable but prevents all editing actions + return UIMenu(title: "", children: []) + } - return filteredMenu + print("*** Returning original menu with \(suggestedMenu.children.count) children") + return suggestedMenu } // MARK: - Menu Filtering Helper /** - * Helper method to determine if annotation modification actions should be hidden based on custom data. + * Helper method to determine if annotation editing should be completely disabled based on custom data. * * Checks if any of the selected annotations have 'hideDelete' set to true in their customData. - * When true, all modification actions (delete, edit, copy, cut, style, etc.) will be hidden. + * When true, the entire context menu will be disabled to prevent all editing actions + * while still allowing annotation selection and movement. * * References: * - Annotation Custom Data: https://www.nutrient.io/guides/ios/annotations/annotation-json/ * - Annotation Class: https://www.nutrient.io/api/ios/documentation/pspdfkit/annotation/ * * @param annotations The annotations to check - * @return True if modification actions should be hidden, false otherwise + * @return True if editing should be disabled (context menu hidden), false otherwise */ - private func shouldHideDeleteButton(for annotations: [Annotation]) -> Bool { + private func shouldDisableEditing(for annotations: [Annotation]) -> Bool { // Check each annotation's custom data for annotation in annotations { if let customData = annotation.customData, let hideDelete = customData["hideDelete"] { // Check if hideDelete is set to true (as string or boolean) if (hideDelete as? String) == "true" || (hideDelete as? Bool) == true { - print("*** Hiding delete button for annotation with hideDelete custom data") + print("*** Disabling editing for annotation with hideDelete custom data") return true } } @@ -185,94 +185,44 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV return false } + // MARK: - Annotation Interaction Control + // + // Note: The previous filterActionsFromMenu method has been removed since we now + // completely disable the context menu for protected annotations rather than + // filtering individual actions. This provides a cleaner user experience. + + /** - * Recursively filters UIMenu actions to remove modification-related actions when shouldHideDelete is true. - * - * This method identifies actions by checking both their identifier.rawValue and title for keywords - * related to modification operations (delete, copy, cut, edit, style, inspector, note, group). + * Helper method to check if an individual annotation is protected. * - * References: - * - UIMenu API: https://developer.apple.com/documentation/uikit/uimenu - * - UIAction API: https://developer.apple.com/documentation/uikit/uiaction - * - PSPDFKit Action Identifiers: Actions typically follow "com.pspdfkit.action.{actionName}" pattern + * @param annotation The annotation to check + * @return True if the annotation has hideDelete set to true, false otherwise + */ + private func isAnnotationProtected(_ annotation: Annotation) -> Bool { + if let customData = annotation.customData, + let hideDelete = customData["hideDelete"] { + return (hideDelete as? String) == "true" || (hideDelete as? Bool) == true + } + return false + } + + + /** + * PDFViewController delegate method to control annotation removal. * - * @param menu The menu to filter - * @param shouldHideDelete Whether to hide modification actions - * @return A new UIMenu with filtered actions + * Blocks removal of protected annotations. */ - private func filterActionsFromMenu(_ menu: UIMenu, shouldHideDelete: Bool = true) -> UIMenu { - var filteredChildren: [UIMenuElement] = [] - - for element in menu.children { - if let action = element as? UIAction { - print("Found action - identifier: '\(action.identifier.rawValue)', title: '\(action.title)'") - - // Filter all modification actions if shouldHideDelete is true - let isDeleteAction = action.identifier.rawValue == "com.pspdfkit.action.delete" || - action.identifier.rawValue.hasSuffix(".delete") == true || - action.identifier.rawValue.lowercased().contains("delete") == true || - action.title.lowercased().contains("delete") - - let isInspectorAction = action.identifier.rawValue == "com.pspdfkit.action.inspector" || - action.identifier.rawValue.hasSuffix(".inspector") == true || - action.identifier.rawValue.lowercased().contains("inspector") == true || - action.title.lowercased().contains("inspector") - - let isCopyAction = action.identifier.rawValue == "com.pspdfkit.action.copy" || - action.identifier.rawValue.hasSuffix(".copy") == true || - action.identifier.rawValue.lowercased().contains("copy") == true || - action.title.lowercased().contains("copy") - - let isCutAction = action.identifier.rawValue == "com.pspdfkit.action.cut" || - action.identifier.rawValue.hasSuffix(".cut") == true || - action.identifier.rawValue.lowercased().contains("cut") == true || - action.title.lowercased().contains("cut") - - let isEditAction = action.identifier.rawValue == "com.pspdfkit.action.edit" || - action.identifier.rawValue.hasSuffix(".edit") == true || - action.identifier.rawValue.lowercased().contains("edit") == true || - action.title.lowercased().contains("edit") - - let isStyleAction = action.identifier.rawValue == "com.pspdfkit.action.style" || - action.identifier.rawValue.hasSuffix(".style") == true || - action.identifier.rawValue.lowercased().contains("style") == true || - action.identifier.rawValue.lowercased().contains("picker") == true || - action.title.lowercased().contains("style") || - action.title.lowercased().contains("picker") - - let isNoteAction = action.identifier.rawValue == "com.pspdfkit.action.note" || - action.identifier.rawValue.hasSuffix(".note") == true || - action.identifier.rawValue.lowercased().contains("note") == true || - action.title.lowercased().contains("note") - - let isGroupAction = action.identifier.rawValue == "com.pspdfkit.action.group" || - action.identifier.rawValue.hasSuffix(".group") == true || - action.identifier.rawValue.lowercased().contains("group") == true || - action.title.lowercased().contains("group") - - let shouldFilter = shouldHideDelete && (isDeleteAction || isInspectorAction || isCopyAction || - isCutAction || isEditAction || isStyleAction || isNoteAction || isGroupAction) - - if !shouldFilter { - filteredChildren.append(element) - print("Kept action: \(action.identifier.rawValue)") - } else { - print("*** FILTERED OUT modification action: '\(action.identifier.rawValue)' - '\(action.title)'") - } - } else if let submenu = element as? UIMenu { - // Recursively filter submenus - let filteredSubmenu = filterActionsFromMenu(submenu, shouldHideDelete: shouldHideDelete) - if !filteredSubmenu.children.isEmpty { - filteredChildren.append(filteredSubmenu) - } - } else { - // Keep other elements (like UICommand) - filteredChildren.append(element) + public func pdfViewController(_ pdfController: PDFViewController, shouldDelete annotations: [Annotation]) -> Bool { + for annotation in annotations { + if isAnnotationProtected(annotation) { + print("*** Blocking deletion for protected annotation") + return false } } - - return menu.replacingChildren(filteredChildren) + return true } + + func setFormFieldValue(value: String, fullyQualifiedName: String, completion: @escaping (Result) -> Void) { do { From 2faf4387bc7aaec2071521234aba0e9153f9cf1b Mon Sep 17 00:00:00 2001 From: Julius Kato Mutumba Date: Tue, 9 Sep 2025 14:28:31 +0200 Subject: [PATCH 4/4] disable resizing and rotation --- .../flutter/pspdfkit/FlutterPdfUiFragment.kt | 33 +++++++++-- .../lib/hide_delete_annotation_example.dart | 38 ++++++++++++- ios/Classes/ProtectedResizableView.swift | 57 +++++++++++++++++++ ios/Classes/PspdfPlatformView.m | 6 ++ ios/Classes/PspdfkitPlatformViewImpl.swift | 2 + 5 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 ios/Classes/ProtectedResizableView.swift diff --git a/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt b/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt index fbc976d6..4a45b129 100644 --- a/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt +++ b/android/src/main/java/com/pspdfkit/flutter/pspdfkit/FlutterPdfUiFragment.kt @@ -13,6 +13,7 @@ import android.content.Context import android.graphics.drawable.Drawable import android.os.Bundle import android.util.Log +import android.view.ContextMenu import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -33,6 +34,8 @@ import com.pspdfkit.ui.PdfUiFragment import com.pspdfkit.ui.toolbar.ContextualToolbar import com.pspdfkit.ui.toolbar.ToolbarCoordinatorLayout import com.pspdfkit.annotations.Annotation +import com.pspdfkit.ui.annotations.OnAnnotationSelectedListener +import com.pspdfkit.ui.special_mode.controller.AnnotationSelectionController class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLayout.OnContextualToolbarLifecycleListener { @@ -45,6 +48,8 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa // Store current contextual toolbar for annotation access private var currentContextualToolbar: ContextualToolbar<*>? = null + private var annotationSelectionController: AnnotationSelectionController? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -69,6 +74,18 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setOnContextualToolbarLifecycleListener(this) + pdfFragment?.addOnAnnotationSelectedListener(object : OnAnnotationSelectedListener { + override fun onPrepareAnnotationSelection( + controller: AnnotationSelectionController, + annotation: Annotation, + annotationCreated: Boolean + ): Boolean { + annotationSelectionController = controller + return true + } + override fun onAnnotationSelected(annotation: Annotation, annotationCreated: Boolean) {} + override fun onAnnotationDeselected(annotation: Annotation, reselected: Boolean) {} + }) } /** @@ -354,29 +371,37 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider, ToolbarCoordinatorLa override fun onPrepareContextualToolbar(toolbar: ContextualToolbar<*>) { currentContextualToolbar = toolbar val shouldDisableEditing = shouldDisableEditing() - + annotationSelectionController?.isResizeEnabled = true if (shouldDisableEditing) { // Completely hide the contextual toolbar for protected annotations // This removes access to resize handles and all editing actions Log.d("FlutterPdfUiFragment", "Hiding contextual toolbar for protected annotation") toolbar.visibility = View.GONE - + annotationSelectionController?.isResizeEnabled = false // This is the most direct approach available in PSPDFKit Android // to prevent users from accessing resize handles for protected annotations + }else{ + // Ensure the toolbar is visible for non-protected annotations + toolbar.visibility = View.VISIBLE + annotationSelectionController?.isResizeEnabled = true } } override fun onDisplayContextualToolbar(toolbar: ContextualToolbar<*>) { currentContextualToolbar = toolbar val shouldDisableEditing = shouldDisableEditing() - + annotationSelectionController?.isResizeEnabled = true if (shouldDisableEditing) { // Completely hide the contextual toolbar for protected annotations // This removes access to resize handles and all editing actions Log.d("FlutterPdfUiFragment", "Hiding contextual toolbar for protected annotation") toolbar.visibility = View.GONE - + annotationSelectionController?.isResizeEnabled = false // This effectively prevents access to resize handles and editing controls + }else{ + // Ensure the toolbar is visible for non-protected annotations + toolbar.visibility = View.VISIBLE + annotationSelectionController?.isResizeEnabled = true } } diff --git a/example/lib/hide_delete_annotation_example.dart b/example/lib/hide_delete_annotation_example.dart index 0fb85c21..aba6e8cb 100644 --- a/example/lib/hide_delete_annotation_example.dart +++ b/example/lib/hide_delete_annotation_example.dart @@ -166,6 +166,7 @@ class _HideDeleteAnnotationExampleWidgetState ), color: const Color(0xFFFF5722), pageIndex: 0, + flags: [AnnotationFlag.lockedContents], creatorName: 'Administrator', customData: { 'hideDelete': true, // Boolean value also works - disables contextual menu and resizing @@ -192,13 +193,48 @@ class _HideDeleteAnnotationExampleWidgetState backgroundColor: const Color(0xFFFF5722), horizontalTextAlign: HorizontalTextAlignment.center, verticalAlign: VerticalAlignment.center, + flags: [AnnotationFlag.lockedContents], customData: { 'hideDelete': 'true', 'systemGenerated': true, 'importance': 'high', }, ), - + InkAnnotation( + id: 'ink-annotation-1', + bbox: [267.4, 335.1, 97.2, 10.3], + createdAt: '2025-01-06T16:36:59+03:00', + lines: InkLines( + points: [ + [ + [269.4, 343.4], + [308.4, 341.7], + [341.2, 339.6], + [358.8, 339.6], + [360.9, 339.2], + [362.6, 338.8], + [361.7, 337.1], + ] + ], + intensities: [ + [1.0, 0.43, 0.64, 0.83, 0.98, 0.99, 0.97] + ], + ), + lineWidth: 4, + opacity: 1.0, + flags: [AnnotationFlag.lockedContents], + creatorName: 'Nutrient Flutter', + name: 'Ink annotation 1', + isDrawnNaturally: false, + strokeColor: const Color(0xFFFF5722), + customData: { + "phone": "123-456-7890", + "email": "3XZ5y@example.com", + "address": "123 Main St, Anytown, USA 12345", + "hideDelete": true, + "userGenerated": true + }, + pageIndex: 0) ]; await document?.addAnnotations(protectedAnnotations); diff --git a/ios/Classes/ProtectedResizableView.swift b/ios/Classes/ProtectedResizableView.swift new file mode 100644 index 00000000..49b1a7d4 --- /dev/null +++ b/ios/Classes/ProtectedResizableView.swift @@ -0,0 +1,57 @@ +// +// Copyright © 2024-2025 PSPDFKit GmbH. All rights reserved. +// +// THIS SOURCE CODE AND ANY ACCOMPANYING DOCUMENTATION ARE PROTECTED BY INTERNATIONAL COPYRIGHT LAW +// AND MAY NOT BE RESOLD OR REDISTRIBUTED. USAGE IS BOUND TO THE PSPDFKIT LICENSE AGREEMENT. +// UNAUTHORIZED REPRODUCTION OR DISTRIBUTION IS SUBJECT TO CIVIL AND CRIMINAL PENALTIES. +// This notice may not be removed from this file. +// + +import Foundation +import PSPDFKit + +/// Custom ResizableView that can be controlled externally to disable resizing for protected annotations +/// +/// This class allows dynamic control of annotation resizing based on the `resizingEnabled` static property. +/// When `resizingEnabled` is set to false, annotations will not show resize handles and cannot be resized. +/// This is used in conjunction with annotation selection delegates to disable resizing for annotations +/// that have 'hideDelete': true in their customData. +/// +/// Usage: +/// ``` +/// // Disable resizing for protected annotations +/// ProtectedResizableView.resizingEnabled = false +/// +/// // Re-enable resizing for normal annotations +/// ProtectedResizableView.resizingEnabled = true +/// ``` +/// +/// References: +/// - PSPDFKit ResizableView: https://www.nutrient.io/api/ios/documentation/pspdfkitui/resizableview +/// - Class Override Guide: https://www.nutrient.io/guides/ios/getting-started/overriding-classes/ +@objc public class ProtectedResizableView: ResizableView{ + /// Static property to control resizing globally for all ProtectedResizableView instances + /// Set to false to disable resizing for protected annotations, true to enable resizing + static var resizingEnabled: Bool = true + + /// Override the allowResizing property to return our static control value + public override var allowResizing: Bool { + get { + return ProtectedResizableView.resizingEnabled + } + set { + // Do nothing - controlled by static property + // This prevents external code from directly setting allowResizing + } + } + + public override var allowRotating: Bool { + get { + return ProtectedResizableView.resizingEnabled + } + set { + // Do nothing - controlled by static property + // This prevents external code from directly setting allowRotating + } + } +} diff --git a/ios/Classes/PspdfPlatformView.m b/ios/Classes/PspdfPlatformView.m index 4903b871..758e489b 100644 --- a/ios/Classes/PspdfPlatformView.m +++ b/ios/Classes/PspdfPlatformView.m @@ -73,6 +73,12 @@ - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId argum BOOL isImageDocument = [PspdfkitFlutterHelper isImageDocument:documentPath]; PSPDFConfiguration *configuration = [PspdfkitFlutterConverter configuration:configurationDictionary isImageDocument:isImageDocument]; + + //Override PSPDFResizableView with ProtectedResizableView to disable text selection handles + configuration = [configuration configurationUpdatedWithBuilder:^(PSPDFConfigurationBuilder *builder) { + [builder overrideClass:PSPDFResizableView.class withClass:ProtectedResizableView.class]; + }]; + // Only update signature settings if signatureSavingStrategy is specified in the configuration if (configurationDictionary[@"signatureSavingStrategy"]) { PSPDFConfiguration *updatedConfig = [configuration configurationUpdatedWithBuilder:^(PSPDFConfigurationBuilder *builder) { diff --git a/ios/Classes/PspdfkitPlatformViewImpl.swift b/ios/Classes/PspdfkitPlatformViewImpl.swift index 96974dab..a1d8387d 100644 --- a/ios/Classes/PspdfkitPlatformViewImpl.swift +++ b/ios/Classes/PspdfkitPlatformViewImpl.swift @@ -137,6 +137,7 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV onPageView pageView: PDFPageView, appearance: EditMenuAppearance, suggestedMenu: UIMenu) -> UIMenu { + ProtectedResizableView.resizingEnabled = false print("*** menuForAnnotations delegate method called! Annotations count: \(annotations.count)") // Check if editing should be disabled based on annotation custom data @@ -146,6 +147,7 @@ public class PspdfkitPlatformViewImpl: NSObject, NutrientViewControllerApi, PDFV print("*** Disabling context menu entirely for protected annotations") // Return an empty menu to completely disable the context menu // This allows annotations to remain selectable and movable but prevents all editing actions + ProtectedResizableView.resizingEnabled = false return UIMenu(title: "", children: []) }