/*
* $Id$
*
* Copyright (c) 2004-2005 by the TeXlapse Team.
* 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
*/
package net.sourceforge.texlipse.viewer;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import net.sourceforge.texlipse.DDEClient;
import net.sourceforge.texlipse.PathUtils;
import net.sourceforge.texlipse.SelectedResourceManager;
import net.sourceforge.texlipse.TexlipsePlugin;
import net.sourceforge.texlipse.builder.BuilderRegistry;
import net.sourceforge.texlipse.builder.TexlipseNature;
import net.sourceforge.texlipse.properties.TexlipseProperties;
import net.sourceforge.texlipse.viewer.util.FileLocationListener;
import net.sourceforge.texlipse.viewer.util.FileLocationServer;
import net.sourceforge.texlipse.viewer.util.ViewerErrorScanner;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchManager;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
/**
* A helper class for opening source files.
* Defined separately, so that it could be used inside a static method.
*
* @author Kimmo Karlsson
*/
class FileLocationOpener implements FileLocationListener {
private IProject project;
public FileLocationOpener(IProject p) {
project = p;
}
public void showLineOfFile(String file, int lineNumber) {
ViewerOutputScanner.openInEditor(project, file, lineNumber);
}
}
/**
* Previewer helper class. Includes methods for launching the previewer.
* There's no need to create instances of this class.
*
* @author Anton Klimovsky
* @author Kimmo Karlsson
* @author Tor Arne Vestb�
* @author Boris von Loesch
*/
public class ViewerManager {
// the file name variable in the arguments
public static final String FILENAME_PATTERN = "%file";
// the line number variable in the arguments
public static final String LINE_NUMBER_PATTERN = "%line";
// the source file name variable in the arguments
public static final String TEX_FILENAME_PATTERN = "%texfile";
// file name with absolute path
public static final String FILENAME_FULLPATH_PATTERN = "%fullfile";
// the source file name variable in the arguments with absolute path
public static final String TEX_FILENAME_FULLPATH_PATTERN = "%fulltexfile";
// viewer attributes
private ViewerAttributeRegistry registry;
// environment variables to add to current environment
private Map envSettings;
// the current project
private IProject project;
/**
* Run the viewer configured in the given viewer attributes.
* First check if there is a viewer already running,
* and if there is, return that process.
*
* @param reg the viewer attributes
* @param addEnv additional environment variables, or null
* @param monitor monitor for process
* @return the viewer process
* @throws CoreException if launching the viewer fails
*/
public static Process preview(ViewerAttributeRegistry reg, Map addEnv, IProgressMonitor monitor) throws CoreException {
ViewerManager mgr = new ViewerManager(reg, addEnv);
if (!mgr.initialize()) {
return null;
}
Process process = mgr.getExisting();
if (process != null) {
// Can send DDE right away
mgr.sendDDEViewCommand();
return process;
}
// Process must be started first
process = mgr.execute();
// Send DDE if on Win32
if (Platform.getOS().equals(Platform.OS_WIN32)) {
// Since the process can take a while to start we must wait
// to "make sure" the DDE command gets there.
// This is probably a HACK: should be fixed/changed.
try {
Thread.sleep(1000); // 1000 enough? Who knows!?
// The timeout should probably be a config setting?
} catch (InterruptedException e) {
}
mgr.sendDDEViewCommand();
}
return process;
}
/**
* Closes the target output document using the DDE command from the
* default viewer, or the most recently launched preview. This method
* is probably fragile since the process and launches handling in
* Texlipse is too weak to always know what documents are locked and
* needs closing.
*
* @throws CoreException
*/
public static void closeOutputDocument() throws CoreException {
ViewerAttributeRegistry registry = new ViewerAttributeRegistry();
// Check to see if we have a running launch configuration which should
// override the DDE close command
ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
ILaunch[] launches = manager.getLaunches();
for (int i = 0; i < launches.length; i++) {
ILaunch l = launches[i];
ILaunchConfiguration configuration = l.getLaunchConfiguration();
if (configuration != null && configuration.exists() && configuration.getType().getIdentifier().equals(
TexLaunchConfigurationDelegate.CONFIGURATION_ID)) {
Map regMap = configuration.getAttributes();
registry.setValues(regMap);
break;
}
}
ViewerManager mgr = new ViewerManager(registry, null);
if (!mgr.initialize()) {
return;
}
// Can only close documents opened by DDE commands themselves
Process process = mgr.getExisting();
if (process != null) {
mgr.sendDDECloseCommand();
try {
Thread.sleep(500); // A small delay required
} catch (InterruptedException e) {
// swallow
}
returnFocusToEclipse(false);
}
}
/**
* Returns the application focus to Eclipse after launching an
* external previewer. This is done by first activating the
* eclipse window, and then setting focus in the editor in a
* worker thread. Note the delay needed before setting focus.
*
* @param useMinimizeTrick if true uses a trick to force focus by
* minimizing and restoring the eclipse window
*/
public static void returnFocusToEclipse(final boolean useMinimizeTrick) {
// Return focus/activation to Eclipse/Texlipse
Display display = PlatformUI.getWorkbench().getDisplay();
if (null != display) {
display.asyncExec(new Runnable() {
public void run() {
IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows();
for (int i = 0; i < workbenchWindows.length; i++) {
Shell shell = workbenchWindows[i].getShell();
if (useMinimizeTrick) {
shell.setMinimized(true);
shell.setMinimized(false);
}
shell.setActive();
shell.forceActive();
break;
}
}
});
}
// Spawn thread to set focus in the editor after the launch has completed
// The reason we cannot do this in the current thread is because the progress
// window is in the way and will not allow us to set focus on the editor.
new Thread(new Runnable() {
public void run() {
try { Thread.sleep(500); } catch (InterruptedException e) { }
Display display = PlatformUI.getWorkbench().getDisplay();
if (null != display) {
display.asyncExec(new Runnable() {
public void run() {
IWorkbenchWindow[] workbenchWindows = PlatformUI.getWorkbench().getWorkbenchWindows();
for (int i = 0; i < workbenchWindows.length; i++) {
IWorkbenchPage activePage = workbenchWindows[i].getActivePage();
activePage.activate(activePage.getActiveEditor());
// Although setFocus should not be called by clients it is
// required for the focus to work. Activate alone is not enough.
activePage.getActiveEditor().setFocus();
}
}
});
}
}
}).start();
}
/**
* Construct a new viewer launcher.
* @param reg viewer attributes
* @param addEnv environment variables to add to the current environment
*/
protected ViewerManager(ViewerAttributeRegistry reg, Map addEnv) {
this.registry = reg;
this.envSettings = addEnv;
}
/**
* Find out the current project.
* @return true, if success
*/
protected boolean initialize() {
project = TexlipsePlugin.getCurrentProject();
if (project == null) {
BuilderRegistry.printToConsole(TexlipsePlugin.getResourceString("viewerNoCurrentProject"));
return false;
}
try { // Make sure it's a LaTeX project
if (project.getNature(TexlipseNature.NATURE_ID) == null)
{
return false;
}
} catch (CoreException e) {
}
return true;
}
/**
* Check if viewer already running.
* This method returns false also, if the user has enabled multiple viewer instances.
* @return the running viewer process, or null if viewer has already terminated
*/
protected Process getExisting() {
Object o = TexlipseProperties.getSessionProperty(project,
TexlipseProperties.SESSION_ATTRIBUTE_VIEWER);
if (o != null) {
if (o instanceof HashMap) {
HashMap viewerInfo = (HashMap) o;
Process p = (Process) viewerInfo.get("process");
String cmd = (String) viewerInfo.get("command");
int code = -1;
try {
code = p.exitValue();
} catch (IllegalThreadStateException e) {
}
// there is a viewer running and forward search is not supported
if (code == -1 && !registry.getForward()) {
// ... so don't launch another viewer window
return p;
} else if (cmd.toLowerCase().indexOf("acrobat.exe") > -1 && code == 1) {
// This is a fix for Acrobat Professional returning 1 even
// though it's still running. Probably because it's using a
// launcher process of some kind which spawns the real acrobat.
if (Platform.getOS().equals(Platform.OS_WIN32)) {
try {
String s = "";
Runtime Rt = Runtime.getRuntime();
InputStream ip = Rt.exec("tasklist").getInputStream();
BufferedReader in = new BufferedReader(new InputStreamReader(ip));
while ((s = in.readLine()) != null) {
if (s.toLowerCase().indexOf("acrobat.exe") > -1)
return p;
}
} catch (IOException e) {
}
}
}
}
TexlipseProperties.setSessionProperty(project,
TexlipseProperties.SESSION_ATTRIBUTE_VIEWER, null);
}
return null;
}
/**
* Run the viewer configured in the given viewer attributes.
* Paths are resolved so that the viewer program is run in source directory.
* The viewer program is given a relative pathname and filename as a command line
* argument.
*
* @return the viewer process
* @throws CoreException if launching the viewer fails
*/
protected Process execute() throws CoreException {
//load settings, if changed on disk
if (TexlipseProperties.isProjectPropertiesFileChanged(project)) {
TexlipseProperties.loadProjectProperties(project);
}
IResource outputRes = getOuputResource(project);
if (outputRes == null || !outputRes.exists()) {
String msg = TexlipsePlugin.getResourceString("viewerNothingWithExtension");
BuilderRegistry.printToConsole(msg.replaceAll("%s", registry.getFormat()));
return null;
}
// resolve the directory to run the viewer in
IContainer sourceDir = TexlipseProperties.getProjectSourceDir(project);
File dir = sourceDir.getLocation().toFile();
try {
return execute(dir);
} catch (IOException e) {
throw new CoreException(TexlipsePlugin.stat("Could not start previewer '"
+ registry.getActiveViewer() + "'. Please make sure you have entered "
+ "the correct path and filename in the viewer preferences.", e));
}
}
protected void sendDDEViewCommand() throws CoreException {
if (Platform.getOS().equals(Platform.OS_WIN32)) {
String command = translatePatterns(registry.getDDEViewCommand());
String server = registry.getDDEViewServer();
String topic = registry.getDDEViewTopic();
int error = DDEClient.execute(server, topic, command);
if (error != 0) {
String errorMessage = "DDE command " + command + " failed! " +
"(server: " + server + ", topic: " + topic + ")";
TexlipsePlugin.log(errorMessage, new Throwable(errorMessage));
}
}
}
protected void sendDDECloseCommand() throws CoreException {
if (Platform.getOS().equals(Platform.OS_WIN32)) {
String command = translatePatterns(registry.getDDECloseCommand());
String server = registry.getDDECloseServer();
String topic = registry.getDDECloseTopic();
int error = DDEClient.execute(server, topic, command);
if (error != 0) {
String errorMessage = "DDE command " + command + " failed! " +
"(server: " + server + ", topic: " + topic + ")";
TexlipsePlugin.log(errorMessage, new Throwable(errorMessage));
}
}
}
/**
* Resolves a relative path from one directory to another.
* The path is returned as an OS-specific string with
* a terminating separator.
*
* @param sourcePath a directory to start from
* @param outputPath a directory to end up to
* @return a relative path from sourcePath to outputPath
*/
public static String resolveRelativePath(IPath sourcePath, IPath outputPath) {
int same = sourcePath.matchingFirstSegments(outputPath);
if (same == sourcePath.segmentCount()
&& same == outputPath.segmentCount()) {
return "";
}
outputPath = outputPath.removeFirstSegments(same);
sourcePath = sourcePath.removeFirstSegments(same);
StringBuffer sb = new StringBuffer();
for (int i = 0; i < sourcePath.segmentCount(); i++) {
sb.append("..");
sb.append(File.separatorChar);
}
for (int i = 0; i < outputPath.segmentCount(); i++) {
sb.append(outputPath.segment(i));
sb.append(File.separatorChar);
}
return sb.toString();
}
/**
* Determines the Resource which should be shown. Respects partial builds.
* @return
* @throws CoreException
*/
public static IResource getOuputResource(IProject project) throws CoreException {
String outFileName = TexlipseProperties.getOutputFileName(project);
if (outFileName == null || outFileName.length() == 0) {
throw new CoreException(TexlipsePlugin.stat("Empty output file name."));
}
// find out the directory where the file should be
IContainer outputDir = null;
// String fmtProp = TexlipseProperties.getProjectProperty(project,
// TexlipseProperties.OUTPUT_FORMAT);
// if (registry.getFormat().equals(fmtProp)) {
outputDir = TexlipseProperties.getProjectOutputDir(project);
/* } else {
String base = outFileName.substring(0, outFileName.lastIndexOf('.') + 1);
outFileName = base + registry.getFormat();
outputDir = TexlipseProperties.getProjectTempDir(project);
}*/
if (outputDir == null) {
outputDir = project;
}
IResource resource = outputDir.findMember(outFileName);
return resource != null ? resource : project.getFile(outFileName);
}
/**
* Returns the current line number of the current page, if possible.
*
* @author Anton Klimovsky
* @return the current line number of the current page
*/
private int getCurrentLineNumber() {
//Fix for Bug: 1637560
int lineNumber = 0;
final IWorkbenchPage currentWorkbenchPage = TexlipsePlugin.getCurrentWorkbenchPage();
if (currentWorkbenchPage != null) {
final int[] buf = new int[1];
//This must run in UI thread
Display.getDefault().syncExec(
new Runnable() {
public void run(){
ISelection selection = currentWorkbenchPage.getSelection();
if (selection != null && selection instanceof ITextSelection) {
ITextSelection textSelection = (ITextSelection) selection;
// The "srcltx" package's line numbers seem to start from 1
// it is also the case with latex's --source-specials option
buf[0] = textSelection.getStartLine() + 1;
}
}
});
lineNumber = buf[0];
}
// lineNumber = SelectedResourceManager.getDefault().getSelectedLine();
if (lineNumber <= 0) {
lineNumber = 1;
}
return lineNumber;
}
/**
* Run the given viewer in the given directory with the given file.
* Also start viewer output listener to enable inverse search.
*
* @param dir the directory to run the viewer in
* @return viewer process
* @throws IOException if launching the viewer fails
*/
private Process execute(File dir) throws IOException, CoreException {
// argument list
ArrayList<String> list = new ArrayList<String>();
// add command as arg0
String command = registry.getCommand();
if (command.indexOf(' ') > 0) {
command = "\"" + command + "\"";
}
list.add(command);
// add arguments
String args = translatePatterns(registry.getArguments());
PathUtils.tokenizeEscapedString(args, list);
// create environment
Properties env = PathUtils.getEnv();
if (envSettings != null) {
env.putAll(envSettings);
}
//String envp[] = PathUtils.getStrings(env);
String envp[] = PathUtils.mergeEnvFromPrefs(env, TexlipseProperties.VIEWER_ENV_SETTINGS);
// print command
BuilderRegistry.printToConsole(TexlipsePlugin.getResourceString("viewerRunning")
+ " " + command + " " + args);
// start viewer process
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec((String[]) list.toArray(new String[0]), envp, dir);
// save attribute
HashMap viewerInfo = new HashMap();
viewerInfo.put("process", process);
viewerInfo.put("command", command);
viewerInfo.put("arguments", args);
TexlipseProperties.setSessionProperty(project,
TexlipseProperties.SESSION_ATTRIBUTE_VIEWER, viewerInfo );
// start viewer listener
startOutputListener(process.getInputStream(), registry.getInverse());
// start error reader
new Thread(new ViewerErrorScanner(process)).start();
return process;
}
/**
* Fills the %arg of the input pattern with the real values
* @param input The input pattern
* @return the filled string
* @throws CoreException
*/
private String translatePatterns(String input) throws CoreException {
if (input == null) return null;
IContainer sourceDir = TexlipseProperties.getProjectSourceDir(project);
if (input.indexOf(FILENAME_PATTERN) >= 0) {
// resolve relative path to the output file
IResource outputRes = getOuputResource(project);
String outFileName = outputRes.getName();
outFileName = resolveRelativePath(sourceDir.getFullPath(), outputRes.getFullPath());
outFileName = outFileName.substring(0, outFileName.length() - 1);
if (outFileName.indexOf(' ') >= 0) {
//Quote filenames with spaces
outFileName = "\"" + outFileName +"\"";
}
input = input.replaceAll(FILENAME_PATTERN, escapeBackslashes(outFileName));
}
if (input.indexOf(FILENAME_FULLPATH_PATTERN) >= 0) {
input = input.replaceAll(FILENAME_FULLPATH_PATTERN, escapeBackslashes(getOuputResource(project).getLocation().toOSString()));
}
if (input.indexOf(LINE_NUMBER_PATTERN) >= 0) {
input = input.replaceAll(LINE_NUMBER_PATTERN, "" + getCurrentLineNumber());
}
if (input.indexOf(TEX_FILENAME_PATTERN) >= 0) {
IResource selectedRes = SelectedResourceManager.getDefault().getSelectedResource();
if (selectedRes.getType() != IResource.FOLDER) {
selectedRes = SelectedResourceManager.getDefault().getSelectedTexResource();
}
String relPath = resolveRelativePath(sourceDir.getFullPath(),
selectedRes.getFullPath().removeLastSegments(1));
String texFile = relPath + selectedRes.getName();
input = input.replaceAll(TEX_FILENAME_PATTERN, escapeBackslashes(texFile));
}
if (input.indexOf(TEX_FILENAME_FULLPATH_PATTERN) >= 0) {
IResource selectedRes = SelectedResourceManager.getDefault().getSelectedResource();
if (selectedRes.getType() != IResource.FOLDER) {
selectedRes = SelectedResourceManager.getDefault().getSelectedTexResource();
}
input = input.replaceAll(TEX_FILENAME_FULLPATH_PATTERN, escapeBackslashes(selectedRes.getLocation().toOSString()));
}
return input;
}
/**
* Escapes backslashes, so that the string can be given to String.replaceAll()
* as argument without the backslashes disappearing.
* @param file input string, typically a filename
* @return the input string with backslashes doubled
*/
private String escapeBackslashes(String file) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < file.length(); i++) {
char c = file.charAt(i);
sb.append(c);
if (c == '\\') {
sb.append(c);
}
}
return sb.toString();
}
/**
* Start a listener thread for the viewer program's standard output.
*
* @param in input stream where the output of a viewer program will be available
* @param viewer the name of the viewer
*/
private void startOutputListener(final InputStream in, String inverse) {
if (inverse.equals(ViewerAttributeRegistry.INVERSE_SEARCH_RUN)) {
FileLocationServer server = FileLocationServer.getInstance();
server.setListener(new FileLocationOpener(project));
if (!server.isRunning()) {
new Thread(server).start();
}
//Read everything from InputStream, otherwise the process will stay open in some cases
//happens e.g. with sumatrapdf
new Thread(new Runnable(){
public void run() {
InputStream st = new BufferedInputStream(in);
try {
byte[] buf = new byte[1024];
//read as long as the process exists and dump its content
while (st.read(buf) != -1) {
//System.out.println(new String(buf));
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
st.close();
} catch (IOException e) {
}
}
}).start();
} else if (inverse.equals(ViewerAttributeRegistry.INVERSE_SEARCH_STD)) {
new Thread(new ViewerOutputScanner(project, in)).start();
}
}
}