org.rstudio.studio.client.common.synctex.Synctex.java Source code

Java tutorial

Introduction

Here is the source code for org.rstudio.studio.client.common.synctex.Synctex.java

Source

/*
 * 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_ = "";

}