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..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 @@ -30,14 +31,24 @@ import androidx.lifecycle.Lifecycle import com.pspdfkit.document.PdfDocument import com.pspdfkit.flutter.pspdfkit.api.CustomToolbarCallbacks import com.pspdfkit.ui.PdfUiFragment -import com.pspdfkit.R +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 { + +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 + + private var annotationSelectionController: AnnotationSelectionController? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -60,6 +71,23 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider { return super.onCreateView(inflater, container, savedInstanceState) } + 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) {} + }) + } + /** * Called when the document is loaded. Notifies Flutter that the document has been loaded. * @@ -300,4 +328,84 @@ class FlutterPdfUiFragment : PdfUiFragment(), MenuProvider { } } } + + /** + * 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 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 editing should be disabled (contextual toolbar hidden, resizing disabled), false otherwise + */ + private fun shouldDisableEditing(): 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")) { + 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 + } + } + } + } + return false + } catch (e: Exception) { + Log.e("FlutterPdfUiFragment", "Error checking annotation custom data", e) + return false + } + } + + 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 + } + } + + 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..6da54e8f 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: 'Protected Annotations (Move Only)', + description: + '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( 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..aba6e8cb --- /dev/null +++ b/example/lib/hide_delete_annotation_example.dart @@ -0,0 +1,347 @@ +/// +/// 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 disable +/// annotation editing while still allowing movement. +/// +/// 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/ +/// - 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; + + 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('Protected Annotations (Move Only)'), + ), + 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 Annotation Modification Example')), + body: Center( + child: Text( + '$defaultTargetPlatform is not yet supported by PSPDFKit for Flutter.', + ), + ), + ); + } + } + + /// Adds annotations with hideDelete: true custom data + /// 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' + /// - Boolean: 'hideDelete': true + /// + /// Reference: https://www.nutrient.io/guides/flutter/annotations/annotation-json/ + Future _addProtectedAnnotations() async { + final protectedAnnotations = [ + // Protected highlight annotation - contextual menu disabled, resizing blocked, movement allowed + 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 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 - contextual menu disabled, resizing blocked, movement allowed + 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, + flags: [AnnotationFlag.lockedContents], + creatorName: 'Administrator', + customData: { + '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 - contextual menu disabled, resizing blocked, movement allowed + 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, + 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); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Protected annotations added! Try selecting them - no contextual menu will appear, but you can still move them.'), + backgroundColor: Colors.orange, + ), + ); + } + } + + /// Adds normal annotations without hideDelete custom data + /// 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 + /// - Edit annotation content and properties + /// - 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 - full contextual menu available + 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 all modification options will be visible + }, + ), + + // Normal note annotation - full contextual menu available + 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 full contextual menu will appear + }, + ), + + ]; + + await document?.addAnnotations(editableAnnotations); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Editable annotations added! These will show the full contextual menu with all editing options.'), + 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/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 05c170ae..a1d8387d 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) { @@ -114,6 +116,115 @@ 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 + + /** + * 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, 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/ + * - 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, + 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 + let shouldDisableEditing = shouldDisableEditing(for: annotations) + + 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 + ProtectedResizableView.resizingEnabled = false + return UIMenu(title: "", children: []) + } + + print("*** Returning original menu with \(suggestedMenu.children.count) children") + return suggestedMenu + } + + // MARK: - Menu Filtering Helper + + /** + * 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, 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 editing should be disabled (context menu hidden), false otherwise + */ + 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("*** Disabling editing for annotation with hideDelete custom data") + return true + } + } + } + + 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. + + + /** + * Helper method to check if an individual annotation is protected. + * + * @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. + * + * Blocks removal of protected annotations. + */ + 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 true + } + + func setFormFieldValue(value: String, fullyQualifiedName: String, completion: @escaping (Result) -> Void) { do {