org.pentaho.reporting.engine.classic.core.modules.gui.base.PreviewPane.java Source code

Java tutorial

Introduction

Here is the source code for org.pentaho.reporting.engine.classic.core.modules.gui.base.PreviewPane.java

Source

/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright (c) 2001 - 2016 Object Refinery Ltd, Hitachi Vantara and Contributors..  All rights reserved.
 */

package org.pentaho.reporting.engine.classic.core.modules.gui.base;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.IllegalComponentStateException;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.print.PageFormat;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JMenu;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JToolBar;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
import org.pentaho.reporting.engine.classic.core.MasterReport;
import org.pentaho.reporting.engine.classic.core.PageDefinition;
import org.pentaho.reporting.engine.classic.core.ReportProcessingException;
import org.pentaho.reporting.engine.classic.core.event.ReportProgressEvent;
import org.pentaho.reporting.engine.classic.core.event.ReportProgressListener;
import org.pentaho.reporting.engine.classic.core.imagemap.ImageMap;
import org.pentaho.reporting.engine.classic.core.imagemap.ImageMapEntry;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableReplacedContentBox;
import org.pentaho.reporting.engine.classic.core.layout.output.RenderUtility;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.actions.ZoomAction;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.event.ReportHyperlinkEvent;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.event.ReportHyperlinkListener;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.event.ReportMouseEvent;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.event.ReportMouseListener;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.internal.ActionCategory;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.internal.ActionPluginComparator;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.internal.CategoryTreeItem;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.internal.PageBackgroundDrawable;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.internal.PreviewDrawablePanel;
import org.pentaho.reporting.engine.classic.core.modules.gui.base.internal.PreviewPaneUtilities;
import org.pentaho.reporting.engine.classic.core.modules.gui.common.IconTheme;
import org.pentaho.reporting.engine.classic.core.modules.gui.common.StatusListener;
import org.pentaho.reporting.engine.classic.core.modules.gui.common.StatusType;
import org.pentaho.reporting.engine.classic.core.modules.gui.commonswing.ActionPlugin;
import org.pentaho.reporting.engine.classic.core.modules.gui.commonswing.CenterLayout;
import org.pentaho.reporting.engine.classic.core.modules.gui.commonswing.ReportEventSource;
import org.pentaho.reporting.engine.classic.core.modules.gui.commonswing.SwingGuiContext;
import org.pentaho.reporting.engine.classic.core.modules.gui.commonswing.WindowSizeLimiter;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.graphics.PageDrawable;
import org.pentaho.reporting.engine.classic.core.modules.output.pageable.graphics.PrintReportProcessor;
import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys;
import org.pentaho.reporting.engine.classic.core.util.Worker;
import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.util.Messages;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.designtime.swing.KeyedComboBoxModel;
import org.pentaho.reporting.libraries.designtime.swing.LibSwingUtil;
import org.pentaho.reporting.libraries.xmlns.LibXmlInfo;
import org.pentaho.reporting.libraries.xmlns.common.ParserUtil;

/**
 * Creation-Date: 11.11.2006, 19:36:13
 *
 * @author Thomas Morgner
 */
public class PreviewPane extends JPanel implements ReportEventSource {
    private static final Log logger = LogFactory.getLog(PreviewPane.class);

    private class PreviewGuiContext implements SwingGuiContext {
        protected PreviewGuiContext() {
        }

        public Window getWindow() {
            return LibSwingUtil.getWindowAncestor(PreviewPane.this);
        }

        public Locale getLocale() {
            final MasterReport report = getReportJob();
            if (report != null) {
                final Locale bundleLocale = report.getResourceBundleFactory().getLocale();
                if (bundleLocale != null) {
                    return bundleLocale;
                }
                return report.getReportEnvironment().getLocale();
            }
            return Locale.getDefault();
        }

        public IconTheme getIconTheme() {
            return PreviewPane.this.getIconTheme();
        }

        public Configuration getConfiguration() {
            return PreviewPane.this.computeContextConfiguration();
        }

        public StatusListener getStatusListener() {
            return PreviewPane.this.getStatusListener();
        }

        public ReportEventSource getEventSource() {
            return PreviewPane.this;
        }
    }

    private class PreviewPaneStatusUpdater implements Runnable {
        private StatusType type;
        private String text;
        private Throwable error;

        protected PreviewPaneStatusUpdater(final StatusType type, final String text, final Throwable error) {
            this.type = type;
            this.text = text;
            this.error = error;
        }

        public Throwable getError() {
            return error;
        }

        public StatusType getType() {
            return type;
        }

        public String getText() {
            return text;
        }

        /**
         * When an object implementing interface <code>Runnable</code> is used to create a thread, starting the thread
         * causes the object's <code>run</code> method to be called in that separately executing thread.
         * <p/>
         * The general contract of the method <code>run</code> is that it may take any action whatsoever.
         *
         * @see Thread#run()
         */
        public void run() {
            setStatusType(type);
            setStatusText(text);
            setError(error);
        }
    }

    /**
     * The StatusListener here shields the preview pane from any attempt to tamper with it.
     */
    private class PreviewPaneStatusListener implements StatusListener {
        protected PreviewPaneStatusListener() {
        }

        public void setStatus(final StatusType type, final String text, final Throwable error) {
            if (SwingUtilities.isEventDispatchThread()) {
                setStatusType(type);
                setStatusText(text);
                setError(error);
            } else {
                SwingUtilities.invokeLater(new PreviewPaneStatusUpdater(type, text, error));
            }
        }
    }

    private class RepaginationRunnable implements Runnable, ReportProgressListener {
        private PrintReportProcessor processor;

        protected RepaginationRunnable(final PrintReportProcessor processor) {
            this.processor = processor;
        }

        public void reportProcessingStarted(final ReportProgressEvent event) {
            forwardReportStartedEvent(event);
        }

        public void reportProcessingUpdate(final ReportProgressEvent event) {
            forwardReportUpdateEvent(event);
        }

        public void reportProcessingFinished(final ReportProgressEvent event) {
            forwardReportFinishedEvent(event);
        }

        /**
         * When an object implementing interface <code>Runnable</code> is used to create a thread, starting the thread
         * causes the object's <code>run</code> method to be called in that separately executing thread.
         * <p/>
         * The general contract of the method <code>run</code> is that it may take any action whatsoever.
         *
         * @see Thread#run()
         */
        public void run() {
            this.processor.addReportProgressListener(this);
            try {
                final UpdatePaginatingPropertyHandler startPaginationNotify = new UpdatePaginatingPropertyHandler(
                        processor, true, false, 0);
                if (SwingUtilities.isEventDispatchThread()) {
                    startPaginationNotify.run();
                } else {
                    SwingUtilities.invokeLater(startPaginationNotify);
                }

                // Perform the pagination ..
                final int pageCount = processor.getNumberOfPages();
                final UpdatePaginatingPropertyHandler endPaginationNotify = new UpdatePaginatingPropertyHandler(
                        processor, false, true, pageCount);
                if (SwingUtilities.isEventDispatchThread()) {
                    endPaginationNotify.run();
                } else {
                    SwingUtilities.invokeLater(endPaginationNotify);
                }
            } catch (Exception e) {
                final UpdatePaginatingPropertyHandler endPaginationNotify = new UpdatePaginatingPropertyHandler(
                        processor, false, false, 0);
                if (SwingUtilities.isEventDispatchThread()) {
                    endPaginationNotify.run();
                } else {
                    SwingUtilities.invokeLater(endPaginationNotify);
                }
                PreviewPane.logger.error("Pagination failed.", e); //$NON-NLS-1$
            } finally {
                this.processor.removeReportProgressListener(this);
            }
        }
    }

