Skip to content

Flutter main toolbar customization demo #7

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.pspdfkit.configuration.search.SearchType;
import com.pspdfkit.configuration.settings.SettingsMenuItemType;
import com.pspdfkit.configuration.sharing.ShareFeatures;
import com.pspdfkit.configuration.signatures.SignatureSavingStrategy;
import com.pspdfkit.configuration.theming.ThemeMode;
import com.pspdfkit.preferences.PSPDFKitPreferences;
import com.pspdfkit.ui.special_mode.controller.AnnotationTool;
Expand Down Expand Up @@ -858,6 +859,9 @@ PdfActivityConfiguration build() {
this.configuration
.enabledAnnotationTools(annotationTools);
}
//https://www.nutrient.io/guides/android/signatures/signature-storage/
// Configure to save the signature if it is selected. You can also configure it to always save or never save.
configuration.signatureSavingStrategy(SignatureSavingStrategy.SAVE_IF_SELECTED);
return configuration.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,154 @@

package com.pspdfkit.flutter.pspdfkit

/// Copyright © 2021-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 android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.pspdfkit.document.PdfDocument
import com.pspdfkit.flutter.pspdfkit.api.CustomToolbarCallbacks
import com.pspdfkit.signatures.storage.DatabaseSignatureStorage
import com.pspdfkit.signatures.storage.SignatureStorage
import com.pspdfkit.ui.PdfUiFragment

/**
* A custom PdfUiFragment that supports custom toolbar items.
* It extends PdfUiFragment from PSPDFKit and adds functionality for handling custom toolbar items.
*/
class FlutterPdfUiFragment : PdfUiFragment() {

// Maps identifier strings to menu item IDs to track custom toolbar items
private val customToolbarItemIds = HashMap<String, Int>()
private var customToolbarCallbacks: CustomToolbarCallbacks? = null

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Enable options menu for the fragment
setHasOptionsMenu(true)
// See guides: https://www.nutrient.io/guides/android/signatures/signature-storage/
// Set the signature storage for the PdfFragment.
val storage: SignatureStorage = DatabaseSignatureStorage
.withName(requireContext(),"MyCoolSignatureDatabase")
pdfFragment?.signatureStorage = storage
}

/**
* Called when the document is loaded. Notifies Flutter that the document has been loaded.
*
* @param document The loaded PDF document.
*/
override fun onDocumentLoaded(document: PdfDocument) {
super.onDocumentLoaded(document)
// Notify the Flutter PSPDFKit plugin that the document has been loaded.
// Notify the Flutter PSPDFKit plugin that the document has been loaded.
EventDispatcher.getInstance().notifyDocumentLoaded(document)
}

/**
* Sets the custom toolbar items to be displayed in the toolbar.
*
* @param items List of custom toolbar item configurations from Flutter.
* @param callbacks Callbacks to notify Flutter when a custom toolbar item is tapped.
*/
fun setCustomToolbarItems(items: List<Map<String, Any>>, callbacks: CustomToolbarCallbacks) {
this.customToolbarCallbacks = callbacks
// Clear existing custom items
activity?.invalidateOptionsMenu()
customToolbarItemIds.clear()
// Add the new items
addCustomToolbarItems(items)
}

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)

// Add all registered custom toolbar items
for ((identifier, itemId) in customToolbarItemIds) {
// Only add it if it's not already in the menu
if (menu.findItem(itemId) == null) {
// Get the stored configuration for this item
val title = getCustomToolbarItemTitle(identifier)
val drawable = getCustomToolbarItemDrawable(identifier)

menu.add(Menu.NONE, itemId, Menu.NONE, title)
.setIcon(drawable)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
}
}
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Check if this is one of our custom toolbar items
val matchingIdentifier = customToolbarItemIds.entries.find { it.value == item.itemId }?.key
if (matchingIdentifier != null) {
// Notify Flutter about the tap
customToolbarCallbacks?.onCustomToolbarItemTapped(matchingIdentifier){
}
return true
}
return super.onOptionsItemSelected(item)
}

// Store titles for custom toolbar items
private val customToolbarItemTitles = HashMap<String, String>()
// Store drawables for custom toolbar items
private val customToolbarItemDrawables = HashMap<String, Drawable?>()

