Skip to content

Content of file TreeMasterDetailComposite.java

/*******************************************************************************
 * Copyright (c) 2011-2020 EclipseSource Muenchen GmbH and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 * Clemens Elflein - initial API and implementation
 * Johannes Faltermeier - initial API and implementation
 * Christian W. Damus - bugs 533568, 545460, 527686, 548592, 559116
 ******************************************************************************/
package org.eclipse.emfforms.spi.swt.treemasterdetail;

import static org.eclipse.emfforms.spi.localization.LocalizationServiceHelper.getString;

import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.internal.databinding.observable.DelayedObservableValue;
import org.eclipse.emf.common.command.Command;
import org.eclipse.emf.common.command.CompoundCommand;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecp.common.spi.UniqueSetting;
import org.eclipse.emf.ecp.ui.view.swt.ECPSWTView;
import org.eclipse.emf.ecp.ui.view.swt.ECPSWTViewRenderer;
import org.eclipse.emf.ecp.view.spi.common.callback.ViewModelPropertiesUpdateCallback;
import org.eclipse.emf.ecp.view.spi.context.ViewModelContext;
import org.eclipse.emf.ecp.view.spi.context.ViewModelContextFactory;
import org.eclipse.emf.ecp.view.spi.model.VView;
import org.eclipse.emf.ecp.view.spi.swt.masterdetail.DetailViewCache;
import org.eclipse.emf.ecp.view.spi.swt.masterdetail.DetailViewManager;
import org.eclipse.emf.ecp.view.spi.swt.selection.IMasterDetailSelectionProvider;
import org.eclipse.emf.ecp.view.spi.swt.selection.MasterDetailFocusAdapter;
import org.eclipse.emf.ecp.view.spi.swt.selection.MasterDetailSelectionProvider;
import org.eclipse.emf.ecp.view.treemasterdetail.model.VTreeMasterDetail;
import org.eclipse.emf.edit.command.AddCommand;
import org.eclipse.emf.edit.command.DeleteCommand;
import org.eclipse.emf.edit.command.SetCommand;
import org.eclipse.emf.edit.domain.AdapterFactoryEditingDomain;
import org.eclipse.emf.edit.domain.EditingDomain;
import org.eclipse.emf.edit.domain.IEditingDomainProvider;
import org.eclipse.emfforms.spi.core.services.reveal.EMFFormsRevealService;
import org.eclipse.emfforms.spi.swt.treemasterdetail.util.DetailPanelRenderingFinishedCallback;
import org.eclipse.emfforms.spi.swt.treemasterdetail.util.RootObject;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Sash;

/**
 * The Class MasterDetailRenderer.
 * It is the base renderer for the editor.
 *
 * It takes any object as input and renders a tree on the left-hand side.
 * When selecting an item in the tree (that is an EObject) EMF-Forms is used to render the detail pane on the right-hand
 * side
 *
 * MasterDetailRenderer implements IEditingDomainProvider to allow Undo/Redo/Copy/Cut/Paste actions to be performed
 * externally.
 *
 * MasterDetailRenderer provides an ISelectionProvider to get the currently selected items in the tree
 *
 */
@SuppressWarnings("restriction")
public class TreeMasterDetailComposite extends Composite implements IEditingDomainProvider {

	/** The input. */
	private final Object input;

	/** The editing domain. */
	private final EditingDomain editingDomain;

	/** The tree viewer. */
	private TreeViewer treeViewer;

	/** The selection provider. */
	private IMasterDetailSelectionProvider selectionProvider;

	/** The vertical sash. */
	private Sash verticalSash;

	/** The detail scrollable composite. */
	private Composite detailComposite;

	/** Manager of the currently rendered ECPSWTView with caching. */
	private DetailViewManager detailManager;

	private final String selectNodeMessage = getString(getClass(), "selectNodeMessage"); //$NON-NLS-1$
	private final String loadingMessage = getString(getClass(), "loadingMessage"); //$NON-NLS-1$

	private Object lastRenderedObject;