    private class UpdatePaginatingPropertyHandler implements Runnable {
        private boolean paginating;
        private boolean paginated;
        private int pageCount;
        private PrintReportProcessor processor;

        protected UpdatePaginatingPropertyHandler(final PrintReportProcessor processor, final boolean paginating,
                final boolean paginated, final int pageCount) {
            this.processor = processor;
            this.paginating = paginating;
            this.paginated = paginated;
            this.pageCount = pageCount;
        }

        public void run() {
            if (processor != getPrintReportProcessor()) {
                PreviewPane.logger.debug(messages.getString("PreviewPane.DEBUG_NO_LONGER_VALID")); //$NON-NLS-1$
                return;
            }

            PreviewPane.logger.debug(messages.getString("PreviewPane.DEBUG_PAGINATION", String.valueOf(paginating),
                    String.valueOf(pageCount))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            if (paginating == false) {
                setNumberOfPages(pageCount);
                if (getPageNumber() < 1) {
                    setPageNumber(1);
                } else if (getPageNumber() > pageCount) {
                    setPageNumber(pageCount);
                }
            }
            setPaginating(paginating);
            setPaginated(paginated);

            if (processor.isError()) {
                setError(processor.getErrorReason());
                setStatusType(StatusType.ERROR);
                setStatusText(processor.getErrorReason().getLocalizedMessage());
            }
        }
    }

    private class PreviewUpdateHandler implements PropertyChangeListener {
        protected PreviewUpdateHandler() {
        }

        public void propertyChange(final PropertyChangeEvent evt) {
            final String propertyName = evt.getPropertyName();
            if (PreviewPane.PAGINATING_PROPERTY.equals(propertyName)) {
                if (isPaginating()) {
                    PreviewPane.this.getReportPreviewArea().setDrawableAsRawObject(getPaginatingDrawable());
                } else {
                    updateVisiblePage(getPageNumber());
                }
            } else if (PreviewPane.REPORT_JOB_PROPERTY.equals(propertyName)) {
                if (getReportJob() == null) {
                    PreviewPane.this.getReportPreviewArea().setDrawableAsRawObject(getNoReportDrawable());
                }
                // else the paginating property will be fired anyway ..
            } else if (PreviewPane.PAGE_NUMBER_PROPERTY.equals(propertyName)) {
                if (isPaginating()) {
                    return;
                }

                updateVisiblePage(getPageNumber());
            }
        }
    }

    private class UpdateZoomHandler implements PropertyChangeListener {
        protected UpdateZoomHandler() {
        }

        /**
         * This method gets called when a bound property is changed.
         *
         * @param evt
         *          A PropertyChangeEvent object describing the event source and the property that has changed.
         */

        public void propertyChange(final PropertyChangeEvent evt) {
            if ("zoom".equals(evt.getPropertyName()) == false) { //$NON-NLS-1$
                return;
            }

            final double zoom = getZoom();
            pageDrawable.setZoom(zoom);
            final KeyedComboBoxModel<Double, String> zoomModel = PreviewPane.this.getZoomModel();
            zoomModel.setSelectedKey(new Double(zoom));
            if (zoomModel.getSelectedKey() == null) {
                zoomModel.setSelectedItem(formatZoomText(zoom));
            }
            drawablePanel.revalidate();
        }
    }

    /**
     * Maps incomming report-mouse events into Hyperlink events.
     */
    private class HyperLinkEventProcessor implements ReportMouseListener {
        private boolean mouseLinkActive;

        private HyperLinkEventProcessor() {
        }

        public void reportMouseClicked(final ReportMouseEvent event) {
            final RenderNode renderNode = event.getSourceNode();
            final String target = extractLink(renderNode, event);
            if (target == null) {
                return;
            }

            final String window = (String) renderNode.getStyleSheet()
                    .getStyleProperty(ElementStyleKeys.HREF_WINDOW);
            final String title = (String) renderNode.getStyleSheet().getStyleProperty(ElementStyleKeys.HREF_TITLE);
            final ReportHyperlinkEvent hyEvent = new ReportHyperlinkEvent(PreviewPane.this, renderNode, target,
                    window, title);
            fireReportHyperlinkEvent(hyEvent);
        }

        private String extractLink(final RenderNode node, final ReportMouseEvent event) {
            if (node instanceof RenderableReplacedContentBox) {
                // process image map
                final ImageMap imageMap = RenderUtility.extractImageMap((RenderableReplacedContentBox) node);
                if (imageMap != null) {
                    final PageDrawable physicalPageDrawable = drawablePanel.getPageDrawable();
                    final PageFormat pf = physicalPageDrawable.getPageFormat();
                    final float x1 = (float) (event.getSourceEvent().getX() / zoom);
                    final float y1 = (float) (event.getSourceEvent().getY() / zoom);
                    final float imageMapX = (float) (x1 - pf.getImageableX()
                            - StrictGeomUtility.toExternalValue(node.getX()));
                    final float imageMapY = (float) (y1 - pf.getImageableY()
                            - StrictGeomUtility.toExternalValue(node.getY()));
                    final ImageMapEntry[] imageMapEntries = imageMap.getEntriesForPoint(imageMapX, imageMapY);
                    for (int i = 0; i < imageMapEntries.length; i++) {
                        final ImageMapEntry entry = imageMapEntries[i];
                        final Object imageMapTarget = entry.getAttribute(LibXmlInfo.XHTML_NAMESPACE, "href");
                        if (imageMapTarget != null) {
                            return String.valueOf(imageMapTarget);
                        }
                    }
                }
            }

            final String target = (String) node.getStyleSheet().getStyleProperty(ElementStyleKeys.HREF_TARGET);
            if (target == null) {
                return null;
            }
            return target;
        }

        public void reportMousePressed(final ReportMouseEvent event) {
            // not used.
        }

        public void reportMouseReleased(final ReportMouseEvent event) {
            // not used.
        }

        public void reportMouseMoved(final ReportMouseEvent event) {
            if (isHyperlinkSystemActive() == false) {
                return;
            }

            final RenderNode renderNode = event.getSourceNode();
            final String target = extractLink(renderNode, event);
            if (target == null) {
                if (mouseLinkActive) {
                    setCursor(Cursor.getDefaultCursor());
                    mouseLinkActive = false;
                }
                return;
            }

            if (mouseLinkActive == false) {
                setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
                mouseLinkActive = true;
            }
        }

        public void reportMouseDragged(final ReportMouseEvent event) {
        }
    }

    private static class ScrollablePanel extends JPanel implements Scrollable {
        /**
         * Creates a new <code>JPanel</code> with a double buffer and a flow layout.
         */
        private ScrollablePanel() {
            setLayout(new CenterLayout());
        }

