/*
* PDFViewer.java
*
* Copyright (C) 2009-12 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.pdfviewer;
import org.rstudio.core.client.Point;
import org.rstudio.core.client.dom.WindowEx;
import org.rstudio.core.client.widget.Operation;
import org.rstudio.core.client.widget.OperationWithInput;
import org.rstudio.studio.client.application.Desktop;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.application.model.ApplicationServerOperations;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.GlobalDisplay.NewWindowOptions;
import org.rstudio.studio.client.common.compilepdf.events.CompilePdfCompletedEvent;
import org.rstudio.studio.client.common.compilepdf.model.CompilePdfResult;
import org.rstudio.studio.client.common.satellite.SatelliteManager;
import org.rstudio.studio.client.common.satellite.events.WindowOpenedEvent;
import org.rstudio.studio.client.common.synctex.Synctex;
import org.rstudio.studio.client.common.synctex.events.SynctexViewPdfEvent;
import org.rstudio.studio.client.common.synctex.model.PdfLocation;
import org.rstudio.studio.client.pdfviewer.events.LookupSynctexSourceEvent;
import org.rstudio.studio.client.pdfviewer.model.PdfJsWindow;
import org.rstudio.studio.client.pdfviewer.model.SyncTexCoordinates;
import org.rstudio.studio.client.pdfviewer.pdfjs.events.PDFLoadEvent;
import org.rstudio.studio.client.pdfviewer.pdfjs.events.PdfJsLoadEvent;
import org.rstudio.studio.client.pdfviewer.pdfjs.events.PdfJsWindowClosedEvent;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.user.client.Window;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@Singleton
public class PDFViewer implements CompilePdfCompletedEvent.Handler,
SynctexViewPdfEvent.Handler,
PDFLoadEvent.Handler,
LookupSynctexSourceEvent.Handler,
PdfJsLoadEvent.Handler,
PdfJsWindowClosedEvent.Handler,
WindowOpenedEvent.Handler
{
@Inject
public PDFViewer(EventBus eventBus,
final ApplicationServerOperations server,
final GlobalDisplay display,
final SatelliteManager satelliteManager,
final Synctex synctex,
final UIPrefs prefs)
{
display_ = display;
server_ = server;
synctex_ = synctex;
prefs_ = prefs;
eventBus.addHandler(CompilePdfCompletedEvent.TYPE, this);
eventBus.addHandler(SynctexViewPdfEvent.TYPE, this);
eventBus.addHandler(PDFLoadEvent.TYPE, this);
eventBus.addHandler(WindowOpenedEvent.TYPE, this);
PdfJsWindow.addPDFLoadHandler(this);
PdfJsWindow.addPageClickHandler(this);
PdfJsWindow.addWindowClosedHandler(this);
PdfJsWindow.addPdfJsLoadHandler(this);
// when this window is closed, automatically close the PDF.js window,
// if it's open
Window.addCloseHandler(new CloseHandler<Window>()
{
@Override
public void onClose(CloseEvent<Window> event)
{
if (pdfJsWindow_ != null)
pdfJsWindow_.close();
pdfJsWindow_ = null;
}
});
}
@Override
public void onPDFLoad(PDFLoadEvent event)
{
if (executeOnPdfLoad_ != null)
{
executeOnPdfLoad_.execute();
executeOnPdfLoad_ = null;
}
}
@Override
public void onCompilePdfCompleted(CompilePdfCompletedEvent event)
{
// only handle PDF compile events when we're the preferred viewer
if (!prefs_.pdfPreview().getValue().equals(UIPrefs.PDF_PREVIEW_RSTUDIO))
return;
// only handle successful compiles
final CompilePdfResult result = event.getResult();
if (!result.getSucceeded())
return;
// when the PDF is finished rendering, optionally navigate to the desired
// location, or set and restore the current location
final PdfLocation pdfLocation = result.getPdfLocation();
if (pdfLocation != null)
{
executeOnPdfLoad_ = new Operation()
{
@Override
public void execute()
{
PdfJsWindow.navigateTo(pdfJsWindow_, pdfLocation);
}
};
}
lastSuccessfulPdfPath_ = result.getPdfPath();
openPdfUrl(result.getViewPdfUrl(), result.isSynctexAvailable(),
pdfLocation == null);
}
@Override
public void onSynctexViewPdf(SynctexViewPdfEvent event)
{
if (event.getPdfLocation().getFile().equals(lastSuccessfulPdfPath_))
{
PdfJsWindow.navigateTo(pdfJsWindow_, event.getPdfLocation());
if (Desktop.isDesktop())
{
Desktop.getFrame().activateMinimalWindow(WINDOW_NAME);
}
}
}
@Override
public void onLookupSynctexSource(LookupSynctexSourceEvent event)
{
if (lastSuccessfulPdfPath_ != null)
{
if (Desktop.isDesktop())
{
Desktop.getFrame().bringMainFrameToFront();
}
else
{
focusMainWindow();
}
synctexInverseSearch(event.getCoordinates(), event.fromClick());
}
}
@Override
public void onPdfJsWindowClosed(PdfJsWindowClosedEvent event)
{
synctex_.notifyPdfViewerClosed(lastSuccessfulPdfPath_);
locationHash_ = pdfJsWindow_.getLocationHash();
pdfJsWindow_ = null;
lastSuccessfulPdfPath_ = null;
}
@Override
public void onWindowOpened(WindowOpenedEvent event)
{
if (event.getName().equals(WINDOW_NAME))
{
initializePdfJsWindow(event.getWindow());
}
}
@Override
public void onPdfJsLoad(PdfJsLoadEvent event)
{
if (executeOnPdfJsLoad_ != null)
{
executeOnPdfJsLoad_.execute();
executeOnPdfJsLoad_ = null;
}
}
public void viewPdfUrl(final String url, final Integer initialPage)
{
if (initialPage != null)
{
executeOnPdfLoad_ = new Operation()
{
@Override
public void execute()
{
pdfJsWindow_.goToPage(initialPage.intValue());
}
};
}
lastSuccessfulPdfPath_ = null;
openPdfUrl(url, false, initialPage == null);
}
// Private methods ---------------------------------------------------------
private void openPdfUrl(final String url, final boolean synctex,
boolean restorePosition)
{
int width = 1070;
int height = 1200;
Point pos = null;
// if there's a window open, restore the position when we're done
if (restorePosition &&
url.equals(lastSuccessfulPdfUrl_))
{
// if we don't have an active window, we'll use the hash stored when
// the window closed
if (haveActivePdfJsWindow())
locationHash_ = pdfJsWindow_.getLocationHash();
executeOnPdfLoad_ = createRestorePositionOperation();
}
// create the operation to load the PDF--we'll call this when the window
// is finished opening, or immediately if there's already a window open
Operation loadPdf = new Operation()
{
@Override
public void execute()
{
pdfJsWindow_.openPdf(server_.getApplicationURL(url), 0, synctex);
lastSuccessfulPdfUrl_ = url;
}
};
// in the browser we need to close and reopen the window
if (haveActivePdfJsWindow() && !Desktop.isDesktop())
{
width = pdfJsWindow_.getOuterWidth();
height = pdfJsWindow_.getOuterHeight();
pos = new Point(pdfJsWindow_.getLeft(), pdfJsWindow_.getTop());
pdfJsWindow_.close();
pdfJsWindow_ = null;
}
lastSuccessfulPdfUrl_ = null;
if (!haveActivePdfJsWindow())
{
// open the window and continue
String viewerUrl =
server_.getApplicationURL("pdf_js/web/viewer.html?file=");
NewWindowOptions options = new NewWindowOptions();
options.setName(WINDOW_NAME);
options.setShowDesktopToolbar(false);
if (pos != null)
options.setPosition(pos);
options.setCallback(new OperationWithInput<WindowEx>()
{
@Override
public void execute(WindowEx win)
{
initializePdfJsWindow(win);
}
});
executeOnPdfJsLoad_ = loadPdf;
if (Desktop.isDesktop() && Desktop.getFrame().isCocoa())
{
// on cocoa, we can open a native window
display_.openMinimalWindow(viewerUrl, false, width, height, options);
}
else
{
// on Qt, we need to open a web window so window.opener is wired
display_.openWebMinimalWindow(viewerUrl, false, width, height, options);
}
}
else
{
// we already have an open window, activate it
if (Desktop.isDesktop())
Desktop.getFrame().activateMinimalWindow(WINDOW_NAME);
loadPdf.execute();
}
}
private boolean haveActivePdfJsWindow()
{
return pdfJsWindow_ != null && !pdfJsWindow_.isClosed();
}
private void initializePdfJsWindow(WindowEx win)
{
pdfJsWindow_ = win.cast();
pdfJsWindow_.injectUiOnLoad(Desktop.isDesktop());
}
private void synctexInverseSearch(SyncTexCoordinates coord,
boolean fromClick)
{
synctex_.inverseSearch(PdfLocation.create(lastSuccessfulPdfPath_,
coord.getPageNum(),
coord.getX(),
coord.getY(),
0,
0,
fromClick));
}
private Operation createRestorePositionOperation()
{
return new Operation()
{
@Override
public void execute()
{
pdfJsWindow_.applyLocationHash(locationHash_);
locationHash_ = null;
}
};
}
private final native void focusMainWindow() /*-{
$wnd.focus();
}-*/;
private PdfJsWindow pdfJsWindow_;
private String lastSuccessfulPdfPath_;
private String lastSuccessfulPdfUrl_;
private String locationHash_;
// continuation operations for asynchronous operations:
// pdf.js loaded, PDF loaded in pdf.js
private Operation executeOnPdfJsLoad_;
private Operation executeOnPdfLoad_;
private final GlobalDisplay display_;
private final ApplicationServerOperations server_;
private final Synctex synctex_;
private final UIPrefs prefs_;
private final static String WINDOW_NAME = "rstudio_pdfjs";
}