/******************************************************************************* * 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(); } } }