        /**
         * Returns the preferred size of the viewport for a view component. For example, the preferred size of a
         * <code>JList</code> component is the size required to accommodate all of the cells in its list. However, the value
         * of <code>preferredScrollableViewportSize</code> is the size required for <code>JList.getVisibleRowCount</code>
         * rows. A component without any properties that would affect the viewport size should just return
         * <code>getPreferredSize</code> here.
         *
         * @return the preferredSize of a <code>JViewport</code> whose view is this <code>Scrollable</code>
         * @see javax.swing.JViewport#getPreferredSize
         */
        public Dimension getPreferredScrollableViewportSize() {
            return getPreferredSize();
        }

        /**
         * Components that display logical rows or columns should compute the scroll increment that will completely expose
         * one new row or column, depending on the value of orientation. Ideally, components should handle a partially
         * exposed row or column by returning the distance required to completely expose the item.
         * <p/>
         * Scrolling containers, like JScrollPane, will use this method each time the user requests a unit scroll.
         *
         * @param visibleRect
         *          The view area visible within the viewport
         * @param orientation
         *          Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
         * @param direction
         *          Less than zero to scroll up/left, greater than zero for down/right.
         * @return The "unit" increment for scrolling in the specified direction. This value should always be positive.
         */
        public int getScrollableUnitIncrement(final Rectangle visibleRect, final int orientation,
                final int direction) {
            return 20;
        }

        /**
         * Components that display logical rows or columns should compute the scroll increment that will completely expose
         * one block of rows or columns, depending on the value of orientation.
         * <p/>
         * Scrolling containers, like JScrollPane, will use this method each time the user requests a block scroll.
         *
         * @param visibleRect
         *          The view area visible within the viewport
         * @param orientation
         *          Either SwingConstants.VERTICAL or SwingConstants.HORIZONTAL.
         * @param direction
         *          Less than zero to scroll up/left, greater than zero for down/right.
         * @return The "block" increment for scrolling in the specified direction. This value should always be positive.
         */
        public int getScrollableBlockIncrement(final Rectangle visibleRect, final int orientation,
                final int direction) {
            return 100;
        }

        /**
         * Return true if a viewport should always force the width of this <code>Scrollable</code> to match the width of the
         * viewport. For example a normal text view that supported line wrapping would return true here, since it would be
         * undesirable for wrapped lines to disappear beyond the right edge of the viewport. Note that returning true for a
         * Scrollable whose ancestor is a JScrollPane effectively disables horizontal scrolling.
         * <p/>
         * Scrolling containers, like JViewport, will use this method each time they are validated.
         *
         * @return True if a viewport should force the Scrollables width to match its own.
         */
        public boolean getScrollableTracksViewportWidth() {
            return false;
        }

        /**
         * Return true if a viewport should always force the height of this Scrollable to match the height of the viewport.
         * For example a columnar text view that flowed text in left to right columns could effectively disable vertical
         * scrolling by returning true here.
         * <p/>
         * Scrolling containers, like JViewport, will use this method each time they are validated.
         *
         * @return True if a viewport should force the Scrollables height to match its own.
         */
        public boolean getScrollableTracksViewportHeight() {
            return false;
        }
    }

    /**
     * A zoom select action.
     */
    private static class ZoomSelectAction extends AbstractAction {
        private KeyedComboBoxModel source;
        private PreviewPane pane;

        /**
         * Creates a new action.
         */
        protected ZoomSelectAction(final KeyedComboBoxModel source, final PreviewPane pane) {
            this.source = source;
            this.pane = pane;
        }

        /**
         * Invoked when an action occurs.
         *
         * @param e
         *          the event.
         */
        public void actionPerformed(final ActionEvent e) {
            final Double selected = (Double) source.getSelectedKey();
            if (selected != null) {
                pane.setZoom(selected.doubleValue());
            }
        }
    }

    private static final double[] ZOOM_FACTORS = { 0.5, 0.75, 1, 1.25, 1.50, 2.00 };

    private static final int DEFAULT_ZOOM_INDEX = 2;
    public static final String STATUS_TEXT_PROPERTY = "statusText"; //$NON-NLS-1$
    public static final String STATUS_TYPE_PROPERTY = "statusType"; //$NON-NLS-1$
    public static final String REPORT_CONTROLLER_PROPERTY = "reportController"; //$NON-NLS-1$
    public static final String ZOOM_PROPERTY = "zoom"; //$NON-NLS-1$
    public static final String CLOSED_PROPERTY = "closed"; //$NON-NLS-1$
    public static final String ERROR_PROPERTY = "error"; //$NON-NLS-1$

    public static final String REPORT_JOB_PROPERTY = "reportJob"; //$NON-NLS-1$
    public static final String PAGINATING_PROPERTY = "paginating"; //$NON-NLS-1$
    public static final String PAGINATED_PROPERTY = "paginated"; //$NON-NLS-1$
    public static final String PAGE_NUMBER_PROPERTY = "pageNumber"; //$NON-NLS-1$
    public static final String NUMBER_OF_PAGES_PROPERTY = "numberOfPages"; //$NON-NLS-1$

    public static final String ICON_THEME_PROPERTY = "iconTheme"; //$NON-NLS-1$
    public static final String TITLE_PROPERTY = "title"; //$NON-NLS-1$
    public static final String MENU_PROPERTY = "menu"; //$NON-NLS-1$

    /**
     * The preferred width key.
     */
    public static final String PREVIEW_PREFERRED_WIDTH = "org.pentaho.reporting.engine.classic.core.modules.gui.base.PreferredWidth"; //$NON-NLS-1$

    /**
     * The preferred height key.
     */
    public static final String PREVIEW_PREFERRED_HEIGHT = "org.pentaho.reporting.engine.classic.core.modules.gui.base.PreferredHeight"; //$NON-NLS-1$

    /**
     * The maximum width key.
     */
    public static final String PREVIEW_MAXIMUM_WIDTH = "org.pentaho.reporting.engine.classic.core.modules.gui.base.MaximumWidth"; //$NON-NLS-1$

    /**
     * The maximum height key.
     */
    public static final String PREVIEW_MAXIMUM_HEIGHT = "org.pentaho.reporting.engine.classic.core.modules.gui.base.MaximumHeight"; //$NON-NLS-1$

    /**
     * The maximum zoom key.
     */
    public static final String ZOOM_MAXIMUM_KEY = "org.pentaho.reporting.engine.classic.core.modules.gui.base.MaximumZoom"; //$NON-NLS-1$

    /**
     * The minimum zoom key.
     */
    public static final String ZOOM_MINIMUM_KEY = "org.pentaho.reporting.engine.classic.core.modules.gui.base.MinimumZoom"; //$NON-NLS-1$

    /**
     * The default maximum zoom.
     */
    private static final float ZOOM_MAXIMUM_DEFAULT = 20.0f; // 2000%

    /**
     * The default minimum zoom.
     */
    private static final float ZOOM_MINIMUM_DEFAULT = 0.01f; // 1%