	private final TreeMasterDetailSWTCustomization customization;

	/** the delay between a selection change and the start of the rendering. */
	private final int renderDelay;

	private ViewModelPropertiesUpdateCallback viewModelPropertiesUpdateCallback;
	private final Set<DetailPanelRenderingFinishedCallback> detailPanelRenderingFinishedCallbacks = new LinkedHashSet<DetailPanelRenderingFinishedCallback>();

	/**
	 * Default constructor.
	 *
	 * @param parent the parent composite
	 * @param style the style bits
	 * @param input the input for the tree
	 * @param customization the customization
	 * @param renderDelay the delay between a selection change and updating the detail
	 */
	/* package */ TreeMasterDetailComposite(Composite parent, int style, Object input,
		TreeMasterDetailSWTCustomization customization, int renderDelay) {
		super(parent, style);
		this.input = input;
		if (input instanceof Resource) {
			editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(((Resource) input).getContents().get(0));
		} else if (input instanceof RootObject) {
			editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(RootObject.class.cast(input).getRoot());
		} else {
			editingDomain = AdapterFactoryEditingDomain.getEditingDomainFor(input);
		}
		this.renderDelay = renderDelay;
		this.customization = customization;

		renderControl(customization);

		parent.addDisposeListener(new DisposeListener() {

			@Override
			public void widgetDisposed(DisposeEvent e) {
				TreeMasterDetailComposite.this.dispose();
			}
		});
	}

