/* * Synctex.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.common.synctex; import org.rstudio.core.client.BrowseCap; import org.rstudio.core.client.FilePosition; import org.rstudio.core.client.StringUtil; import org.rstudio.core.client.command.AppCommand; import org.rstudio.core.client.files.FileSystemItem; import org.rstudio.core.client.widget.ProgressIndicator; import org.rstudio.studio.client.application.Desktop; import org.rstudio.studio.client.application.events.EventBus; import org.rstudio.studio.client.common.GlobalDisplay; import org.rstudio.studio.client.common.GlobalProgressDelayer; import org.rstudio.studio.client.common.compilepdf.events.CompilePdfCompletedEvent; import org.rstudio.studio.client.common.compilepdf.events.CompilePdfStartedEvent; import org.rstudio.studio.client.common.filetypes.FileTypeRegistry; import org.rstudio.studio.client.common.satellite.Satellite; import org.rstudio.studio.client.common.synctex.events.SynctexStatusChangedEvent; import org.rstudio.studio.client.common.synctex.events.SynctexViewPdfEvent; import org.rstudio.studio.client.common.synctex.events.SynctexEditFileEvent; import org.rstudio.studio.client.common.synctex.model.PdfLocation; import org.rstudio.studio.client.common.synctex.model.SourceLocation; import org.rstudio.studio.client.common.synctex.model.SynctexServerOperations; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.workbench.commands.Commands; import org.rstudio.studio.client.workbench.model.Session; import org.rstudio.studio.client.workbench.prefs.model.UIPrefs; import com.google.gwt.core.client.JavaScriptObject; import com.google.inject.Inject; import com.google.inject.Singleton; @Singleton public class Synctex implements CompilePdfStartedEvent.Handler, CompilePdfCompletedEvent.Handler, SynctexEditFileEvent.Handler { @Inject public Synctex(GlobalDisplay globalDisplay, EventBus eventBus, Commands commands, SynctexServerOperations server, FileTypeRegistry fileTypeRegistry, Session session, UIPrefs prefs, Satellite satellite) { globalDisplay_ = globalDisplay; eventBus_ = eventBus; commands_ = commands; server_ = server; fileTypeRegistry_ = fileTypeRegistry; session_ = session; prefs_ = prefs; satellite_ = satellite; // main window and satellite export callbacks to eachother if (!Satellite.isCurrentWindowSatellite()) { registerMainWindowCallbacks(); eventBus_.addHandler(SynctexEditFileEvent.TYPE, this); } // fixup synctex tooltips for macos if (BrowseCap.isMacintosh()) fixupSynctexCommandDescription(commands_.synctexSearch()); // disable commands at the start setNoSynctexStatus(); // subscribe to compile pdf status event so we can update command status eventBus_.addHandler(CompilePdfStartedEvent.TYPE, this); eventBus_.addHandler(CompilePdfCompletedEvent.TYPE, this); } @Override public void onCompilePdfStarted(CompilePdfStartedEvent event) { setNoSynctexStatus(); } @Override public void onCompilePdfCompleted(CompilePdfCompletedEvent event) { String pdfPreview = prefs_.pdfPreview().getValue(); boolean synctexSupported = // internal previewer pdfPreview.equals(UIPrefs.PDF_PREVIEW_RSTUDIO) || // platform-specific desktop previewer (pdfPreview.equals(UIPrefs.PDF_PREVIEW_DESKTOP_SYNCTEX) && Desktop.isDesktop()); boolean synctexAvailable = synctexSupported && event.getResult().isSynctexAvailable(); if (synctexAvailable) setSynctexStatus(event.getResult().getTargetFile(), event.getResult().getPdfPath()); else setNoSynctexStatus(); // if this was a desktop synctex preview then invoke it directly // (internal previews are handled by the compile pdf window directly) if (event.getResult().getSucceeded() && handleDesktopSynctex()) { int page = 1; if (event.getResult().isSynctexAvailable()) page = event.getResult().getPdfLocation().getPage(); Desktop.getFrame().externalSynctexPreview( event.getResult().getPdfPath(), page); } } @Override public void onSynctexEditFile(SynctexEditFileEvent event) { if (Desktop.isDesktop()) Desktop.getFrame().bringMainFrameToFront(); goToSourceLocation(event.getSourceLocation()); } public boolean isSynctexAvailable() { return pdfPath_ != null; } public void enableCommands(boolean enabled) { commands_.synctexSearch().setVisible(enabled); commands_.synctexSearch().setEnabled(enabled); } // NOTE: the original design was for a single internal pdf viewer. for // that configuration we could keep a global pdfPath_ around and be // confident that it was always correct. we were also globally managing // the state of the synctex command based on any external viewer closing. // now that we optionally support desktop viewers for synctex this // assumption may not hold -- specfically there might be multiple active // PDF viewers for different document or we might not know that the // external viewer has closed . we have explicitly chosen to // avoid the complexity of tracking distinct viewer states. if we want // to do this we probably should do the following: // // - always keep the the syncex command available in all editors // so long as there is at least one preview window alive; OR // // - for cases where we do know whether the window is still alive // editors could dynamically show/hide their synctex button // based on that more granular state // public boolean forwardSearch(final String pdfFile, SourceLocation sourceLocation) { if (handleDesktopSynctex()) { // apply concordane final ProgressIndicator indicator = getSyncProgress(); server_.applyForwardConcordance( pdfFile, sourceLocation, new ServerRequestCallback<SourceLocation>() { @Override public void onResponseReceived(SourceLocation sourceLocation) { indicator.onCompleted(); if (sourceLocation != null) { Desktop.getFrame().externalSynctexView( pdfFile, sourceLocation.getFile(), sourceLocation.getLine(), sourceLocation.getColumn()); } } @Override public void onError(ServerError error) { indicator.onError(error.getUserMessage()); } }); return true; } // use internal viewer else { doForwardSearch(targetFile_, sourceLocation); return true; } } public void inverseSearch(PdfLocation pdfLocation) { if (Satellite.isCurrentWindowSatellite()) { // switch back to the main window satellite_.focusMainWindow(); // warn firefox users that this doesn't really work in Firefox if (BrowseCap.isFirefox()) SynctexUtils.maybeShowFirefoxWarning("source editor"); // do the inverse search callInverseSearch(pdfLocation); } else { doInverseSearch(pdfLocation); } } public void notifyPdfViewerClosed(String pdfFile) { setNoSynctexStatus(); } private void doForwardSearch(String rootDocument, JavaScriptObject sourceLocationObject) { SourceLocation sourceLocation = sourceLocationObject.cast(); final ProgressIndicator indicator = getSyncProgress(); server_.synctexForwardSearch( rootDocument, sourceLocation, new ServerRequestCallback<PdfLocation>() { @Override public void onResponseReceived(PdfLocation location) { indicator.onCompleted(); if (location != null) eventBus_.fireEvent(new SynctexViewPdfEvent(location)); } @Override public void onError(ServerError error) { indicator.onError(error.getUserMessage()); } }); } private void doInverseSearch(JavaScriptObject pdfLocationObject) { PdfLocation pdfLocation = pdfLocationObject.cast(); final ProgressIndicator indicator = getSyncProgress(); server_.synctexInverseSearch( pdfLocation, new ServerRequestCallback<SourceLocation>() { @Override public void onResponseReceived(SourceLocation location) { indicator.onCompleted(); if (location != null) goToSourceLocation(location); } @Override public void onError(ServerError error) { indicator.onError(error.getUserMessage()); } }); } private void doDesktopInverseSearch(String file, int line, int column) { // apply concordance final ProgressIndicator indicator = getSyncProgress(); server_.applyInverseConcordance( SourceLocation.create(file, line, column, true), new ServerRequestCallback<SourceLocation>() { @Override public void onResponseReceived(SourceLocation sourceLocation) { indicator.onCompleted(); if (sourceLocation != null) goToSourceLocation(sourceLocation); } @Override public void onError(ServerError error) { indicator.onError(error.getUserMessage()); } }); } private void goToSourceLocation(SourceLocation location) { FilePosition position = FilePosition.create( location.getLine(), Math.min(1, location.getColumn())); fileTypeRegistry_.editFile( FileSystemItem.createFile(location.getFile()), position); } private boolean isCurrentWindowPdfViewerSatellite() { return false; } private boolean handleDesktopSynctex() { return Desktop.isDesktop() && !Satellite.isCurrentWindowSatellite() && prefs_.pdfPreview().getValue().equals( UIPrefs.PDF_PREVIEW_DESKTOP_SYNCTEX); } private ProgressIndicator getSyncProgress() { return new GlobalProgressDelayer(globalDisplay_, 500, "Syncing...").getIndicator(); } private void setNoSynctexStatus() { setSynctexStatus(null, null); } private void setSynctexStatus(String targetFile, String pdfPath) { // set flag and fire event if (!StringUtil.notNull(pdfPath_).equals(StringUtil.notNull(pdfPath))) { targetFile_ = targetFile; pdfPath_ = pdfPath; eventBus_.fireEvent(new SynctexStatusChangedEvent(targetFile, pdfPath)); } } private void fixupSynctexCommandDescription(AppCommand command) { String desc = command.getDesc().replace("Ctrl+", "Cmd+"); command.setDesc(desc); } private native void registerMainWindowCallbacks() /*-{ var synctex = this; $wnd.synctexInverseSearch = $entry( function(pdfLocation) { synctex.@org.rstudio.studio.client.common.synctex.Synctex::doInverseSearch(Lcom/google/gwt/core/client/JavaScriptObject;)(pdfLocation); } ); $wnd.desktopSynctexInverseSearch = $entry( function(file,line,column) { synctex.@org.rstudio.studio.client.common.synctex.Synctex::doDesktopInverseSearch(Ljava/lang/String;II)(file,line,column); } ); $wnd.synctexNotifyPdfViewerClosed = $entry( function(pdfPath) { synctex.@org.rstudio.studio.client.common.synctex.Synctex::notifyPdfViewerClosed(Ljava/lang/String;)(pdfPath); } ); }-*/; private final native void callInverseSearch(JavaScriptObject pdfLocation)/*-{ $wnd.opener.synctexInverseSearch(pdfLocation); }-*/; private final native void callNotifyPdfViewerClosed(String pdfPath) /*-{ $wnd.opener.synctexNotifyPdfViewerClosed(pdfPath); }-*/; private native void callForwardSearch(JavaScriptObject satellite, String rootDocument, JavaScriptObject sourceLocation) /*-{ satellite.synctexForwardSearch(rootDocument, sourceLocation); }-*/; private final GlobalDisplay globalDisplay_; private final EventBus eventBus_; private final Commands commands_; private final SynctexServerOperations server_; private final FileTypeRegistry fileTypeRegistry_; private final Session session_; private final UIPrefs prefs_; private final Satellite satellite_; private String pdfPath_ = null; private String targetFile_ = ""; }