    private static final String MENUBAR_AVAILABLE_KEY = "org.pentaho.reporting.engine.classic.core.modules.gui.base.MenuBarAvailable"; //$NON-NLS-1$
    private static final String TOOLBAR_AVAILABLE_KEY = "org.pentaho.reporting.engine.classic.core.modules.gui.base.ToolbarAvailable"; //$NON-NLS-1$
    private static final String TOOLBAR_FLOATABLE_KEY = "org.pentaho.reporting.engine.classic.core.modules.gui.base.ToolbarFloatable"; //$NON-NLS-1$

    private Object paginatingDrawable;
    private Object noReportDrawable;
    private PageBackgroundDrawable pageDrawable;

    private PreviewDrawablePanel drawablePanel;
    private ReportController reportController;
    private JMenu[] menus;
    private JToolBar toolBar;
    private String statusText;
    private Throwable error;
    private String title;
    private StatusType statusType;
    private boolean closed;
    private MasterReport reportJob;

    private int numberOfPages;
    private int pageNumber;
    private SwingGuiContext swingGuiContext;
    private IconTheme iconTheme;
    private double zoom;
    private boolean paginating;
    private boolean paginated;

    private PrintReportProcessor printReportProcessor;

    private Worker paginationWorker;
    private JPanel toolbarHolder;
    private JPanel outerReportControllerHolder;
    private boolean reportControllerInner;
    private String reportControllerLocation;
    private JComponent reportControllerComponent;
    private KeyedComboBoxModel<Double, String> zoomModel;
    private PreviewPane.PreviewPaneStatusListener statusListener;
    private static final JMenu[] EMPTY_MENU = new JMenu[0];
    private boolean toolbarFloatable;
    private ArrayList reportProgressListener;

    private double maxZoom;
    private double minZoom;

    private Messages messages;
    private WindowSizeLimiter sizeLimiter;
    private boolean deferredRepagination;
    private ArrayList hyperlinkListeners;
    private transient ReportHyperlinkListener[] cachedHyperlinkListeners;
    private Map<ActionCategory, ActionPlugin[]> actionPlugins;
    private Map<ActionCategory, ZoomAction[]> zoomActions;
    private JComboBox zoomSelectorBox;

    private JScrollPane reportPaneScrollPane;

    private int reportControllerSliderSize;
    private boolean performInitializationRunning;

    /**
     * Creates a new <code>JPanel</code> with a double buffer and a flow layout.
     */
    public PreviewPane() {
        this(true);
    }

    public PreviewPane(final boolean init) {
        messages = new Messages(getLocale(), SwingPreviewModule.BUNDLE_NAME,
                ObjectUtilities.getClassLoader(SwingPreviewModule.class));
        sizeLimiter = new WindowSizeLimiter();
        zoomActions = new HashMap<ActionCategory, ZoomAction[]>();

        this.menus = PreviewPane.EMPTY_MENU;
        setLayout(new BorderLayout());

        zoomModel = new KeyedComboBoxModel();
        zoomModel.setAllowOtherValue(true);
        zoom = PreviewPane.ZOOM_FACTORS[PreviewPane.DEFAULT_ZOOM_INDEX];

        final Configuration configuration = ClassicEngineBoot.getInstance().getGlobalConfig();
        minZoom = getMinimumZoom(configuration);
        maxZoom = getMaximumZoom(configuration);

        pageDrawable = new PageBackgroundDrawable();

        drawablePanel = new PreviewDrawablePanel();
        drawablePanel.setOpaque(false);
        drawablePanel.setBackground(null);
        drawablePanel.setDoubleBuffered(true);
        drawablePanel.setDrawableAsRawObject(pageDrawable);
        drawablePanel.addReportMouseListener(new HyperLinkEventProcessor());

        swingGuiContext = new PreviewGuiContext();

        final JPanel reportPaneHolder = new ScrollablePanel();
        reportPaneHolder.setOpaque(false);
        reportPaneHolder.setBackground(null);
        reportPaneHolder.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
        reportPaneHolder.add(drawablePanel);

        reportPaneScrollPane = new JScrollPane(reportPaneHolder);
        reportPaneScrollPane.getVerticalScrollBar().setUnitIncrement(20);
        reportPaneScrollPane.setBackground(null);
        reportPaneScrollPane.setOpaque(false);
        reportPaneScrollPane.getViewport().setOpaque(false);
        ((JComponent) reportPaneScrollPane.getViewport().getView()).setOpaque(false);

        toolbarHolder = new JPanel();
        toolbarHolder.setLayout(new BorderLayout());

        outerReportControllerHolder = new JPanel();
        outerReportControllerHolder.setOpaque(false);
        outerReportControllerHolder.setBackground(null);
        outerReportControllerHolder.setLayout(new BorderLayout());
        outerReportControllerHolder.add(toolbarHolder, BorderLayout.NORTH);
        outerReportControllerHolder.add(reportPaneScrollPane, BorderLayout.CENTER);
        add(outerReportControllerHolder, BorderLayout.CENTER);

        addPropertyChangeListener(new PreviewUpdateHandler());
        addPropertyChangeListener("zoom", new UpdateZoomHandler()); //$NON-NLS-1$
        statusListener = new PreviewPaneStatusListener();

        zoomSelectorBox = createZoomSelector(this);
        setReportController(new ParameterReportController());
        if (init) {
            initializeWithoutJob();
        }
    }

    protected JComboBox createZoomSelector(final PreviewPane pane) {
        final JComboBox zoomSelect = new JComboBox(pane.getZoomModel());
        zoomSelect.addActionListener(new ZoomSelectAction(pane.getZoomModel(), pane));
        zoomSelect.setAlignmentX(Component.RIGHT_ALIGNMENT);
        return zoomSelect;
    }

    public PreviewDrawablePanel getReportPreviewArea() {
        return drawablePanel;
    }

    public boolean isDeferredRepagination() {
        return deferredRepagination;
    }

    public void setDeferredRepagination(final boolean deferredRepagination) {
        this.deferredRepagination = deferredRepagination;
    }

    public synchronized PrintReportProcessor getPrintReportProcessor() {
        return printReportProcessor;
    }

    protected synchronized void setPrintReportProcessor(final PrintReportProcessor printReportProcessor) {
        this.printReportProcessor = printReportProcessor;
    }

    public JMenu[] getMenu() {
        return menus;
    }

    protected void setMenu(final JMenu[] menus) {
        if (menus == null) {
            throw new NullPointerException();
        }
        final JMenu[] oldmenu = this.menus;
        this.menus = (JMenu[]) menus.clone();
        firePropertyChange(PreviewPane.MENU_PROPERTY, oldmenu, this.menus);
    }

    public JToolBar getToolBar() {
        return toolBar;
    }

    public String getStatusText() {
        return statusText;
    }

    public void setStatusText(final String statusText) {
        final String oldStatus = this.statusText;
        this.statusText = statusText;

        firePropertyChange(PreviewPane.STATUS_TEXT_PROPERTY, oldStatus, statusText);
    }

    public Throwable getError() {
        return error;
    }

    public void setError(final Throwable error) {
        final Throwable oldError = this.error;
        this.error = error;
        firePropertyChange(PreviewPane.ERROR_PROPERTY, oldError, error);
    }

    public StatusType getStatusType() {
        return statusType;
    }

