no.resheim.elibrarium.epub.ui.reader.EpubReader.java Source code

Java tutorial

Introduction

Here is the source code for no.resheim.elibrarium.epub.ui.reader.EpubReader.java

Source

/*******************************************************************************
 * Copyright (c) 2011, 2013, 2014 Torkild U. Resheim.
 *
 * All rights reserved. This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License v1.0 which
 * accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Torkild U. Resheim - initial API and implementation
 *******************************************************************************/
package no.resheim.elibrarium.epub.ui.reader;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.util.Date;
import java.util.UUID;

import no.resheim.elibrarium.epub.core.EpubCollection;
import no.resheim.elibrarium.epub.core.EpubUtil;
import no.resheim.elibrarium.epub.ui.EpubUiPlugin;
import no.resheim.elibrarium.library.AnnotationColor;
import no.resheim.elibrarium.library.Book;
import no.resheim.elibrarium.library.Bookmark;
import no.resheim.elibrarium.library.LibraryFactory;
import no.resheim.elibrarium.library.TextAnnotation;
import no.resheim.elibrarium.library.core.ILibraryCatalog;
import no.resheim.elibrarium.library.core.ILibraryCatalog.ITransactionalOperation;
import no.resheim.elibrarium.library.core.Librarian;
import no.resheim.elibrarium.library.core.LibraryUtil;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.emf.common.notify.Adapter;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.emf.ecore.util.FeatureMap;
import org.eclipse.emf.ecore.util.FeatureMapUtil.FeatureEList;
import org.eclipse.emf.ecore.xml.type.XMLTypePackage;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.mylyn.docs.epub.core.EPUB;
import org.eclipse.mylyn.docs.epub.core.Publication;
import org.eclipse.mylyn.docs.epub.dc.Title;
import org.eclipse.mylyn.docs.epub.ncx.NavPoint;
import org.eclipse.mylyn.docs.epub.opf.Item;
import org.eclipse.mylyn.docs.epub.opf.Itemref;
import org.eclipse.mylyn.docs.epub.opf.Reference;
import org.eclipse.mylyn.docs.epub.opf.Type;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.BrowserFunction;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.ProgressListener;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.EditorPart;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;

/**
 *
 * @author Torkild U. Resheim
 */
public class EpubReader extends EditorPart {

    /**
     * The direction of browsing.
     */
    private enum Direction {
        /** Browse backwards */
        BACKWARD,
        /** Browser forwards */
        FORWARD,
        /** Initial state */
        INITIAL,
        /** Browse chapter */
        LOCATION,
        /** Browse to specific page */
        PAGE
    }

    /**
     * Handles text selections. The function(Object[]) method is called from
     * JavaScript executing in the browser when the user releases the mouse
     * button.
     */
    private class MarkTextHandler extends BrowserFunction {

        public MarkTextHandler(Browser browser) {
            super(browser, "javaMarkTextHandler");
        }

        @Override
        public Object function(Object[] arguments) {

            currentColor = null;
            currentRange = null;
            currentText = null;

            final String range = (String) arguments[0];
            final String text = (String) arguments[1];
            if (text.length() > 0) {
                currentRange = range;
                currentText = text;
            }
            if ((Boolean) arguments[2]) {
                currentColor = AnnotationColor.YELLOW;
            }
            return super.function(arguments);
        }

    }

    /**
     * Performs selection of text in the browser.
     */
    private class MarkTextMenuItem {

        public MenuItem menuItem;

        public MarkTextMenuItem(Menu parent, int style) {
            menuItem = new MenuItem(parent, style);
            menuItem.setText("Mark text");
            installListener();
        }

        private void installListener() {
            menuItem.addSelectionListener(new SelectionListener() {

                @Override
                public void widgetDefaultSelected(SelectionEvent e) {
                }

                @Override
                public void widgetSelected(SelectionEvent e) {
                    toggleTextMarking();
                }
            });
        }

    }

    /**
     * Listens to the pagination job and updates labels when it is done.
     */
    private class PaginationJobListener extends JobChangeAdapter {

        @Override
        public void done(IJobChangeEvent event) {
            updateLocation();
        }

        @Override
        public void running(IJobChangeEvent event) {
            updateLocation();
        }

    }

    private class RemoveMarkedItem {

        public MenuItem menuItem;