private fun getCustomToolbarItemTitle(identifier: String): String {
return customToolbarItemTitles[identifier] ?: ""
}

private fun getCustomToolbarItemDrawable(identifier: String): Drawable? {
return customToolbarItemDrawables[identifier]
}

/**
* Adds custom toolbar items based on the configuration from Flutter.
*
* @param items The list of custom toolbar item configurations.
*/
private fun addCustomToolbarItems(items: List<Map<String, Any>>) {
if (items.isEmpty()) return
val activity = requireActivity()

for (itemConfig in items) {
val identifier = itemConfig["identifier"] as? String ?: continue
val title = itemConfig["title"] as? String ?: continue
val iconName = itemConfig["iconName"] as? String
val iconColorHex = itemConfig["iconColor"] as? String

// Store the title for this item
customToolbarItemTitles[identifier] = title

// Generate a unique ID for this menu item
val itemId = identifier.hashCode()
customToolbarItemIds[identifier] = itemId

// Load drawable if available
if (iconName != null) {
val fragmentContext = activity.applicationContext ?: continue
val resourceId = fragmentContext.resources.getIdentifier(iconName, "drawable", fragmentContext.packageName)
if (resourceId != 0) {
val drawable = ContextCompat.getDrawable(fragmentContext, resourceId)?.mutate()

// Apply tint if specified
if (drawable != null && iconColorHex != null) {
try {
val color = Color.parseColor(iconColorHex)
DrawableCompat.setTint(drawable, color)
} catch (e: Exception) {
// Invalid color format, use default
}
}
customToolbarItemDrawables[identifier] = drawable
}
}
}
// Update the menu
activity.invalidateOptionsMenu()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import com.pspdfkit.document.processor.PdfProcessor
import com.pspdfkit.document.processor.PdfProcessor.ProcessorProgress
import com.pspdfkit.document.processor.PdfProcessorTask
import com.pspdfkit.flutter.pspdfkit.AnnotationConfigurationAdaptor.Companion.convertAnnotationConfigurations
import com.pspdfkit.flutter.pspdfkit.api.CustomToolbarCallbacks
import com.pspdfkit.flutter.pspdfkit.api.NutrientEventsCallbacks
import com.pspdfkit.flutter.pspdfkit.api.PspdfkitWidgetCallbacks
import com.pspdfkit.flutter.pspdfkit.api.PspdfkitWidgetControllerApi
Expand All @@ -43,6 +44,9 @@ import com.pspdfkit.forms.ChoiceFormElement
import com.pspdfkit.forms.EditableButtonFormElement
import com.pspdfkit.forms.SignatureFormElement
import com.pspdfkit.forms.TextFormElement
import com.pspdfkit.signatures.storage.DatabaseSignatureStorage
import com.pspdfkit.signatures.storage.SignatureStorage
import com.pspdfkit.ui.PdfFragment
import com.pspdfkit.ui.PdfUiFragment
import com.pspdfkit.ui.PdfUiFragmentBuilder
import io.flutter.plugin.common.BinaryMessenger
Expand All @@ -65,6 +69,7 @@ internal class PSPDFKitView(
private val messenger: BinaryMessenger,
documentPath: String? = null,
configurationMap: HashMap<String, Any>? = null,
customToolbarItems: List<Map<String, Any>>
) : PlatformView, MethodCallHandler {

private var fragmentContainerView: FragmentContainerView? = FragmentContainerView(context)
Expand All @@ -73,8 +78,8 @@ internal class PSPDFKitView(
private var fragmentCallbacks: FlutterPdfUiFragmentCallbacks? = null
private val pspdfkitViewImpl: PspdfkitViewImpl = PspdfkitViewImpl()
private val nutrientEventsCallbacks: NutrientEventsCallbacks = NutrientEventsCallbacks(messenger, "events.callbacks.$id")
private val widgetCallbacks: PspdfkitWidgetCallbacks =
PspdfkitWidgetCallbacks(messenger, "widget.callbacks.$id")
private val widgetCallbacks: PspdfkitWidgetCallbacks = PspdfkitWidgetCallbacks(messenger, "widget.callbacks.$id")
private val customToolbarCallbacks: CustomToolbarCallbacks = CustomToolbarCallbacks(messenger, "customToolbar.callbacks.$id")

init {
fragmentContainerView?.id = View.generateViewId()
Expand Down Expand Up @@ -125,7 +130,12 @@ internal class PSPDFKitView(
if (toolbarGroupingItems != null) {
val groupingRule = FlutterMenuGroupingRule(context, toolbarGroupingItems)
val flutterViewModeController = FlutterViewModeController(groupingRule)
pdfUiFragment.setOnContextualToolbarLifecycleListener(flutterViewModeController)
pdfUiFragment.setOnContextualToolbarLifecycleListener(flutterViewModeController)
}

// Process custom toolbar items
if (customToolbarItems.isNotEmpty() && f is PdfFragment) {
(pdfUiFragment as FlutterPdfUiFragment).setCustomToolbarItems(customToolbarItems, customToolbarCallbacks)
}
}
}
Expand Down Expand Up @@ -177,6 +187,7 @@ internal class PSPDFKitView(
val flutterEventsHelper = FlutterEventsHelper(nutrientEventsCallbacks)
pspdfkitViewImpl.setEventDispatcher(flutterEventsHelper)
PspdfkitWidgetControllerApi.setUp(messenger, pspdfkitViewImpl, id.toString())
// Set up custom toolbar API
}

@SuppressLint("CheckResult")
Expand All @@ -186,8 +197,10 @@ internal class PSPDFKitView(
if (!pdfUiFragment.isAdded) {
return
}
val document = pdfUiFragment.document ?: return

// For other operations, we need the document to be loaded
val document = pdfUiFragment.document ?: return

when (call.method) {
"applyInstantJson" -> {
val annotationsJson: String? = call.argument("annotationsJson")
Expand Down Expand Up @@ -684,6 +697,7 @@ class PSPDFKitViewFactory(
messenger,
creationParams?.get("document") as String?,
creationParams?.get("configuration") as HashMap<String, Any>?,
creationParams?.get("customToolbarItems") as List<Map<String, Any>>
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// 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.
// Autogenerated from Pigeon (v24.2.1), do not edit directly.
// Autogenerated from Pigeon (v24.2.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")

Expand Down Expand Up @@ -2592,3 +2592,34 @@ class AnalyticsEventsCallback(private val binaryMessenger: BinaryMessenger, priv
}
}
}
/**
* Callbacks for custom toolbar item interactions
*
* Generated class from Pigeon that represents Flutter messages that can be called from Kotlin.
*/
class CustomToolbarCallbacks(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
companion object {
/** The codec used by CustomToolbarCallbacks. */
val codec: MessageCodec<Any?> by lazy {
PspdfkitApiPigeonCodec()
}
}
/** Called when a custom toolbar item is tapped */
fun onCustomToolbarItemTapped(identifierArg: String, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.pspdfkit_flutter.CustomToolbarCallbacks.onCustomToolbarItemTapped$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(identifierArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(PspdfkitApiError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(createConnectionError(channelName)))
}
}
}
}
11 changes: 11 additions & 0 deletions example/lib/examples.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:pspdfkit_example/models/papsdkit_example_item.dart';
import 'package:pspdfkit_example/pspdfkit_toolbar_customization.dart';

import 'instant_collaboration_web.dart';
import 'nutrient_custom_toolbar_items_example.dart';
import 'pspdfkit_annotation_preset_customisation.dart';
import 'pspdfkit_document_example.dart';
import 'pspdfkit_event_listeners_example.dart';
Expand Down Expand Up @@ -189,6 +190,16 @@ List<PspdfkitExampleItem> examples(BuildContext context) => [
),
context));
}),
PspdfkitExampleItem(
title: 'Custom Toolbar Items',
description: 'Shows how to add custom toolbar items to PDF view.',
onTap: () async {
await extractAsset(context, _documentPath).then((value) => goTo(
NutrientCustomToolbarItemsExample(
documentPath: value.path,
),
context));
}),
if (kIsWeb)
PspdfkitExampleItem(
title: 'Instant collaboration Web',
Expand Down
Loading