    public void setStatusType(final StatusType statusType) {
        final StatusType oldType = this.statusType;
        this.statusType = statusType;

        firePropertyChange(PreviewPane.STATUS_TYPE_PROPERTY, oldType, statusType);
    }

    public ReportController getReportController() {
        return reportController;
    }

    public void setReportController(final ReportController reportController) {
        final ReportController oldController = this.reportController;
        this.reportController = reportController;
        firePropertyChange(PreviewPane.REPORT_CONTROLLER_PROPERTY, oldController, reportController);

        if (this.reportController != oldController) {
            if (oldController != null) {
                oldController.deinitialize(this);
            }
            // Now add the controller to the GUI ..
            refreshReportController(reportController);
        }
    }

    private void refreshReportController(final ReportController newReportController) {
        for (int i = 0; i < outerReportControllerHolder.getComponentCount(); i++) {
            final Component maybeSplitPane = outerReportControllerHolder.getComponent(i);
            if (maybeSplitPane instanceof JSplitPane) {
                final JSplitPane splitPane = (JSplitPane) maybeSplitPane;
                reportControllerSliderSize = splitPane.getDividerLocation();
                break;
            }
        }

        if (newReportController == null) {
            if (reportControllerComponent != null) {
                // thats relatively easy.
                outerReportControllerHolder.removeAll();
                outerReportControllerHolder.add(toolbarHolder, BorderLayout.NORTH);
                outerReportControllerHolder.add(reportPaneScrollPane, BorderLayout.CENTER);
                reportControllerComponent = null;
                reportControllerInner = false;
                reportControllerLocation = null;
            }
        } else {
            final JComponent rcp = newReportController.getControlPanel();
            if (rcp == null) {
                if (reportControllerComponent != null) {
                    outerReportControllerHolder.removeAll();
                    outerReportControllerHolder.add(toolbarHolder, BorderLayout.NORTH);
                    outerReportControllerHolder.add(reportPaneScrollPane, BorderLayout.CENTER);
                    reportControllerComponent = null;
                    reportControllerInner = false;
                    reportControllerLocation = null;
                }
            } else if (reportControllerComponent != rcp
                    || reportControllerInner != newReportController.isInnerComponent()
                    || ObjectUtilities.equal(reportControllerLocation,
                            newReportController.getControllerLocation()) == false) {
                // if either the controller component or its position (inner vs outer)
                // and border-position has changed, then refresh ..
                this.reportControllerLocation = newReportController.getControllerLocation();
                this.reportControllerInner = newReportController.isInnerComponent();
                this.reportControllerComponent = newReportController.getControlPanel();

                outerReportControllerHolder.removeAll();
                if (reportControllerInner) {
                    final JSplitPane innerHolder = new JSplitPane();
                    innerHolder.setOpaque(false);
                    if (BorderLayout.SOUTH.equals(reportControllerLocation)) {
                        innerHolder.setOrientation(JSplitPane.VERTICAL_SPLIT);
                        innerHolder.setTopComponent(reportPaneScrollPane);
                        innerHolder.setBottomComponent(reportControllerComponent);
                    } else if (BorderLayout.EAST.equals(reportControllerLocation)) {
                        innerHolder.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
                        innerHolder.setLeftComponent(reportPaneScrollPane);
                        innerHolder.setRightComponent(reportControllerComponent);
                    } else if (BorderLayout.WEST.equals(reportControllerLocation)) {
                        innerHolder.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
                        innerHolder.setRightComponent(reportPaneScrollPane);
                        innerHolder.setLeftComponent(reportControllerComponent);
                    } else {
                        innerHolder.setOrientation(JSplitPane.VERTICAL_SPLIT);
                        innerHolder.setBottomComponent(reportPaneScrollPane);
                        innerHolder.setTopComponent(reportControllerComponent);
                    }

                    if (reportControllerSliderSize > 0) {
                        innerHolder.setDividerLocation(reportControllerSliderSize);
                    }
                    outerReportControllerHolder.add(toolbarHolder, BorderLayout.NORTH);
                    outerReportControllerHolder.add(innerHolder, BorderLayout.CENTER);
                } else {
                    final JPanel reportPaneHolder = new JPanel();
                    reportPaneHolder.setOpaque(false);
                    reportPaneHolder.setLayout(new BorderLayout());
                    reportPaneHolder.add(toolbarHolder, BorderLayout.NORTH);
                    reportPaneHolder.add(reportPaneScrollPane, BorderLayout.CENTER);

                    final JSplitPane innerHolder = new JSplitPane();
                    if (BorderLayout.SOUTH.equals(reportControllerLocation)) {
                        innerHolder.setOrientation(JSplitPane.VERTICAL_SPLIT);
                        innerHolder.setTopComponent(reportPaneHolder);
                        innerHolder.setBottomComponent(reportControllerComponent);
                    } else if (BorderLayout.EAST.equals(reportControllerLocation)) {
                        innerHolder.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
                        innerHolder.setLeftComponent(reportPaneHolder);
                        innerHolder.setRightComponent(reportControllerComponent);
                    } else if (BorderLayout.WEST.equals(reportControllerLocation)) {
                        innerHolder.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
                        innerHolder.setRightComponent(reportPaneHolder);
                        innerHolder.setLeftComponent(reportControllerComponent);
                    } else {
                        innerHolder.setOrientation(JSplitPane.VERTICAL_SPLIT);
                        innerHolder.setBottomComponent(reportPaneHolder);
                        innerHolder.setTopComponent(reportControllerComponent);
                    }
                    if (reportControllerSliderSize > 0) {
                        innerHolder.setDividerLocation(reportControllerSliderSize);
                    }
                    outerReportControllerHolder.add(innerHolder, BorderLayout.CENTER);
                }
            }
        }
    }

    public MasterReport getReportJob() {
        return reportJob;
    }

    public void setReportJob(final MasterReport reportJob) {
        final MasterReport oldJob = this.reportJob;
        this.reportJob = reportJob;

        firePropertyChange(PreviewPane.REPORT_JOB_PROPERTY, oldJob, reportJob);

        if (reportJob == null) {
            killThePaginationWorker();
            setPaginated(false);
            setPageNumber(0);
            setNumberOfPages(0);
            refreshReportController(reportController);
            initializeWithoutJob();
        } else {
            refreshReportController(reportController);
            initializeFromReport();
        }

    }

    public double getZoom() {
        return zoom;
    }

    public void setZoom(final double zoom) {
        final double oldZoom = this.zoom;
        this.zoom = Math.max(Math.min(zoom, maxZoom), minZoom);
        if (this.zoom != oldZoom) {
            firePropertyChange(PreviewPane.ZOOM_PROPERTY, oldZoom, zoom);

            updateZoomModel(zoom);
        }
    }

    private void updateZoomModel(final double zoom) {
        for (int i = 0; i < zoomModel.getSize(); i++) {
            final Object o = zoomModel.getKeyAt(i);
            if (o instanceof Double) {
                Double d = (Double) o;
                if (d.doubleValue() == zoom) {
                    zoomModel.setSelectedKey(d);
                    return;
                }
            }
        }
        zoomModel.setSelectedItem(formatZoomText(zoom));
    }