        public RemoveMarkedItem(Menu parent, int style) {
            menuItem = new MenuItem(parent, style);
            menuItem.setText("Remove");
            installListener();
        }

        private void installListener() {
            menuItem.addSelectionListener(new SelectionListener() {

                @Override
                public void widgetDefaultSelected(SelectionEvent e) {
                }

                @Override
                public void widgetSelected(SelectionEvent e) {
                    // XXX: Does not work!
                    // if (browser.execute("unmarkRange('" + currentRange +
                    // "');")) {
                    // } else {
                    // System.err.println("Could not remove mark");
                    // }
                }
            });
        }

    }

    /**
     * Listens to changes in the browser widget's size and starts paginating the
     * current chapter and the entire book 500ms after the last resize event.
     */
    private class ResizeListener implements ControlListener, Runnable, Listener {

        private static final int RESIZE_DELAY = 500;

        private long lastEvent = 0;

        private boolean mouse = true;

        public void controlMoved(ControlEvent e) {
        }

        public void controlResized(ControlEvent e) {
            lastEvent = System.currentTimeMillis();
            Display.getDefault().timerExec(RESIZE_DELAY, this);
        }

        @Override
        public void handleEvent(Event event) {
            mouse = event.type == SWT.MouseUp;
        }

        private void paginate() {
            if (closing) {
                return;
            }
            int x = browser.getSize().x;
            int y = browser.getSize().y;
            if (x != lastWidth && y != lastHeight) {
                paginateChapter();
                paginationJob.update(x, y);
                lastWidth = x;
                lastHeight = y;
            }
        }

        @Override
        public void run() {
            if ((lastEvent + RESIZE_DELAY) < System.currentTimeMillis() && mouse) {
                paginate();
            } else {
                Display.getDefault().timerExec(RESIZE_DELAY, this);
            }
        }

    }

    protected static final String PROPERTY_TITLE = "title"; //$NON-NLS-1$

    private static final EStructuralFeature TEXT = XMLTypePackage.eINSTANCE.getXMLTypeDocumentRoot_Text();

    public static final String WEB_BROWSER_EDITOR_ID = "org.eclipse.ui.browser.editor"; //$NON-NLS-1$

    protected Browser browser;

    /** The current anchor (may be null) */
    private String currentAnchor;

    private Book currentBook;

    private AnnotationColor currentColor;

    /** The current href, excluding the anchor */
    private String currentHref;

    /**
     * The current location  for use in page bookmarks. This is specified in
     * Rangy format
     */
    private String currentLocation;

    private String currentRange;

    private String currentText;

    /** Used to navigate to a certain page */
    private Direction direction = Direction.INITIAL;

    private boolean disposed;

    private Label headerLabel;

    protected String initialURL;

    private Label footerLabel;

    int lastHeight;

    int lastWidth;

    private Menu menu;

    private Publication ops;

    private Object outlinePage;

    /** Page to navigate to if direction is PAGE */
    private int page;

    private int pageCount;

    private int pageWidth;

    PaginationJob paginationJob;

    boolean paginationRequired = true;

    private ResizeListener resizeListener;

    private boolean closing;

    private Label bookmarkLabel;

    public EpubReader() {
        super();
        utility = new EpubUiUtility();
    }

    /**
     * Close the editor correctly.
     */
    public boolean close() {
        closing = true;
        final boolean[] result = new boolean[1];
        Display.getDefault().asyncExec(new Runnable() {
            public void run() {
                result[0] = getEditorSite().getPage().closeEditor(EpubReader.this, false);
            }
        });
        return result[0];
    }