	private Control renderControl(TreeMasterDetailSWTCustomization buildBehaviour) {
		// Create the Form with two panels and a header
		setLayout(new FormLayout());

		// Create the Separator
		verticalSash = createSash(this, buildBehaviour);

		// Create the Tree
		final Composite treeComposite = new Composite(this, SWT.NONE);
		addTreeViewerLayoutData(treeComposite, verticalSash);
		GridLayoutFactory.fillDefaults().numColumns(1).applyTo(treeComposite);
		treeViewer = TreeViewerSWTFactory.createTreeViewer(treeComposite, input, customization);
		selectionProvider = new MasterDetailSelectionProvider(treeViewer);
		treeViewer.getControl().addFocusListener(
			new MasterDetailFocusAdapter(selectionProvider, () -> detailManager.getDetailContainer()));

		// Create detail composite
		detailComposite = buildBehaviour.createDetailComposite(this);
		addDetailCompositeLayoutData(detailComposite, verticalSash);
		Composite detailParent = detailComposite;
		if (detailParent instanceof ScrolledComposite) {
			final Composite detailPanel = new Composite(detailParent, SWT.BORDER);
			((ScrolledComposite) detailParent).setContent(detailPanel);
			detailParent = detailPanel;
		}
		detailManager = new DetailViewManager(detailParent);
		detailManager.setNoDetailMessage(selectNodeMessage);
		detailManager.layoutDetailParent(detailParent);

		/* enable optional delayed update mechanism */
		IObservableValue<?> treeViewerSelectionObservable = ViewersObservables
			.observeSingleSelection(treeViewer);
		if (renderDelay > 0) {
			treeViewerSelectionObservable = new DelayedObservableValue<>(renderDelay,
				treeViewerSelectionObservable);
		}
		treeViewerSelectionObservable.addChangeListener(__ -> doUpdateDetailPanel(false));

		final IObservableValue<?> observableToDispose = treeViewerSelectionObservable;
		treeComposite.addDisposeListener(__ -> observableToDispose.dispose());

		/* add key listener to switch focus on enter */
		treeViewer.getTree().addKeyListener(new KeyAdapter() {
@Override public void keyReleased(KeyEvent e) { if (e.keyCode == SWT.CR || e.keyCode == SWT.LF) { doUpdateDetailPanel(true); } } }); /* add double click listener to switch focus on enter */ treeViewer.addDoubleClickListener(new IDoubleClickListener() { @Override public void doubleClick(DoubleClickEvent event) { doUpdateDetailPanel(true); } }); updateDetailPanel(false); return this; } private void setFocusToDetail() { detailManager.setFocus(); } private void addDetailCompositeLayoutData(Composite detailComposite, Sash verticalSash) { final FormData detailFormData = new FormData(); detailFormData.left = new FormAttachment(verticalSash, 2); detailFormData.top = new FormAttachment(0, 5); detailFormData.bottom = new FormAttachment(100, -5); detailFormData.right = new FormAttachment(100, -5); detailComposite.setLayoutData(detailFormData); } private void addTreeViewerLayoutData(Composite treeComposite, Sash verticalSash) { final FormData treeFormData = new FormData(); treeFormData.bottom = new FormAttachment(100, -5); treeFormData.left = new FormAttachment(0, 5); treeFormData.right = new FormAttachment(verticalSash, -2); treeFormData.top = new FormAttachment(0, 5); treeComposite.setLayoutData(treeFormData); } private Sash createSash(final Composite parent, TreeWidthProvider buildBehaviour) { final Sash sash = new Sash(parent, SWT.VERTICAL); // Make the left panel 300px wide and put it below the header final FormData sashFormData = new FormData(); sashFormData.bottom = new FormAttachment(100, -5); sashFormData.left = new FormAttachment(0, buildBehaviour.getInitialTreeWidth()); sashFormData.top = new FormAttachment(0, 5); sash.setLayoutData(sashFormData); // As soon as the sash is moved, layout the parent to reflect the changes sash.addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { sash.setLocation(e.x, e.y); final FormData sashFormData = new FormData(); sashFormData.bottom = new FormAttachment(100, -5); sashFormData.left = new FormAttachment(0, e.x); sashFormData.top = new FormAttachment(0, 5); sash.setLayoutData(sashFormData); parent.layout(true); } }); return sash; } // TODO JF this needs to be refactored, when used as the replacement for the treemasterdetail renderer. // selection modification required as well as adjusting the loading properties /** * Updates the detail panel of the tree master detail. * * @param setFocusToDetail <code>true</code> if the focus should be moved to the detail panel * * @since 1.11 */ public void updateDetailPanel(final boolean setFocusToDetail) { // Create a new detail panel in the scrollable composite. Disposes any old panels. // createDetailPanel(); // TODO create detail panel at the right location final IStructuredSelection selection = (StructuredSelection) treeViewer.getSelection(); final Object selectedObject = getSelectedObject(selection); detailManager.cacheCurrentDetail(); boolean asyncRendering = false; ViewModelContext context = null; if (selectedObject instanceof EObject) { lastRenderedObject = selectedObject; final EObject eObject = EObject.class.cast(selectedObject); if (detailManager.isCached(eObject)) { // It's ready to present (no async needed) context = detailManager.activate(eObject).getViewModelContext(); updateScrolledComposite(); } else { if (viewModelPropertiesUpdateCallback != null) { viewModelPropertiesUpdateCallback.updateViewModelProperties(detailManager.getDetailProperties()); } // Check, if the selected object would be rendered using a TreeMasterDetail. If so, render the provided // detail view. final VView view = detailManager.getDetailView(eObject); if (view.getChildren().size() > 0 && view.getChildren().get(0) instanceof VTreeMasterDetail) { // Yes, we need to render this node differently final VView treeDetailView = VTreeMasterDetail.class.cast(view.getChildren().get(0)) .getDetailView(); // Even if the TMD composite is not configured as read-only honor the effective read-only // configuration of the loaded detail view treeDetailView.setReadonly(treeDetailView.isEffectivelyReadonly() || customization.isReadOnly()); context = ViewModelContextFactory.INSTANCE.createViewModelContext(treeDetailView, eObject); detailManager.render(context, ECPSWTViewRenderer.INSTANCE::render); } else { // No, everything is fine detailManager.setNoDetailMessage(loadingMessage); asyncRendering = true; Display.getDefault().asyncExec(new UpdateDetailRunnable(setFocusToDetail, eObject)); } // After rendering the Forms, compute the size of the form. So the scroll container knows when to scroll updateScrolledComposite(); } } else { renderEmptyDetailPanel(); } /* * Notify the callbacks that the rendering has been finished. * In case of async rendering, the async process needs to notify the callbacks. */ if (!asyncRendering) { for (final DetailPanelRenderingFinishedCallback callback : detailPanelRenderingFinishedCallbacks) { callback.renderingFinished(context, selectedObject); } } } private Object getSelectedObject(IStructuredSelection selection) { // Get the selected object, if it is an EObject, render the details using EMF Forms Object selectedObject = selection != null ? selection.getFirstElement() : null; if (customization.enableVerticalCopy() && selectedObject instanceof EObject && selection.size() > 1) { boolean allOfSameType = true; final EObject dummy = EcoreUtil.create(((EObject) selectedObject).eClass()); final Iterator<?> iterator = selection.iterator(); final Set<EObject> selectedEObjects = new LinkedHashSet<EObject>(); while (iterator.hasNext()) { final EObject eObject = (EObject) iterator.next(); allOfSameType &= eObject.eClass() == dummy.eClass(); if (allOfSameType) { for (final EAttribute attribute : dummy.eClass().getEAllAttributes()) { if (eObject == selectedObject) { dummy.eSet(attribute, eObject.eGet(attribute)); } else if (dummy.eGet(attribute) != null && !dummy.eGet(attribute).equals(eObject.eGet(attribute))) { dummy.eUnset(attribute); } } selectedEObjects.add(eObject); } else { break; } } if (allOfSameType) { selectedObject = dummy; dummy.eAdapters().add(new MultiEditAdapter(selectedEObjects, dummy)); } } return selectedObject; } private void updateScrolledComposite() { if (ScrolledComposite.class.isInstance(detailComposite)) { ScrolledComposite.class.cast(detailComposite) .setMinSize(detailManager.getDetailContainer().computeSize(SWT.DEFAULT, SWT.DEFAULT)); } } private void renderEmptyDetailPanel() { lastRenderedObject = null; detailManager.cacheCurrentDetail(); updateScrolledComposite(); } @Override public void dispose() { detailManager.dispose(); customization.dispose(); super.dispose(); } /** * Gets the current selection. * * @return the current selection */ public Object getCurrentSelection() { if (!(treeViewer.getSelection() instanceof StructuredSelection)) { return null; } return ((StructuredSelection) treeViewer.getSelection()).getFirstElement(); } /** * Sets the selection. * * @param structuredSelection the new selection * @since 1.9 */ public void setSelection(ISelection structuredSelection) { treeViewer.setSelection(structuredSelection); } /** * Gets the tree viewer. * * @return the tree viewer (which is a selection provider) * * @deprecated Use the {@link #getMasterDetailSelectionProvider() master-detail selection provider}, instead}, * or {@link #refresh()} to force a refresh of the tree, or {@link #selectAndReveal(Object)} * to select and reveal some object in my tree * @see #getMasterDetailSelectionProvider() */ @Deprecated public TreeViewer getSelectionProvider() { return treeViewer; } /** * Get the master/detail-aware selection provider. * * @return a selection provider that is aware of the user's focus on either the * master tree or the detail view * @since 1.21 */ public ISelectionProvider getMasterDetailSelectionProvider() { return selectionProvider; } /** * Request a refresh of my tree. * * @since 1.22 */ public void refresh() { if (treeViewer != null) { treeViewer.refresh(); } } /** * Select and reveal a {@code selection} in my tree. If the argument is an {@link UniqueSetting}, * then the {@linkplain UniqueSetting#getEObject() owner} of the setting will be revealed and the * control that edits the {@linkplain UniqueSetting#getEStructuralFeature() setting} will be * revealed and focused (if possible) in the object's detail view. * * @param selection the objet to select and reveal * @return {@code true} if the {@code selection} was revealed; {@code false}, otherwise, including * the case where the nearest parent object up the tree was revealed instead * @since 1.22 */ public boolean selectAndReveal(final Object selection) { boolean result = false; Object toReveal = selection; final EStructuralFeature feature; if (selection instanceof UniqueSetting) { final UniqueSetting setting = (UniqueSetting) selection; toReveal = setting.getEObject(); feature = setting.getEStructuralFeature(); } else if (selection instanceof EStructuralFeature.Setting) { final EStructuralFeature.Setting setting = (EStructuralFeature.Setting) selection; toReveal = setting.getEObject(); feature = setting.getEStructuralFeature(); } else { feature = null; } if (feature != null) { final CompletableFuture<ECPSWTView> renderedDetail = new CompletableFuture<>(); final DetailPanelRenderingFinishedCallback detailReady = __ -> renderedDetail .complete(detailManager.getCurrentDetail()); registerDetailPanelRenderingFinishedCallback(detailReady); final EObject owner = (EObject) toReveal; result = selectAndRevealInTree(owner); if (result) { renderedDetail.whenComplete((view, e) -> { unregisterDetailPanelRenderingFinishedCallback(detailReady); revealInDetail(view, owner, feature); }); } else { // Won't need the call-back so remove it now renderedDetail.cancel(false); unregisterDetailPanelRenderingFinishedCallback(detailReady); } // Do we already have the detail? final ECPSWTView currentDetail = detailManager.getCurrentDetail(); if (currentDetail != null && currentDetail.getViewModelContext().getDomainModel() == owner) { // There won't be an asynchronous rendering to wait for renderedDetail.complete(currentDetail); } } else { result = selectAndRevealInTree(toReveal); } return result; } private boolean selectAndRevealInTree(final Object selection) { if (treeViewer == null) { return false; } boolean result = false; // Try to reveal the 'selection' in the tree. If it isn't in the // tree, then search up the content provider to find the nearest // object that can be revealed and select that, or give up for (Object objectToReveal = selection; objectToReveal != null;) { treeViewer.reveal(objectToReveal); if (treeViewer.testFindItem(objectToReveal) != null) { // Select it and we're done treeViewer.setSelection(new StructuredSelection(objectToReveal)); result = objectToReveal == selection; break; } // Look up the content tree for an object to reveal objectToReveal = ((ITreeContentProvider) treeViewer.getContentProvider()).getParent(objectToReveal); } return result; } private void revealInDetail(ECPSWTView detail, EObject object, EStructuralFeature feature) { final ViewModelContext context = detail.getViewModelContext(); if (!context.hasService(EMFFormsRevealService.class)) { // Nothing to do return; } final EMFFormsRevealService reveal = context.getService(EMFFormsRevealService.class); reveal.reveal(object, feature); } /** * Gets the editing domain. * * @return the editing domain */ @Override public EditingDomain getEditingDomain() { return editingDomain; } /** * Allows to set a different input for the treeviewer. * * @param input the new input */ public void setInput(Object input) { treeViewer.setInput(input); } /** * Allows to override the default cache implementation by the provided one. * * @param cache The {@link TreeMasterDetailCache} to use. * @since 1.9 * * @deprecated As of 1.22, use the {@link #setCache(DetailViewCache)} API, instead */ @Deprecated public void setCache(TreeMasterDetailCache cache) { setCache((DetailViewCache) cache); } /** * Override the default cache implementation. * * @param cache the {@link DetailViewCache} to use, or {@code null} to use no cache * @since 1.22 */ public void setCache(DetailViewCache cache) { detailManager.setCache(cache); } private void doUpdateDetailPanel(boolean setFocusToDetail) { if (lastRenderedObject == getCurrentSelection()) { if (setFocusToDetail) { setFocusToDetail(); } /* * possible when e.g. a double click or enter has forced an instant rendering and the delay update kicks in. */ return; } updateDetailPanel(setFocusToDetail); } /** * Returns whether I am read-only. * * @return <code>true</code> if read-only * @since 1.22 * @see TreeMasterDetailSWTBuilder#customizeReadOnly(boolean) */ public boolean isReadOnly() { return customization.isReadOnly(); } /** * Adapter which listens to changes and delegates the notification to other EObjects. * * @author Eugen Neufeld * */ private final class MultiEditAdapter extends AdapterImpl { private final Set<EObject> selectedEObjects; private final EObject dummy; private MultiEditAdapter(Set<EObject> selectedEObjects, EObject dummy) { this.selectedEObjects = selectedEObjects; this.dummy = dummy; } @Override public void notifyChanged(Notification notification) { if (dummy.eClass().getEAllAttributes().contains(notification.getFeature())) { final CompoundCommand cc = new CompoundCommand(); for (final EObject selected : selectedEObjects) { Command command = null; switch (notification.getEventType()) { case Notification.SET: command = SetCommand.create(editingDomain, selected, notification.getFeature(), notification.getNewValue()); break; case Notification.UNSET: command = SetCommand.create(editingDomain, selected, notification.getFeature(), SetCommand.UNSET_VALUE); break; case Notification.ADD: case Notification.ADD_MANY: command = AddCommand.create(editingDomain, selected, notification.getFeature(), notification.getNewValue()); break; case Notification.REMOVE: case Notification.REMOVE_MANY: command = DeleteCommand.create(editingDomain, notification.getOldValue()); break; default: continue; } cc.append(command); } editingDomain.getCommandStack().execute(cc); } } } /** * Runnable which updates the detail panel. */ private final class UpdateDetailRunnable implements Runnable { private final boolean setFocusToDetail; private final EObject eObject; UpdateDetailRunnable(boolean setFocusToDetail, EObject eObject) { super(); this.setFocusToDetail = setFocusToDetail; this.eObject = eObject; } @Override public void run() { if (detailManager.isDisposed()) { // We've been disposed. Nothing to do return; } if (viewModelPropertiesUpdateCallback != null) { viewModelPropertiesUpdateCallback.updateViewModelProperties(detailManager.getDetailProperties()); } final VView view = detailManager.getDetailView(eObject); // Even if the TMD is not configured as read-only honor the effective read-only // configuration of the loaded view view.setReadonly(view.isEffectivelyReadonly() || customization.isReadOnly()); final ViewModelContext modelContext = ViewModelContextFactory.INSTANCE .createViewModelContext( view, eObject, customization.getViewModelServices(view, eObject)); detailManager.setNoDetailMessage(selectNodeMessage); if (detailManager.isDisposed()) { return; } detailManager.render(modelContext, ECPSWTViewRenderer.INSTANCE::render); updateScrolledComposite(); if (setFocusToDetail) { setFocusToDetail(); } // notify callbacks that the rendering was finished for (final DetailPanelRenderingFinishedCallback callback : detailPanelRenderingFinishedCallbacks) { callback.renderingFinished(modelContext, eObject); } } } /** * Adds a {@link ViewModelPropertiesUpdateCallback}. * * @param viewModelPropertiesUpdateCallback the callback * @since 1.11 */ public void addViewModelPropertiesUpdateCallback( ViewModelPropertiesUpdateCallback viewModelPropertiesUpdateCallback) { this.viewModelPropertiesUpdateCallback = viewModelPropertiesUpdateCallback; } /** * Register a callback that is notified whenever the rendering of a detail panel is finished. * * @param detailPanelRenderingFinishedCallback the callback * @return <code>true</code> if the callback has been added, <code>false</code> if it was already registered * @since 1.13 */ public boolean registerDetailPanelRenderingFinishedCallback( DetailPanelRenderingFinishedCallback detailPanelRenderingFinishedCallback) { return detailPanelRenderingFinishedCallbacks.add(detailPanelRenderingFinishedCallback); } /** * Register a callback that is notified whenever the rendering of a detail panel is finished. * * @param detailPanelRenderingFinishedCallback the callback * @return <code>true</code> if the callback has been removed, <code>false</code> if it was not registered * @since 1.13 */ public boolean unregisterDetailPanelRenderingFinishedCallback( DetailPanelRenderingFinishedCallback detailPanelRenderingFinishedCallback) { return detailPanelRenderingFinishedCallbacks.remove(detailPanelRenderingFinishedCallback); } }