    public boolean isClosed() {
        return closed;
    }

    public void setClosed(final boolean closed) {
        final boolean oldClosed = this.closed;
        this.closed = closed;
        firePropertyChange(PreviewPane.CLOSED_PROPERTY, oldClosed, closed);
        if (closed) {
            prepareShutdown();
        }
    }

    private void prepareShutdown() {
        synchronized (this) {

            paginationWorker = null;

            if (printReportProcessor != null) {
                printReportProcessor.cancel();
                printReportProcessor.close();
                printReportProcessor = null;
            }
            closeToolbar();
        }
    }

    private int getUserDefinedCategoryPosition() {
        return ParserUtil.parseInt(swingGuiContext.getConfiguration().getConfigProperty(
                "org.pentaho.reporting.engine.classic.core.modules.gui.swing.user-defined-category.position"), //$NON-NLS-1$
                15000);
    }

    public Locale getLocale() {
        if (getParent() == null) {
            try {
                return super.getLocale();
            } catch (IllegalComponentStateException ex) {
                return Locale.getDefault();
            }
        }
        return super.getLocale();
    }

    public int getNumberOfPages() {
        return numberOfPages;
    }

    public void setNumberOfPages(final int numberOfPages) {
        final int oldPageNumber = this.numberOfPages;
        this.numberOfPages = numberOfPages;
        firePropertyChange(PreviewPane.NUMBER_OF_PAGES_PROPERTY, oldPageNumber, numberOfPages);
    }

    public int getPageNumber() {
        return pageNumber;
    }

    public void setPageNumber(final int pageNumber) {
        final int oldPageNumber = this.pageNumber;
        this.pageNumber = pageNumber;
        // Log.debug("Setting PageNumber: " + pageNumber);
        firePropertyChange(PreviewPane.PAGE_NUMBER_PROPERTY, oldPageNumber, pageNumber);
    }

    public IconTheme getIconTheme() {
        return iconTheme;
    }

    protected void setIconTheme(final IconTheme theme) {
        final IconTheme oldTheme = this.iconTheme;
        this.iconTheme = theme;
        firePropertyChange(PreviewPane.ICON_THEME_PROPERTY, oldTheme, theme);
    }

    protected void initializeFromReport() {
        final PageDefinition pageDefinition = reportJob.getPageDefinition();
        if (pageDefinition.getPageCount() > 0) {
            final PageFormat pageFormat = pageDefinition.getPageFormat(0);
            pageDrawable.setDefaultWidth((int) pageFormat.getWidth());
            pageDrawable.setDefaultHeight((int) pageFormat.getHeight());
        }

        if (reportJob.getTitle() == null) {
            setTitle(messages.getString("PreviewPane.EMPTY_TITLE")); //$NON-NLS-1$
        } else {
            setTitle(messages.getString("PreviewPane.PREVIEW_TITLE", reportJob.getTitle())); //$NON-NLS-1$
        }

        final Configuration configuration = reportJob.getConfiguration();
        setIconTheme(PreviewPaneUtilities.createIconTheme(configuration));

        performInitialization(configuration);

        if (deferredRepagination == false) {
            startPagination();
        }
    }

    protected void initializeWithoutJob() {
        if (printReportProcessor != null) {
            printReportProcessor.close();
            printReportProcessor = null;
        }

        setTitle(messages.getString("PreviewPane.EMPTY_TITLE")); //$NON-NLS-1$
        final Configuration configuration = ClassicEngineBoot.getInstance().getGlobalConfig();
        setIconTheme(PreviewPaneUtilities.createIconTheme(configuration));
        performInitialization(configuration);
    }

    private synchronized void performInitialization(final Configuration configuration) {
        if (performInitializationRunning) {
            throw new IllegalStateException("This method is not re-entrant");
        }

        try {
            performInitializationRunning = true;
            applyDefinedDimension(configuration);

            final Double key = zoomModel.getSelectedKey();
            zoomModel.clear();
            for (int i = 0; i < PreviewPane.ZOOM_FACTORS.length; i++) {
                zoomModel.add(new Double(PreviewPane.ZOOM_FACTORS[i]), formatZoomText(PreviewPane.ZOOM_FACTORS[i]));
            }
            if (key == null) {
                updateZoomModel(zoom);
            } else {
                zoomModel.setSelectedKey(key);
            }

            if (this.actionPlugins != null) {
                for (final ActionPlugin[] plugins : this.actionPlugins.values()) {
                    for (int i = 0; i < plugins.length; i++) {
                        final ActionPlugin plugin = plugins[i];
                        plugin.deinitialize(swingGuiContext);
                        plugins[i] = null;
                    }
                }
                this.actionPlugins = null;
            }

            this.actionPlugins = PreviewPaneUtilities.loadActions(swingGuiContext);

            if ("true".equals(configuration.getConfigProperty(PreviewPane.MENUBAR_AVAILABLE_KEY))) { //$NON-NLS-1$
                buildMenu();
            } else {
                setMenu(PreviewPane.EMPTY_MENU);
            }

            if (toolBar != null) {
                toolbarHolder.remove(toolBar);
            }
            if ("true".equals(configuration.getConfigProperty(PreviewPane.TOOLBAR_AVAILABLE_KEY))) { //$NON-NLS-1$
                final boolean floatable = isToolbarFloatable()
                        || "true".equals(configuration.getConfigProperty(PreviewPane.TOOLBAR_FLOATABLE_KEY)); //$NON-NLS-1$
                toolBar = buildToolbar(floatable);
            } else {
                toolBar = null;
            }
            if (toolBar != null) {
                toolbarHolder.add(toolBar, BorderLayout.NORTH);
            }
        } finally {
            performInitializationRunning = false;
        }
    }

    /**
     * Read the defined dimensions from the report's configuration and set them to the Dialog. If a maximum size is
     * defined, add a WindowSizeLimiter to check the maximum size
     *
     * @param configuration
     *          the report-configuration of this dialog.
     */
    private void applyDefinedDimension(final Configuration configuration) {
        String width = configuration.getConfigProperty(PreviewPane.PREVIEW_PREFERRED_WIDTH);
        String height = configuration.getConfigProperty(PreviewPane.PREVIEW_PREFERRED_HEIGHT);

        // only apply if both values are set.
        if (width != null && height != null) {
            try {
                final Dimension pref = createCorrectedDimensions(Integer.parseInt(width), Integer.parseInt(height));
                setPreferredSize(pref);
            } catch (Exception nfe) {
                PreviewPane.logger
                        .warn("Preferred viewport size is defined, but the specified values are invalid."); //$NON-NLS-1$
            }
        }

        width = configuration.getConfigProperty(PreviewPane.PREVIEW_MAXIMUM_WIDTH);
        height = configuration.getConfigProperty(PreviewPane.PREVIEW_MAXIMUM_HEIGHT);

        removeComponentListener(sizeLimiter);

        // only apply if at least one value is set.
        if (width != null || height != null) {
            try {
                final int iWidth = (width == null) ? Short.MAX_VALUE : (int) parseRelativeFloat(width);
                final int iHeight = (height == null) ? Short.MAX_VALUE : (int) parseRelativeFloat(height);
                final Dimension pref = createCorrectedDimensions(iWidth, iHeight);
                setMaximumSize(pref);
                addComponentListener(sizeLimiter);
            } catch (Exception nfe) {
                PreviewPane.logger.warn("Maximum viewport size is defined, but the specified values are invalid."); //$NON-NLS-1$
            }
        }
    }