    /*
     * Creates the SWT controls for this workbench part.
     */
    @Override
    public void createPartControl(Composite parent) {
        Composite c = new Composite(parent, SWT.NONE);
        c.setBackground(JFaceResources.getColorRegistry().get(JFacePreferences.ERROR_COLOR));
        GridLayout layout = new GridLayout();
        layout.numColumns = 2;
        layout.marginTop = 0;
        c.setLayout(layout);
        c.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));

        GridData gdHeader = new GridData(SWT.CENTER, SWT.TOP, true, false);
        gdHeader.minimumWidth = 500;
        headerLabel = new Label(c, SWT.CENTER);
        headerLabel.setLayoutData(gdHeader);
        headerLabel.setText(" ");
        headerLabel.setForeground(JFaceResources.getColorRegistry().get(JFacePreferences.QUALIFIER_COLOR));

        GridData gdBookmark = new GridData(SWT.CENTER, SWT.BEGINNING, false, false);
        gdBookmark.minimumWidth = 32;
        gdBookmark.widthHint = 32;
        gdBookmark.verticalSpan = 3;
        bookmarkLabel = new Label(c, SWT.CENTER);
        bookmarkLabel
                .setImage(EpubUiPlugin.getDefault().getImageRegistry().get(EpubUiPlugin.IMG_BOOKMARK_INACTIVE));
        bookmarkLabel.setLayoutData(gdBookmark);
        bookmarkLabel.addMouseListener(new MouseListener() {

            @Override
            public void mouseUp(MouseEvent e) {
            }

            @Override
            public void mouseDown(MouseEvent e) {
                toggleBookmark();
            }

            @Override
            public void mouseDoubleClick(MouseEvent e) {
            }
        });

        // We rely on having a WebKit based browser
        GridData gdBrowser = new GridData(SWT.FILL, SWT.FILL, true, true);
        browser = new Browser(c, SWT.WEBKIT);
        browser.setLayoutData(gdBrowser);
        browser.setBackground(JFaceResources.getColorRegistry().get(JFacePreferences.ERROR_COLOR));

        GridData gdFooter = new GridData(SWT.CENTER, SWT.BOTTOM, true, false);
        gdFooter.minimumWidth = 500;
        footerLabel = new Label(c, SWT.CENTER);
        gdFooter.horizontalSpan = 2;
        footerLabel.setLayoutData(gdFooter);
        footerLabel.setText(" ");
        footerLabel.setForeground(JFaceResources.getColorRegistry().get(JFacePreferences.QUALIFIER_COLOR));

        // Install listener to figure out when we need to re-paginate
        resizeListener = new ResizeListener();
        browser.addControlListener(resizeListener);
        browser.getDisplay().addFilter(SWT.MouseDown, resizeListener);
        browser.getDisplay().addFilter(SWT.MouseUp, resizeListener);

        // Various javascript
        installInjector();

        // Handle key-presses
        installKeyListener();

        // These events are triggered when the location of the browser has
        // changed, that is when the URL has changed. It may or may not be
        // within the document currently open. It does normally not change when
        // browsing between pages unless the chapter has been changed.
        // browser.addLocationListener(new LocationListener() {
        //
        // @Override
        // public void changed(LocationEvent event) {
        // updateLocation();
        // }
        //
        // @Override
        // public void changing(LocationEvent event) {
        // }
        // });

        browser.setCapture(false);
        if (browser != null) {
            if (initialURL != null) {
                browser.setUrl(initialURL);
            }
        }

        new MarkTextHandler(browser);

        // Create a new menu for the browser. We want this dynamically populated
        // so all items are removed when the menu is shown and it will be
        // re-populated.
        menu = new Menu(browser);
        browser.setMenu(menu);
        menu.addListener(SWT.Show, new Listener() {
            public void handleEvent(Event event) {
                MenuItem[] menuItems = menu.getItems();
                for (MenuItem menuItem : menuItems) {
                    menuItem.dispose();
                }
                populateMenu();
            }
        });

    }

    private boolean deleteFolder(File folder) {
        if (folder.isDirectory()) {
            String[] children = folder.list();
            for (String element : children) {
                boolean ok = deleteFolder(new File(folder, element));
                if (!ok) {
                    return false;
                }
            }
        }
        return folder.delete();
    }

    @Override
    public void dispose() {
        if (paginationJob != null) {
            paginationJob.cancel();
        }
        // Store the last location
        if (currentBook != null) {
            ILibraryCatalog.INSTANCE.modify(currentBook, new ITransactionalOperation<Book>() {
                @Override
                public Object execute(Book object) {
                    object.cdoWriteLock().lock();
                    object.setLastLocation(currentLocation);
                    object.setLastHref(currentHref);
                    object.cdoWriteLock().unlock();
                    return null;
                }
            });
        }
        disposed = true;
        super.dispose();
    }

    @Override
    public void doSave(IProgressMonitor monitor) {
        // do nothing
    }

    @Override
    public void doSaveAs() {
        // do nothing
    }

    public IActionBars getActionBars() {
        return getEditorSite().getActionBars();
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Object getAdapter(Class required) {
        if (IContentOutlinePage.class.equals(required)) {
            if (outlinePage == null) {
                outlinePage = new TOCOutlinePage(ops, this);
            }
            return outlinePage;
        }
        return super.getAdapter(required);
    }

    /**
     * Determines the chapter number of the item currently displayed. If
     * <b>-1</b> is returned, the item is not in the spine.
     *
     * @return the current chapter, starting from chapter one.
     */
    private int getCurrentChapter() {
        int currentChapter = -1;
        EList<Itemref> spineItems = ops.getPackage().getSpine().getSpineItems();
        EList<Item> items = ops.getPackage().getManifest().getItems();
        String ref = currentHref;
        for (Item item : items) {
            if (item.getHref().equals(ref)) {
                for (int c = 0; c < spineItems.size(); c++) {
                    if (spineItems.get(c).getIdref().equals(item.getId())) {
                        currentChapter = c + 1;
                        break;
                    }
                }
            }
        }
        return currentChapter;
    }

    /**
     * Determines the current page in the chapter using the content viewport
     * position. Since this offset may not be exactly the same as the start of
     * the page we need to do a bit of calculation.
     *
     * @return
     */
    private int getCurrentChapterPage() {
        int page = 1;
        int offset = (int) Math.round((Double) browser
                .evaluate("bodyID = document.getElementsByTagName('body')[0];return bodyID.scrollLeft"));
        if (offset != 0) {
            for (int i = 0; i < pageCount; i++) {
                if (offset <= i * pageWidth) {
                    page = i + 1;
                    break;
                }
            }
        }
        return page;
    }

    /**
     * Calculates the book relative page index of the currently displayed page.
     * Note that the index of the first page is "0".
     *
     * @return the currently displayed page index
     */
    private int getCurrentPageIndex() {
        return getCurrentChapterPage() + getCurrentChapterPageIndex();
    }

    /**
     * Calculates the book relative page index at the beginning of the chapter -
     * using information of chapter lengths obtained from the pagination job.
     * Note that the index of the first page is
     *
     * @return the page number at the start of the chapter
     */
    private int getCurrentChapterPageIndex() {
        int page = 0;
        int chapter = getCurrentChapter();
        int[] chapterSizes = paginationJob.getChapterSizes();
        if (chapterSizes.length < chapter) {
            return 0;
        }
        for (int i = 0; i < chapter - 1; i++) {
            page += chapterSizes[i];
        }
        return page;
    }

    /**
     * Returns the URL of the first text page of the publication. That is
     * excluding any cover page etc.
     *
     * @return URL of the first text page
     */
    private String getOpeningPage(String href) {
        if (href != null) {
            return "file:" + ops.getRootFolder().getAbsolutePath() + File.separator + href;
        }
        // First try the first TEXT type page if there is a guide.
        if (ops.getPackage().getGuide() != null) {
            EList<Reference> references = ops.getPackage().getGuide().getGuideItems();
            for (Reference reference : references) {
                if (reference.getType().equals(Type.TEXT)) {
                    return "file:" + ops.getRootFolder().getAbsolutePath() + File.separator + reference.getHref();
                }
            }
        }
        // Then try the first page in the spine
        EList<Itemref> items = ops.getPackage().getSpine().getSpineItems();
        for (Itemref itemref : items) {
            if (itemref.getLinear() == null || Boolean.parseBoolean(itemref.getLinear())) {
                Item item = ops.getItemById(itemref.getIdref());
                return "file:" + ops.getRootFolder().getAbsolutePath() + File.separator + item.getHref();
            }
        }
        return null;
    }

    @SuppressWarnings("rawtypes")
    public String getTitle(Publication epub) {
        EList<Title> titles = epub.getPackage().getMetadata().getTitles();
        if (titles.size() > 0) {
            FeatureMap fm = titles.get(0).getMixed();
            Object o = fm.get(TEXT, false);
            if (o instanceof FeatureEList) {
                if (((FeatureEList) o).size() > 0) {
                    return ((FeatureEList) o).get(0).toString();
                }
            }
        }
        return "";
    }

    @Override
    public void init(IEditorSite site, IEditorInput input) throws PartInitException {
        if (input instanceof IPathEditorInput) {
            IPathEditorInput pei = (IPathEditorInput) input;
            IPath path = pei.getPath();
            EPUB epub = new EPUB();
            try {
                // If the EPUB has already been unpacked it's contents will be
                // used as it is unless the modification dates differ.
                IPath storageLocation = Librarian.getDefault().getStorageLocation();
                File rootFolder = storageLocation.append(path.lastSegment()).toFile();
                if (rootFolder.lastModified() != path.toFile().lastModified()) {
                    deleteFolder(rootFolder);
                }
                epub.unpack(path.toFile(), rootFolder);
                // Use the first OPS publication we find
                ops = epub.getOPSPublications().get(0);
                registerBook(path, ops);
                installBookListener(currentBook);
                initialURL = getOpeningPage(currentBook.getLastHref());
                currentLocation = currentBook.getLastLocation();
                setPartName(getTitle(ops));
                paginationJob = new PaginationJob(currentBook, ops);
                paginationJob.setUser(false);
                paginationJob.setPriority(Job.LONG);
                paginationJob.addJobChangeListener(new PaginationJobListener());
                //
                ILibraryCatalog.INSTANCE.modify(currentBook, new ITransactionalOperation<Book>() {
                    @Override
                    public Object execute(Book object) {
                        object.cdoWriteLock().lock();
                        object.setLastOpened(System.currentTimeMillis());
                        object.cdoWriteLock().unlock();
                        return null;
                    }
                });
            } catch (Exception e) {
                StatusManager.getManager().handle(
                        new Status(IStatus.ERROR, EpubUiPlugin.PLUGIN_ID, "Could not open book", e),
                        StatusManager.SHOW);
                close();
            }

        } else {
            IPathEditorInput pinput = (IPathEditorInput) input.getAdapter(IPathEditorInput.class);
            if (pinput != null) {
                init(site, pinput);
            } else {
                throw new PartInitException(NLS.bind("Invalid editor input", input.getName()));
            }
        }

        setSite(site);
        setInput(input);
    }

    /**
     * Install a listener that will respond to changes in the book.
     *
     * @param book
     *            the book to listen to
     */
    private void installBookListener(Book book) {
        Adapter adapter = new AdapterImpl() {
            @Override
            public void notifyChanged(Notification notification) {
                if (notification.getFeature() instanceof EReference) {
                    EReference ref = (EReference) notification.getFeature();
                    if (ref.getName().equals("annotations")) {
                        // An annotation has been removed. Due to difficulties
                        // simply removing the marking using JavaScript we
                        // reload the URL and set the page to the current page.
                        // That will refresh the contents, although cause a bit
                        // of flickering.
                        if (notification.getEventType() == Notification.REMOVE) {
                            page = getCurrentChapterPage();
                            direction = Direction.PAGE;
                            browser.setUrl(browser.getUrl());
                        }
                    }
                }
            }
        };
        book.eAdapters().add(adapter);
    }

    private void installInjector() {
        browser.addProgressListener(new ProgressListener() {
            @Override
            public void changed(ProgressEvent event) {
            }

            @Override
            public void completed(ProgressEvent event) {
                // Ignore this one.
                if (browser.getUrl().equals("about:blank")) {
                    return;
                }

                // Detect the current href and anchor
                String url = browser.getUrl().substring(browser.getUrl().lastIndexOf('/') + 1);
                if (url.indexOf('#') > -1) {
                    currentHref = url.substring(0, url.indexOf('#'));
                    currentAnchor = url.substring(url.indexOf('#') + 1);
                } else {
                    currentHref = url;
                    currentAnchor = null;
                }

                // Do the pagination of the chapter
                paginateChapter();

                // Iterate over all bookmarks and annotations and insert markers
                // into the HTML code. Also update page numbers.
                EList<Bookmark> bookmarks = currentBook.getBookmarks();
                for (final Bookmark bookmark : bookmarks) {
                    if (bookmark.getHref() != null && bookmark.getHref().equals(currentHref)) {
                        String id = bookmark.getId();
                        if (bookmark instanceof TextAnnotation) {
                            // Mark text
                            if (!browser.execute("markRange('" + bookmark.getLocation() + "','" + id + "');")) {
                            }
                        } else {
                            // Mark page
                            browser.evaluate("injectIdentifier('" + bookmark.getLocation() + "','" + id + "');");
                        }
                    }
                }

                // Navigate to a a certain page.
                switch (direction) {
                case INITIAL:
                    // Navigates to the given location.
                    if (currentLocation != null) {
                        browser.execute("navigateToLocation('" + currentLocation + "');");
                        updateLocation();
                    } else {
                        navigateToPage(1);
                    }
                    break;
                case LOCATION:
                    // Navigates to the location of the current anchor
                    if (currentAnchor != null) {
                        browser.execute("setOffsetToElement('" + currentAnchor + "')");
                        updateLocation();
                    }
                    break;
                case FORWARD:
                    navigateToPage(1);
                    break;
                case BACKWARD:
                    navigateToPage(pageCount);
                    break;
                case PAGE:
                    navigateToPage(page);
                    break;
                default:
                    break;
                }

                // Reset direction
                // XXX: Do we need this?
                direction = Direction.INITIAL;

                // Size may be 0,0 when the view is first opened so we want to
                // delay until the browser is resized.
                if (paginationRequired && browser.getSize().x > 0 && browser.getSize().y > 0) {
                    paginationJob.update(browser.getSize().x, browser.getSize().y);
                    paginationRequired = false;
                }
            }
        });
    }

    /**
     * Installs key handling for the reader. Changes behaviour of so that the
     * left arrow key browser left and the right arrow key browsers right.
     */
    private void installKeyListener() {
        browser.addKeyListener(new KeyListener() {

            @Override
            public void keyPressed(KeyEvent e) {
                if (e.keyCode == SWT.ARROW_RIGHT) {
                    e.doit = false;
                }
                if (e.keyCode == SWT.ARROW_LEFT) {
                    e.doit = false;
                }
            }

            @Override
            public void keyReleased(KeyEvent e) {
                if (e.keyCode == SWT.ARROW_RIGHT) {
                    nextPage();
                    e.doit = false;
                }
                if (e.keyCode == SWT.ARROW_LEFT) {
                    previousPage();
                    e.doit = false;
                }
            }
        });
    }

    /*
     * (non-Javadoc) Returns whether the contents of this editor have changed
     * since the last save operation.
     */
    @Override
    public boolean isDirty() {
        return false;
    }

    /*
     * (non-Javadoc) Returns whether the "save as" operation is supported by
     * this editor.
     */
    @Override
    public boolean isSaveAsAllowed() {
        return false;
    }

    /**
     * Browses to the next or previous chapter depending on the direction.
     *
     * @param direction
     *            the browsing direction.
     */
    private void navigateChapter(Direction direction) {
        this.direction = direction;
        int currentChapter = getCurrentChapter();
        switch (direction) {
        case FORWARD:
            currentChapter++;
            break;
        case BACKWARD:
            currentChapter--;
            break;
        default:
            break;
        }
        EList<Itemref> spineItems = ops.getPackage().getSpine().getSpineItems();
        if (currentChapter > 0 && currentChapter <= spineItems.size()) {
            Item item = ops.getItemById(spineItems.get(currentChapter - 1).getIdref());
            openItem(item);
        }
    };

    /**
     * Navigates to the given bookmark. When a chapter is loaded all bookmarks
     * are handled and identifying elements are created in the document.
     *
     * @param bookmark
     *            the marker to navigate to
     */
    public void navigateTo(Bookmark bookmark) {
        String ref = bookmark.getHref();
        String id = bookmark.getId();
        if (id != null) {
            ref = ref + "#" + id;
        }
        navigateTo(ref);
    }

    /**
     * Use to navigate to a specific {@link NavPoint}.
     *
     * @param navPoint
     *            the point to navigate to
     */
    public void navigateTo(NavPoint navPoint) {
        String url = navPoint.getContent().getSrc();
        navigateTo(url);
    }

    /**
     *
     * @param url
     */
    private void navigateTo(String url) {
        System.out.println("Navigating to: " + url);
        try {
            String newRef = url;
            String newAnchor = null;
            if (url.indexOf('#') > -1) {
                newRef = url.substring(0, url.indexOf('#'));
                newAnchor = url.substring(url.indexOf('#') + 1);
            }
            // Navigate to a specific location
            direction = Direction.LOCATION;
            // Load a new XHTML file
            if (!currentHref.equals(newRef)) {
                // New chapter which must be loaded
                String path = "file:" + ops.getRootFolder().getAbsolutePath() + File.separator + url;
                browser.setUrl(path);
            } else {
                // Browse to the anchor or the first page
                if (newAnchor != null) {
                    currentAnchor = newAnchor;
                    browser.execute("setOffsetToElement('" + currentAnchor + "')");
                    updateLocation();
                } else {
                    navigateToPage(1);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Adds a bookmark at the current location unless there is already one
     * present. If there is a bookmark already present it will be removed.
     */
    private void toggleBookmark() {
        final Bookmark existing = hasBookmark();
        if (existing == null) {
            String title = (String) browser.evaluate("title = getBookmarkTitle();return title;");
            final Bookmark bookmark = LibraryFactory.eINSTANCE.createBookmark();
            String id = UUID.randomUUID().toString();
            bookmark.setId(id);
            bookmark.setTimestamp(new Date());
            bookmark.setHref(currentHref);
            bookmark.setLocation(currentLocation);
            bookmark.setText(title);
            int bookmarkPage = (int) Math.round((Double) browser
                    .evaluate("page=injectIdentifier('" + currentLocation + "','" + id + "');return page;"));
            bookmarkPage = getCurrentChapterPageIndex() + bookmarkPage + 1;
            bookmark.setPage(bookmarkPage);
            // Wrap the operation of adding a bookmark into a transaction
            ILibraryCatalog.INSTANCE.modify(currentBook, new ITransactionalOperation<Book>() {
                @Override
                public Object execute(Book object) {
                    object.cdoWriteLock().lock();
                    object.getBookmarks().add(bookmark);
                    object.cdoWriteLock().unlock();
                    return null;
                }
            });
        } else {
            // Wrap the operation of adding a bookmark into a transaction
            ILibraryCatalog.INSTANCE.modify(currentBook, new ITransactionalOperation<Book>() {
                @Override
                public Object execute(Book object) {
                    object.cdoWriteLock().lock();
                    object.getBookmarks().remove(existing);
                    object.cdoWriteLock().unlock();
                    return null;
                }
            });
        }
        updateLocation();
    }

    private void toggleTextMarking() {
        String id = UUID.randomUUID().toString();
        final TextAnnotation annotation = LibraryFactory.eINSTANCE.createTextAnnotation();
        annotation.setId(id);
        annotation.setTimestamp(new Date());
        annotation.setColor(AnnotationColor.YELLOW);
        annotation.setHref(currentHref);
        annotation.setLocation(currentRange);
        annotation.setText(currentText);
        int annotationPage = (int) Math.round(
                (Double) browser.evaluate("page=markRange('" + currentRange + "','" + id + "');return page;"));
        annotationPage = getCurrentChapterPageIndex() + annotationPage + 1;
        annotation.setPage(annotationPage);

        // Wrap the operation of adding a bookmark into a transaction
        ILibraryCatalog.INSTANCE.modify(currentBook, new ITransactionalOperation<Book>() {
            @Override
            public Object execute(Book object) {
                object.cdoWriteLock().lock();
                object.getBookmarks().add(annotation);
                object.cdoWriteLock().unlock();
                return null;
            }
        });
    }

    private Bookmark hasBookmark() {
        Bookmark marked = null;
        EList<Bookmark> bookmarks = currentBook.getBookmarks();
        for (Bookmark bookmark : bookmarks) {
            if (bookmark.getHref() != null && bookmark.getHref().equals(currentHref)) {
                // Only looking for page bookmarks
                if (!(bookmark instanceof TextAnnotation)) {
                    Boolean intersects = (Boolean) browser
                            .evaluate("bookmark = intersects('" + bookmark.getLocation() + "');return bookmark;");
                    if (intersects) {
                        marked = bookmark;
                    }
                }
            }
        }
        return marked;
    }

    public void updateLocation() {
        if (!disposed) {
            browser.getDisplay().syncExec(new Runnable() {
                @Override
                public void run() {
                    // See issue 28
                    try {
                        // Determine the current location so that it can be
                        // restored when reopening the book at a later stage.
                        String location = (String) browser
                                .evaluate("bookmark = getPageLocation();return bookmark;");
                        currentLocation = location;
                        Bookmark b = hasBookmark();
                        if (b != null) {
                            bookmarkLabel.setImage(EpubUiPlugin.getDefault().getImageRegistry()
                                    .get(EpubUiPlugin.IMG_BOOKMARK_ACTIVE));
                            bookmarkLabel.setToolTipText(b.getText() + "\n(Click to remove bookmark)");
                        } else {
                            bookmarkLabel.setImage(EpubUiPlugin.getDefault().getImageRegistry()
                                    .get(EpubUiPlugin.IMG_BOOKMARK_INACTIVE));
                            bookmarkLabel.setToolTipText("Click to add bookmark");
                        }
                    } catch (Exception e) {
                        System.err.println("Could not determine current location.");
                    }
                    // Set the title
                    String title = (String) browser.evaluate("title = getChapterTitle();return title;");
                    // Fake a small caps effect.
                    title = title.toUpperCase();
                    StringReader sr = new StringReader(title);
                    StringBuilder sb = new StringBuilder();
                    int c = -1;
                    try {
                        while ((c = sr.read()) > -1) {
                            sb.append((char) c);
                            sb.append(' ');
                        }
                        headerLabel.setText(sb.toString());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

            });
            // Update page number
            footerLabel.getDisplay().asyncExec(new Runnable() {
                @Override
                public void run() {
                    synchronized (paginationJob) {
                        if (paginationJob.getState() == Job.NONE) {
                            footerLabel.setText(
                                    "Page " + getCurrentPageIndex() + " of " + paginationJob.getTotalpages());
                        } else {
                            footerLabel.setText("Paginating...");
                        }
                    }
                }
            });
        }
    }

    /**
     * Navigates to the given page number of the chapter.
     *
     * @param page
     *            the page number to go to
     */
    private void navigateToPage(int page) {
        EpubUiUtility.navigateToPage(browser, page);
        updateLocation();
    }

    /**
     * Browse to the next page in the reading order. If already on the last page
     * of the chapter, the first page of the next chapter will be shown.
     */
    public void nextPage() {
        int page = getCurrentChapterPage();
        if (page < pageCount) {
            navigateToPage(++page);
        } else if (page >= pageCount) {
            navigateChapter(Direction.FORWARD);
        }
    }

    public void openInExternalBrowser(String url) {
        throw new RuntimeException("Not implemented");
    }

    private void openItem(Item item) {
        String url = "file:" + ops.getRootFolder().getAbsolutePath() + File.separator + item.getHref();
        browser.setUrl(url);
        setPartName(getTitle(ops));
    }

    private final EpubUiUtility utility;

    /**
     * Executes JavaScript that will reformat the chapter and obtain information
     * that is required for browsing it.
     */
    private void paginateChapter() {
        try {
            boolean ok = utility.injectJavaScript(browser);
            if (ok) {
                pageCount = (int) Math.round((Double) browser.evaluate("return pageCount"));
                pageWidth = EpubUiUtility.getPageWidth(browser);
                lastWidth = browser.getSize().x;
                lastHeight = browser.getSize().y;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void populateMenu() {
        if (currentColor != null) {
            new RemoveMarkedItem(menu, SWT.PUSH);
        } else if (currentRange != null) {
            // Allow text to be marked
            new MarkTextMenuItem(menu, SWT.PUSH);
        }
    }

    /**
     * Browse to the previous page in the reading order. If already on the first
     * page of the chapter, the last page of the previous chapter will be shown.
     */
    public void previousPage() {
        int page = getCurrentChapterPage();
        if (page > 1) {
            navigateToPage(--page);
        } else {
            navigateChapter(Direction.BACKWARD);
        }
    }

    private void registerBook(IPath path, Publication ops) {
        String title = EpubUtil.getFirstTitle(ops);
        String author = EpubUtil.getFirstAuthor(ops);
        String id = EpubUtil.getIdentifier(ops);
        if (!EpubCollection.getCollection().hasBook(id)) {
            URI uri = path.toFile().toURI();
            currentBook = LibraryUtil.createNewBook(EpubCollection.COLLECTION_ID, uri, id, title, author);
            EpubCollection.getCollection().add(currentBook);
        } else {
            currentBook = EpubCollection.getCollection().getBook(id);
        }
    }

    /*
     * Asks this part to take focus within the workbench.
     */
    @Override
    public void setFocus() {
        if (browser != null) {
            browser.setFocus();
        }
    }

}