    protected float parseRelativeFloat(final String value) {
        if (value == null) {
            throw new NumberFormatException();
        }
        final String tvalue = value.trim();
        if (tvalue.length() > 0 && tvalue.charAt(tvalue.length() - 1) == '%') { //$NON-NLS-1$
            final String number = tvalue.substring(0, tvalue.length() - 1); //$NON-NLS-1$
            return Float.parseFloat(number) * -1.0f;
        } else {
            return Float.parseFloat(tvalue);
        }
    }

    /**
     * Correct the given width and height. If the values are negative, the height and width is considered a proportional
     * value where -100 corresponds to 100%. The proportional attributes are given is relation to the screen width and
     * height.
     *
     * @param w
     *          the to be corrected width
     * @param h
     *          the height that should be corrected
     * @return the dimension of width and height, where all relative values are normalized.
     */
    private Dimension createCorrectedDimensions(int w, int h) {
        final Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        if (w < 0) {
            w = (w * screenSize.width / -100);
        }
        if (h < 0) {
            h = (h * screenSize.height / -100);
        }
        return new Dimension(w, h);
    }

    /**
     * Gets a list of Actions to add to the toolbar ahead of all other toolbar actions. Left protected for subclasses to
     * override.
     *
     * @return Action[] an array of javax.swing.Action objects
     */
    protected Action[] getToolbarPreActions() {
        return new Action[] {};
    }

    protected JToolBar buildToolbar(final boolean floatable) {
        toolBar = new JToolBar();
        toolBar.setFloatable(floatable);

        final Action[] preActions = getToolbarPreActions();
        if (preActions != null && preActions.length > 0) {
            for (int i = 0; i < preActions.length; i++) {
                toolBar.add(preActions[i]);
            }
            toolBar.addSeparator();
        }

        final ArrayList<ActionPlugin> list = new ArrayList<ActionPlugin>();
        for (final ActionPlugin[] plugins : actionPlugins.values()) {
            list.addAll(Arrays.asList(plugins));
        }
        final ActionPlugin[] plugins = list.toArray(new ActionPlugin[list.size()]);
        Arrays.sort(plugins, new ActionPluginComparator());
        PreviewPaneUtilities.addActionsToToolBar(toolBar, plugins, zoomSelectorBox, this);
        return toolBar;
    }

    public void setToolbarFloatable(final boolean toolbarFloatable) {
        this.toolbarFloatable = toolbarFloatable;
    }

    public boolean isToolbarFloatable() {
        return toolbarFloatable;
    }

    private void closeToolbar() {
        if (toolBar == null) {
            return;
        }
        if (toolBar.getParent() != toolbarHolder) {
            // ha!, we detected that the toolbar is floating ...
            // Log.debug (currentToolbar.getParent());
            final Window w = SwingUtilities.windowForComponent(toolBar);
            if (w != null) {
                w.setVisible(false);
                w.dispose();
            }
        }
        toolBar.setVisible(false);
    }

    public SwingGuiContext getSwingGuiContext() {
        return swingGuiContext;
    }

    public KeyedComboBoxModel<Double, String> getZoomModel() {
        return zoomModel;
    }

    protected final String formatZoomText(final double zoom) {
        final NumberFormat numberFormat = NumberFormat.getPercentInstance(swingGuiContext.getLocale());
        return (numberFormat.format(zoom));
    }

    private void buildMenu() {
        for (final ZoomAction[] zoomActions : this.zoomActions.values()) {
            for (int i = 0; i < zoomActions.length; i++) {
                final ZoomAction zoomAction = zoomActions[i];
                zoomAction.deinitialize();
            }
        }
        this.zoomActions.clear();

        final HashMap<ActionCategory, JMenu> menus = new HashMap<ActionCategory, JMenu>();
        final int userPos = getUserDefinedCategoryPosition();

        boolean insertedUserDefinedActions = false;
        final ArrayList<ActionCategory> collectedCategories = new ArrayList<ActionCategory>();
        for (final Map.Entry<ActionCategory, ActionPlugin[]> entry : actionPlugins.entrySet()) {
            final ActionCategory cat = entry.getKey();
            collectedCategories.add(cat);

            final ActionPlugin[] plugins = entry.getValue();
            if (plugins.length > 0) {
                if (plugins[0] == null) {
                    throw new NullPointerException();
                }
            }

            if (insertedUserDefinedActions == false && cat.getPosition() > userPos) {
                final ReportController controller = getReportController();
                if (controller != null) {
                    controller.initialize(this);
                    final JMenu[] controlerMenus = controller.getMenus();
                    for (int i = 0; i < controlerMenus.length; i++) {
                        final ActionCategory userCategory = new ActionCategory();
                        userCategory.setName("X-User-Category-" + i); //$NON-NLS-1$
                        userCategory.setPosition(userPos + i);
                        userCategory.setUserDefined(true);
                        menus.put(userCategory, controlerMenus[i]);
                        collectedCategories.add(userCategory);
                    }
                }

                insertedUserDefinedActions = true;
            }

            final JMenu menu = PreviewPaneUtilities.createMenu(cat);
            zoomActions.put(cat, PreviewPaneUtilities.buildMenu(menu, plugins, this));
            menus.put(cat, menu);
        }

        final ActionCategory[] categories = collectedCategories
                .toArray(new ActionCategory[collectedCategories.size()]);
        final CategoryTreeItem[] categoryTreeItems = PreviewPaneUtilities.buildMenuTree(categories);

        final ArrayList<CategoryTreeItem> menuList = new ArrayList<CategoryTreeItem>();
        for (int i = 0; i < categoryTreeItems.length; i++) {
            final CategoryTreeItem item = categoryTreeItems[i];
            final JMenu menu = menus.get(item.getCategory());
            // now connect all menus ..
            final CategoryTreeItem[] childs = item.getChilds();
            Arrays.sort(childs);
            for (int j = 0; j < childs.length; j++) {
                final CategoryTreeItem child = childs[j];
                final JMenu childMenu = menus.get(child.getCategory());
                if (childMenu != null) {
                    menu.add(childMenu);
                }
            }

            if (item.getParent() == null) {
                menuList.add(item);
            }
        }

        Collections.sort(menuList);
        final ArrayList<JMenu> retval = new ArrayList<JMenu>();
        for (int i = 0; i < menuList.size(); i++) {
            final CategoryTreeItem item = menuList.get(i);
            final JMenu menu = menus.get(item.getCategory());
            if (item.getCategory().isUserDefined() || menu.getItemCount() > 0) {
                retval.add(menu);
            }
        }

        setMenu(retval.toArray(new JMenu[retval.size()]));
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(final String title) {
        final String oldTitle = this.title;
        this.title = title;
        firePropertyChange(PreviewPane.TITLE_PROPERTY, oldTitle, title);
    }

    public double[] getZoomFactors() {
        return (double[]) PreviewPane.ZOOM_FACTORS.clone();
    }

    public boolean isPaginated() {
        return paginated;
    }

    public void setPaginated(final boolean paginated) {
        final boolean oldPaginated = this.paginated;
        this.paginated = paginated;
        firePropertyChange(PreviewPane.PAGINATED_PROPERTY, oldPaginated, paginated);
    }

    public boolean isPaginating() {
        return paginating;
    }

    public void setPaginating(final boolean paginating) {
        final boolean oldPaginating = this.paginating;
        this.paginating = paginating;
        firePropertyChange(PreviewPane.PAGINATING_PROPERTY, oldPaginating, paginating);
    }

    public synchronized void startPagination() {
        killThePaginationWorker();

        if (printReportProcessor != null) {
            printReportProcessor.close();
            printReportProcessor = null;
        }
        // Reset the pagination to automatic mode now. This way changes to the page-format will trigger
        // pagination again in a safe manor.
        deferredRepagination = false;

        try {
            final MasterReport reportJob = getReportJob();
            printReportProcessor = new PrintReportProcessor(reportJob);

            paginationWorker = createWorker();
            paginationWorker.setWorkload(new RepaginationRunnable(printReportProcessor));
        } catch (ReportProcessingException e) {
            PreviewPane.logger.error("Unable to start report pagination:", e); // NON-NLS
            setStatusType(StatusType.ERROR);
            setStatusText(messages.getString("PreviewPane.ERROR_ON_PAGINATION"));
        }

    }

    private void killThePaginationWorker() {

        if (printReportProcessor != null) {
            printReportProcessor.cancel();
        }

        if (paginationWorker != null) {
            // make sure that old pagination handler does not run longer than
            // necessary..
            // noinspection SynchronizeOnNonFinalField
            final Worker paginationWorker = this.paginationWorker;

            while (paginationWorker.isAvailable() == false && paginationWorker.isFinish() == false) {
                try {
                    synchronized (paginationWorker) {
                        paginationWorker.wait(500);
                    }
                } catch (InterruptedException e) {
                    // Got interrupted while waiting ...
                }
            }
            this.paginationWorker = null;
        }
    }

    protected Worker createWorker() {
        return new Worker();
    }

    public Object getNoReportDrawable() {
        return noReportDrawable;
    }

    public void setNoReportDrawable(final Object noReportDrawable) {
        this.noReportDrawable = noReportDrawable;
    }

    public Object getPaginatingDrawable() {
        return paginatingDrawable;
    }

    public void setPaginatingDrawable(final Object paginatingDrawable) {
        this.paginatingDrawable = paginatingDrawable;
    }

    protected void updateVisiblePage(final int pageNumber) {
        //
        if (printReportProcessor == null) {
            throw new IllegalStateException();
        }

        // todo: This can be very expensive - so we better move this off the event-dispatcher
        final int pageIndex = getPageNumber() - 1;
        if (pageIndex < 0 || pageIndex >= printReportProcessor.getNumberOfPages()) {
            this.pageDrawable.setBackend(null);
            this.drawablePanel.setDrawableAsRawObject(pageDrawable);
        } else {
            final PageDrawable drawable = printReportProcessor.getPageDrawable(pageIndex);
            this.pageDrawable.setBackend(drawable);
            this.drawablePanel.setDrawableAsRawObject(pageDrawable);
        }
    }

    protected StatusListener getStatusListener() {
        return statusListener;
    }

    public void addReportProgressListener(final ReportProgressListener progressListener) {
        if (progressListener == null) {
            throw new NullPointerException();
        }

        if (reportProgressListener == null) {
            reportProgressListener = new ArrayList();
        }
        reportProgressListener.add(progressListener);
    }

    public void removeReportProgressListener(final ReportProgressListener progressListener) {
        if (reportProgressListener == null) {
            return;
        }
        reportProgressListener.remove(progressListener);
    }

    protected void forwardReportStartedEvent(final ReportProgressEvent event) {
        if (reportProgressListener == null) {
            return;
        }
        for (int i = 0; i < reportProgressListener.size(); i++) {
            final ReportProgressListener listener = (ReportProgressListener) reportProgressListener.get(i);
            listener.reportProcessingStarted(event);
        }
    }

    protected void forwardReportUpdateEvent(final ReportProgressEvent event) {
        if (reportProgressListener == null) {
            return;
        }
        for (int i = 0; i < reportProgressListener.size(); i++) {
            final ReportProgressListener listener = (ReportProgressListener) reportProgressListener.get(i);
            listener.reportProcessingUpdate(event);
        }
    }

    protected void forwardReportFinishedEvent(final ReportProgressEvent event) {
        if (reportProgressListener == null) {
            return;
        }
        for (int i = 0; i < reportProgressListener.size(); i++) {
            final ReportProgressListener listener = (ReportProgressListener) reportProgressListener.get(i);
            listener.reportProcessingFinished(event);
        }
    }

    private double getMaximumZoom(final Configuration configuration) {
        final String value = configuration.getConfigProperty(PreviewPane.ZOOM_MAXIMUM_KEY);
        return ParserUtil.parseFloat(value, PreviewPane.ZOOM_MAXIMUM_DEFAULT);
    }

    private double getMinimumZoom(final Configuration configuration) {
        final String value = configuration.getConfigProperty(PreviewPane.ZOOM_MINIMUM_KEY);
        return ParserUtil.parseFloat(value, PreviewPane.ZOOM_MINIMUM_DEFAULT);
    }

    public void addReportHyperlinkListener(final ReportHyperlinkListener listener) {
        if (listener == null) {
            throw new NullPointerException();
        }
        if (hyperlinkListeners == null) {
            hyperlinkListeners = new ArrayList();
        }
        hyperlinkListeners.add(listener);
        cachedHyperlinkListeners = null;
    }

    public void removeReportHyperlinkListener(final ReportHyperlinkListener listener) {
        if (listener == null) {
            throw new NullPointerException();
        }
        if (hyperlinkListeners == null) {
            return;
        }
        hyperlinkListeners.remove(listener);
        cachedHyperlinkListeners = null;
    }

    protected boolean isHyperlinkSystemActive() {
        if (hyperlinkListeners == null) {
            return false;
        }
        return hyperlinkListeners.isEmpty() == false;
    }

    protected void fireReportHyperlinkEvent(final ReportHyperlinkEvent event) {
        if (hyperlinkListeners == null) {
            return;
        }

        if (cachedHyperlinkListeners == null) {
            cachedHyperlinkListeners = (ReportHyperlinkListener[]) hyperlinkListeners
                    .toArray(new ReportHyperlinkListener[hyperlinkListeners.size()]);
        }
        final ReportHyperlinkListener[] myListeners = cachedHyperlinkListeners;
        for (int i = 0; i < myListeners.length; i++) {
            final ReportHyperlinkListener listener = myListeners[i];
            listener.hyperlinkActivated(event);
        }
    }

    protected Configuration computeContextConfiguration() {
        final MasterReport report = getReportJob();
        if (report != null) {
            return report.getConfiguration();
        }
        return ClassicEngineBoot.getInstance().getGlobalConfig();
    }
}