1 #include "buildmanager.h"
2 
3 #include "smallUsefulFunctions.h"
4 #include "configmanagerinterface.h"
5 #include "utilsSystem.h"
6 #include "execprogram.h"
7 #include "findindirs.h"
8 
9 #include "userquickdialog.h"
10 
11 #ifdef Q_OS_WIN32
12 #include "windows.h"
13 #endif
14 
onWin_Nix(const char * const win,const char * const nix)15 inline const char * onWin_Nix(const char * const win,const char * const nix){
16     #ifdef Q_OS_WIN32
17         (void) nix;
18         return win;
19     #else
20         (void) win;
21         return nix;
22     #endif
23 }
24 
25 const QString BuildManager::TXS_CMD_PREFIX = "txs:///";
26 
27 int BuildManager::autoRerunLatex = 5;
28 bool BuildManager::showLogInCaseOfCompileError = true;
29 bool BuildManager::m_replaceEnvironmentVariables = true;
30 bool BuildManager::m_interpetCommandDefinitionInMagicComment = true;
31 bool BuildManager::m_supportShellStyleLiteralQuotes = true;
32 bool BuildManager::singleViewerInstance = false;
33 QString BuildManager::autoRerunCommands;
34 QString BuildManager::additionalSearchPaths, BuildManager::additionalPdfPaths, BuildManager::additionalLogPaths;
35 
36 // *INDENT-OFF* (astyle-config)
37 
38 #define CMD_DEFINE(name,id) \
39     const QString BuildManager::CMD_##name = BuildManager::TXS_CMD_PREFIX + #id
40 
41 CMD_DEFINE(LATEX,latex);
42 CMD_DEFINE(PDFLATEX,pdflatex);
43 CMD_DEFINE(XELATEX,xelatex);
44 CMD_DEFINE(LUALATEX,lualatex);
45 CMD_DEFINE(LATEXMK,latexmk);
46 
47 CMD_DEFINE(VIEW_DVI,view-dvi);
48 CMD_DEFINE(VIEW_PS,view-ps);
49 CMD_DEFINE(VIEW_PDF,view-pdf);
50 CMD_DEFINE(VIEW_LOG,view-log);
51 
52 CMD_DEFINE(DVIPNG,dvipng);
53 CMD_DEFINE(DVIPS,dvips);
54 CMD_DEFINE(DVIPDF,dvipdf);
55 CMD_DEFINE(PS2PDF,ps2pdf);
56 
57 CMD_DEFINE(GS,gs);
58 CMD_DEFINE(MAKEINDEX,makeindex);
59 CMD_DEFINE(TEXINDY,texindy);
60 CMD_DEFINE(MAKEGLOSSARIES,makeglossaries);
61 CMD_DEFINE(METAPOST,metapost);
62 CMD_DEFINE(ASY,asy);
63 
64 CMD_DEFINE(BIBTEX,bibtex);
65 CMD_DEFINE(BIBTEX8,bibtex8);
66 CMD_DEFINE(BIBER,biber);
67 
68 CMD_DEFINE(SVN,svn);
69 CMD_DEFINE(SVNADMIN,svnadmin);
70 CMD_DEFINE(GIT,git);
71 CMD_DEFINE(TEXDOC,texdoc);
72 CMD_DEFINE(COMPILE,compile);
73 CMD_DEFINE(VIEW,view);
74 CMD_DEFINE(BIBLIOGRAPHY,bibliography);
75 CMD_DEFINE(INDEX,index);
76 CMD_DEFINE(GLOSSARY,glossary);
77 CMD_DEFINE(QUICK,quick);
78 CMD_DEFINE(RECOMPILE_BIBLIOGRAPHY,recompile-bibliography);
79 CMD_DEFINE(VIEW_PDF_INTERNAL,view-pdf-internal);
80 CMD_DEFINE(CONDITIONALLY_RECOMPILE_BIBLIOGRAPHY,conditionally-recompile-bibliography);
81 CMD_DEFINE(INTERNAL_PRE_COMPILE,internal-pre-compile);
82 CMD_DEFINE(TERMINAL_EXTERNAL,terminal-external);
83 
84 #undef CMD_DEFINE
85 // *INDENT-ON* (astyle-config)
86 
87 //! These commands should not consist of a command list, but rather a single command.
88 //! Otherwise surpising side effects can happen, see https://sourceforge.net/p/texstudio/bugs/2119/
89 const QStringList atomicCommands = QStringList() << "txs:///latex" << "txs:///pdflatex" << "txs:///xelatex"<< "txs:///lualatex" << "txs:///latexmk";
90 
91 QString searchBaseCommand(const QString &cmd, QString options, QString texPath="");
92 QString getCommandLineViewDvi();
93 QString getCommandLineViewPs();
94 QString getCommandLineViewPdfExternal();
95 QString getCommandLineGhostscript();
96 
CommandInfo()97 CommandInfo::CommandInfo(): user(false), meta(false), rerunCompiler(false), guessFunc(nullptr) {}
98 
guessCommandLine(const QString texpath) const99 QString CommandInfo::guessCommandLine(const QString texpath) const
100 {
101 	if (guessFunc) {
102 		QString temp = (*guessFunc)();
103 		if (!temp.isEmpty()) return temp;
104 	}
105 
106 	if (!baseName.isEmpty()) {
107 		//search it
108         QString bestCommand = searchBaseCommand(baseName, defaultArgs,texpath);
109 		if (!bestCommand.isEmpty()) return bestCommand;
110 	}
111 
112 	if (metaSuggestionList.size() > 0)
113 		return metaSuggestionList.first();
114 
115 	return "";
116 }
117 
setCommandLine(const QString & cmdString)118 void CommandInfo::setCommandLine(const QString &cmdString)
119 {
120 	if (cmdString == "<default>") commandLine = guessCommandLine();
121 	if (cmdString == BuildManager::tr("<unknown>")) commandLine = "";
122 	else {
123 		//force commands to include options (e.g. file name)
124 		QString trimmed = cmdString.trimmed();
125 		QString unquote = trimmed;
126 		if (trimmed.startsWith('"') && trimmed.endsWith('"')) unquote = trimmed.mid(1, trimmed.length() - 2);
127 		if (baseName != "" &&
128 		        ((unquote == baseName) ||
129 		         (   (unquote.endsWith(QDir::separator() + baseName) || unquote.endsWith("/" + baseName))
130 		             && (!unquote.contains(" ") || (!unquote.contains('"') && unquote != trimmed)) //spaces mean options, if not everything is quoted
131                      && (QFileInfo::exists(unquote))
132 		         )
133 		        )) {
134 			commandLine = cmdString + " " + defaultArgs;
135 		} else {
136 			commandLine = cmdString;
137 		}
138 	}
139 }
140 
getPrettyCommand() const141 QString CommandInfo::getPrettyCommand() const
142 {
143 	if (commandLine.isEmpty() && metaSuggestionList.isEmpty()) return BuildManager::tr("<unknown>");
144 	else return commandLine;
145 }
146 
getProgramName(const QString & commandLine)147 QString CommandInfo::getProgramName(const QString &commandLine)
148 {
149 	QString cmdStr = commandLine.trimmed();
150 	int p = -1;
151 	if (cmdStr.startsWith('"')) p = cmdStr.indexOf('"', 1) + 1;
152 	else if (cmdStr.contains(' ')) p = cmdStr.indexOf(' ');
153 	if (p == -1) p = cmdStr.length(); //indexOf failed if it returns -1
154 	return cmdStr.mid(0, p);
155 }
156 
getProgramNameUnquoted(const QString & commandLine)157 QString CommandInfo::getProgramNameUnquoted(const QString &commandLine)
158 {
159 	QString cmdStr = getProgramName(commandLine);
160 	if (cmdStr.startsWith('"') && cmdStr.endsWith('"'))
161 		cmdStr = cmdStr.mid(1, cmdStr.length() - 2);
162 	return cmdStr;
163 }
164 
getProgramName() const165 QString CommandInfo::getProgramName() const
166 {
167 	return getProgramName(commandLine);
168 }
169 
getProgramNameUnquoted() const170 QString CommandInfo::getProgramNameUnquoted() const
171 {
172 	return getProgramNameUnquoted(commandLine);
173 }
174 
ExpandingOptions(const QFileInfo & mainFile,const QFileInfo & currentFile,const int currentLine)175 ExpandingOptions::ExpandingOptions(const QFileInfo &mainFile, const QFileInfo &currentFile, const int currentLine): mainFile(mainFile), currentFile(currentFile), currentLine(currentLine), nestingDeep(0), canceled(false)
176 {
177 	override.removeAll = false;
178 }
179 
180 /*
181 QString BuildManager::getLatexCommandExecutable(LatexCommand cmd){
182  QString cmdStr = getLatexCommand(cmd).trimmed();
183  int p=-1;
184  if (cmdStr.startsWith('"')) p = cmdStr.indexOf('"',1)+1;
185  else if (cmdStr.contains(' ')) p = cmdStr.indexOf(' ')+1;
186  if (p==-1) p = cmdStr.length(); //indexOf failed if it returns -1
187  return cmdStr.mid(0, p);
188 }*/
189 
CommandToRun()190 CommandToRun::CommandToRun() {}
CommandToRun(const QString & cmd)191 CommandToRun::CommandToRun(const QString &cmd): command(cmd) {}
192 
chainCommands(const QString & a)193 QString BuildManager::chainCommands(const QString &a)
194 {
195 	return a;
196 }
197 
chainCommands(const QString & a,const QString & b)198 QString BuildManager::chainCommands(const QString &a, const QString &b)
199 {
200 	return a + "|" + b;
201 }
202 
chainCommands(const QString & a,const QString & b,const QString & c)203 QString BuildManager::chainCommands(const QString &a, const QString &b, const QString &c)
204 {
205 	return a + "|" + b + "|" + c;
206 }
207 
chainCommands(const QString & a,const QString & b,const QString & c,const QString & d)208 QString BuildManager::chainCommands(const QString &a, const QString &b, const QString &c, const QString &d)
209 {
210 	return a + "|" + b + "|" + c + "|" + d;
211 }
212 
213 /** splits a string into options. Splitting occurs at spaces, except in quotes. **/
splitOptions(const QString & s)214 QStringList BuildManager::splitOptions(const QString &s)
215 {
216 	QStringList options;
217 	QChar c;
218 	bool inQuote = false;
219 	int start = 0;
220 	int i;
221 	for (i = 0; i < s.length(); i++) {
222 		c = s[i];
223 		if (inQuote) {
224 			if (c == '"' && s[i - 1] != '\\') {
225 				inQuote = false;
226 			}
227 		} else {
228 			if (c == '"') {
229 				inQuote = true;
230 			} else if (c == ' ') {
231 				if (start == i) start = i + 1; // multiple spaces;
232 				else {
233 					options << dequoteStr(s.mid(start, i - start));
234 					start = i + 1;
235 				}
236 			}
237 		}
238 	}
239 	if (start < i) options << dequoteStr(s.mid(start, i - start));
240 	return options;
241 }
242 
getEnvVariables(bool uppercaseNames)243 QHash<QString, QString> getEnvVariables(bool uppercaseNames)
244 {
245 	QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
246 	QHash<QString, QString> result;
247     foreach (const QString &name, env.keys()) {
248 		if (uppercaseNames) {
249 			result.insert(name.toUpper(), env.value(name));
250 		} else {
251 			result.insert(name, env.value(name));
252 		}
253 	}
254 	return result;
255 }
256 
replaceEnvironmentVariables(const QString & s)257 QString BuildManager::replaceEnvironmentVariables(const QString &s)
258 {
259 #ifdef Q_OS_WIN
260 	Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive;
261 #else
262 	Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive;
263 #endif
264 	static QHash<QString, QString> envVariables = getEnvVariables(caseSensitivity == Qt::CaseInsensitive);  // environment variables can be static because they do not change during program execution.
265 	return replaceEnvironmentVariables(s, envVariables, caseSensitivity == Qt::CaseInsensitive);
266 }
267 
268 /*!
269  * Replace environment variables in the string.
270  */
replaceEnvironmentVariables(const QString & s,const QHash<QString,QString> & variables,bool compareNamesToUpper)271 QString BuildManager::replaceEnvironmentVariables(const QString &s, const QHash<QString, QString> &variables, bool compareNamesToUpper)
272 {
273 	QString result(s);
274 #ifdef Q_OS_WIN
275     QRegularExpression rxEnvVar("%([\\w()]+)%");  // word and brackets between %...%
276 #else
277     QRegularExpression rxEnvVar("\\$(\\w+)");
278 #endif
279 	int i = 0;
280 	while (i >= 0 && i < result.length()) {
281         QRegularExpressionMatch match;
282         i = result.indexOf(rxEnvVar, i,&match);
283 		if (i >= 0) {
284             QString varName = match.captured(1);
285 			if (compareNamesToUpper) {
286 				varName = varName.toUpper();
287 			}
288 			QString varContent = variables.value(varName, "");
289             result.replace(match.captured(0), varContent);
290 			i += varContent.length();
291 		}
292 	}
293 	return result;
294 }
295 
296 /*!
297  * returns paths with replaced [txs-app-dir], [txs-config-dir] and optionally with replaced environment variables
298  * paths may be an arbitrary string, in particular a single path or a list of paths
299  */
resolvePaths(QString paths)300 QString BuildManager::resolvePaths(QString paths)
301 {
302 	paths = ConfigManagerInterface::getInstance()->parseDir(paths);
303 	if (m_replaceEnvironmentVariables)
304 		return replaceEnvironmentVariables(paths);
305 	else
306 		return paths;
307 }
308 
BuildManager()309 BuildManager::BuildManager(): processWaitedFor(nullptr)
310 #ifdef Q_OS_WIN32
311 	, pidInst(0)
312 #endif
313 {
314 	initDefaultCommandNames();
315     connect(this, SIGNAL(commandLineRequested(QString,QString*,bool*)), SLOT(commandLineRequestedDefault(QString,QString*,bool*)));
316 }
317 
~BuildManager()318 BuildManager::~BuildManager()
319 {
320 	//remove preview file names
321 	foreach (const QString &elem, previewFileNames)
322 		removePreviewFiles(elem);
323 #ifdef Q_OS_WIN32
324 	if (pidInst) DdeUninitialize(pidInst);
325 #endif
326 }
327 
initDefaultCommandNames()328 void BuildManager::initDefaultCommandNames()
329 {
330 	REQUIRE (commands.isEmpty());
331 
332 	//id, platform-independent command, display name, arguments
333 	registerCommand("latex",       "latex",        "LaTeX",       "-src -interaction=nonstopmode %.tex", "Tools/Latex");
334 	registerCommand("pdflatex",    "pdflatex",     "PdfLaTeX",    "-synctex=1 -interaction=nonstopmode %.tex", "Tools/Pdflatex");
335 	registerCommand("xelatex",     "xelatex",      "XeLaTeX",     "-synctex=1 -interaction=nonstopmode %.tex", "");
336 	registerCommand("lualatex",    "lualatex",     "LuaLaTeX",    "-synctex=1 -interaction=nonstopmode %.tex", "");
337 	registerCommand("view-dvi",    "",             tr("DVI Viewer"), "%.dvi", "Tools/Dvi", &getCommandLineViewDvi);
338 	registerCommand("view-ps",     "",             tr("PS Viewer"), "%.ps", "Tools/Ps", &getCommandLineViewPs);
339 	registerCommand("view-pdf-external", "",        tr("External PDF Viewer"), "%.pdf", "Tools/Pdf", &getCommandLineViewPdfExternal);
340 	registerCommand("dvips",       "dvips",        "DviPs",       "-o %.ps %.dvi", "Tools/Dvips");
341 	registerCommand("dvipng",      "dvipng",       "DviPng",      "-T tight -D 120 %.dvi", "Tools/Dvipng");
342 	registerCommand("ps2pdf",      "ps2pdf",       "Ps2Pdf",      "%.ps", "Tools/Ps2pdf");
343 	registerCommand("dvipdf",      "dvipdfmx",       "DviPdf",      "%.dvi", "Tools/Dvipdf");
344     registerCommand("bibtex",      "bibtex",       "BibTeX",       onWin_Nix("%","%.aux"),  "Tools/Bibtex"); //miktex bibtex will stop (appears like crash in txs) if .aux is attached
345     registerCommand("bibtex8",     "bibtex8",      "BibTeX 8-Bit", onWin_Nix("%","%.aux"));
346 	registerCommand("biber",       "biber",        "Biber" ,       "%"); //todo: correct parameter?
347 	registerCommand("makeindex",   "makeindex",    "Makeindex",   "%.idx", "Tools/Makeindex");
348 	registerCommand("texindy",     "texindy",      "Texindy", "%.idx");
349 	registerCommand("makeglossaries", "makeglossaries", "Makeglossaries", "%");
350 	registerCommand("metapost",    "mpost",        "MetaPost",    "-interaction=nonstopmode ?me)", "Tools/MetaPost");
351 	registerCommand("asy",         "asy",          "Asymptote",   "?m*.asy", "Tools/Asy");
352 	registerCommand("gs",          "gs;mgs",       "Ghostscript", "\"?am.ps\"", "Tools/Ghostscript", &getCommandLineGhostscript);
353 	registerCommand("latexmk",     "latexmk",      "Latexmk",     "-pdf -silent -synctex=1 %");
354     registerCommand("texdoc",     "texdoc",      "texdoc",     "");
355 
356 
357 	QStringList descriptionList;
358 	descriptionList << tr("Compile & View") << tr("PS Chain") << tr("DVI Chain") << tr("PDF Chain") << tr("DVI->PDF Chain") << tr("DVI->PS->PDF Chain") << tr("Asymptote DVI Chain") << tr("Asymptote PDF Chain");
359 	registerCommand("quick", tr("Build & View"), QStringList() << "txs:///compile | txs:///view" << "txs:///ps-chain" << "txs:///dvi-chain" << "txs:///pdf-chain" << "txs:///dvi-pdf-chain" << "txs:///dvi-ps-pdf-chain" << "txs:///asy-dvi-chain" << "txs:///asy-pdf-chain" /* too long breaks design<< "latex -interaction=nonstopmode %.tex|bibtex %.aux|latex -interaction=nonstopmode %.tex|latex -interaction=nonstopmode %.tex| txs:///view-dvi"*/, "Tools/Userquick", true, descriptionList);
360 
361 	descriptionList.clear();
362 	descriptionList << tr("PdfLaTeX") << tr("LaTeX") << tr("XeLaTeX") << tr("LuaLaTeX") << tr("Latexmk");
363 	registerCommand("compile", tr("Default Compiler"), QStringList() << "txs:///pdflatex" << "txs:///latex" << "txs:///xelatex" << "txs:///lualatex" << "txs:///latexmk", "", true, descriptionList);
364 	descriptionList.clear();
365 	descriptionList << tr("PDF Viewer") << tr("DVI Viewer") << tr("PS Viewer");
366 	registerCommand("view", tr("Default Viewer"), QStringList() << "txs:///view-pdf" << "txs:///view-dvi" << "txs:///view-ps", "", true, descriptionList);
367 	descriptionList.clear();
368 	descriptionList << tr("Internal PDF Viewer (Embedded)") << tr("Internal PDF Viewer (Windowed)")  << tr("External PDF Viewer");
369 	registerCommand("view-pdf", tr("PDF Viewer"), QStringList() << "txs:///view-pdf-internal --embedded" << "txs:///view-pdf-internal" << "txs:///view-pdf-external", "", true, descriptionList);
370 	descriptionList.clear();
371 	descriptionList << tr("BibTeX") << tr("BibTeX 8-Bit") << tr("Biber");
372 	registerCommand("bibliography", tr("Default Bibliography Tool"), QStringList() << "txs:///bibtex" << "txs:///bibtex8" << "txs:///biber", "", true, descriptionList);
373 	descriptionList.clear();
374 	descriptionList << tr("BibTeX") << tr("BibTeX 8-Bit") << tr("Biber");
375 	registerCommand("index", tr("Default Index Tool"), QStringList() << "txs:///makeindex" << "txs:///texindy", "", true, descriptionList);
376 	descriptionList.clear();
377 	descriptionList << tr("Makeglossaries");
378 	registerCommand("glossary", tr("Default Glossary Tool"), QStringList() << "txs:///makeglossaries", "", true, descriptionList);
379 
380 	registerCommand("ps-chain", tr("PS Chain"), QStringList() << "txs:///latex | txs:///dvips | txs:///view-ps");
381 	registerCommand("dvi-chain", tr("DVI Chain"), QStringList() << "txs:///latex | txs:///view-dvi");
382 	registerCommand("pdf-chain", tr("PDF Chain"), QStringList() << "txs:///pdflatex | txs:///view-pdf");
383 	registerCommand("dvi-pdf-chain", tr("DVI->PDF Chain"), QStringList() << "txs:///latex | txs:///dvipdf | txs:///view-pdf");
384 	registerCommand("dvi-ps-pdf-chain", tr("DVI->PS->PDF Chain"), QStringList() << "txs:///latex | txs:///dvips | txs:///ps2pdf | txs:///view-pdf");
385 	registerCommand("asy-dvi-chain", tr("Asymptote DVI Chain"), QStringList() << "txs:///latex | txs:///asy | txs:///latex | txs:///view-dvi");
386 	registerCommand("asy-pdf-chain", tr("Asymptote PDF Chain"), QStringList() << "txs:///pdflatex | txs:///asy | txs:///pdflatex | txs:///view-pdf");
387 
388 	registerCommand("pre-compile", tr("Precompile"), QStringList() << "", "Tools/Precompile");
389 	registerCommand("internal-pre-compile", tr("Internal Precompile"), QStringList() << "txs:///pre-compile | txs:///conditionally-recompile-bibliography");
390 	registerCommand("recompile-bibliography", tr("Recompile Bibliography"), QStringList() << "txs:///compile | txs:///bibliography | txs:///compile");
391 
392 
393 	registerCommand("svn",         "svn",          "SVN",         "", "Tools/SVN");
394 	registerCommand("svnadmin",    "svnadmin",     "SVNADMIN",    "", "Tools/SVNADMIN");
395 
396     registerCommand("git",         "git",          "GIT",         "", "Tools/GIT");
397 
398 	registerCommand("terminal-external", "", tr("External Terminal"), "", "", guessTerminalExternal, false);
399 
400 	internalCommands << CMD_VIEW_PDF_INTERNAL << CMD_CONDITIONALLY_RECOMPILE_BIBLIOGRAPHY << CMD_VIEW_LOG;
401 }
402 
guessTerminalExternal(void)403 QString BuildManager::guessTerminalExternal(void)
404 {
405 #if defined(Q_OS_DARWIN)
406 	return "open -a Terminal ?c:a\"";
407 #elif defined(Q_OS_UNIX)
408 	// Linux/Unix does not have a uniform way to determine the default terminal application
409 
410 	// Gnome
411 	ExecProgram execGsettings("gsettings get org.gnome.desktop.default-applications.terminal exec", "");
412 	if (execGsettings.execAndWait()) {
413 		/*
414 			1. "gsettings" terminates with exit code 0 if settings were fetched successfully.
415 			2. The returned value has a trailing LF so we trim it.
416 			3. The command is wrapped in single quotes, e.g. 'gnome-terminal' so we remove the single quotes.
417 		 */
418 		return execGsettings.m_standardOutput.trimmed().replace('\'', "");
419 	}
420 
421 	// Fallback
422 	QStringList fallbacks = QStringList() << "konsole" << "xterm";
423 	foreach (const QString &fallback, fallbacks) {
424 		ExecProgram execWhich("which " + fallback, "");
425 		if (execWhich.execAndWait()) {
426 			// "which" terminates with exit code 0 if settings were fetched successfully
427 			return execWhich.m_standardOutput;
428 		}
429 	}
430 #elif defined(Q_OS_WIN)
431 	QString command = QString::fromLocal8Bit(qgetenv("COMSPEC"));
432 	if (command != "") {
433 		return command;
434 	}
435 #endif
436 	return QString("<unknown>");
437 }
438 
registerCommand(const QString & id,const QString & basename,const QString & displayName,const QString & args,const QString & oldConfig,const GuessCommandLineFunc guessFunc,bool user)439 CommandInfo &BuildManager::registerCommand(const QString &id, const QString &basename, const QString &displayName, const QString &args, const QString &oldConfig, const GuessCommandLineFunc guessFunc, bool user )
440 {
441 	CommandInfo ci;
442 	ci.id = id;
443 	ci.baseName = basename;
444 	ci.displayName = displayName;
445 	ci.defaultArgs = args;
446 	ci.deprecatedConfigName = oldConfig;
447 	ci.guessFunc = guessFunc;
448 	ci.user = user;
449 	if (!user) commandSortingsOrder << id;
450 	return commands.insert(id, ci).value();
451 }
452 
registerCommand(const QString & id,const QString & displayname,const QStringList & alternatives,const QString & oldConfig,const bool metaCommand,const QStringList simpleDescriptions)453 CommandInfo &BuildManager::registerCommand(const QString &id, const QString &displayname, const QStringList &alternatives, const QString &oldConfig, const bool metaCommand, const QStringList simpleDescriptions)
454 {
455 	CommandInfo ci;
456 	ci.id = id;
457 	ci.displayName = displayname;
458 	ci.metaSuggestionList = alternatives;
459 	ci.simpleDescriptionList = simpleDescriptions;
460 	ci.meta = metaCommand;
461 	ci.deprecatedConfigName = oldConfig;
462 	commandSortingsOrder << id;
463 	return commands.insert(id, ci).value();
464 }
465 
getCommandLine(const QString & id,bool * user)466 QString BuildManager::getCommandLine(const QString &id, bool *user)
467 {
468 	QString result;
469 	emit commandLineRequested(id.trimmed(), &result, user);
470 	return result;
471 }
472 
parseExtendedCommandLine(QString str,const QFileInfo & mainFile,const QFileInfo & currentFile,int currentline)473 QStringList BuildManager::parseExtendedCommandLine(QString str, const QFileInfo &mainFile, const QFileInfo &currentFile, int currentline)
474 {
475 	ConfigManagerInterface *config = ConfigManagerInterface::getInstance();
476 	str = config->parseDir(str);
477 	if (m_replaceEnvironmentVariables) {
478 		str = replaceEnvironmentVariables(str);
479 	}
480 	// need to reformat literal quotes before the file insertion logic, because ?a"
481 	// might be extended to "C:\somepath\" which would then be misinterpreted as an
482 	// literal quote at the end.
483 	if (config->getOption("Tools/SupportShellStyleLiteralQuotes", true).toBool()) {
484 		str = ProcessX::reformatShellLiteralQuotes(str);
485 	}
486 
487 	str = str + " "; //end character  so str[i++] is always defined
488 	QStringList result;
489 	result.append("");
490 	for (int i = 0; i < str.size(); i++) {
491 		QString add;
492 		if (str.at(i) == QChar('%')) {
493 			if (str.at(i + 1) == QChar('%')) add = str.at(++i);
494 			else add = "\"" + mainFile.completeBaseName() + "\"";
495 		} else if (str.at(i) == QChar('@')) {
496 			if (str.at(i + 1) == QChar('@')) add = str.at(++i);
497 			else add = QString::number(currentline);
498 		} else if (str.at(i) == QChar('?')) {
499 			if (str.at(++i) == QChar('?')) add = "?";
500 			else {
501 				QString command, commandRem;
502 				QString *createCommand = &command;
503 				int endMode = 0;
504 				bool fullSearch = false;
505 				while (i < str.size()) {
506 					if (str.at(i) == QChar(')')) {
507 						endMode = 1;
508 						break;
509 					} else if (str.at(i) == QChar(' ') || str.at(i) == QChar('\t')) {
510 						endMode = 2;
511 						break;
512 					} else if (str.at(i) == QChar('\"')) {
513 						endMode = 3;
514 						break;
515 					} else if (str.at(i) == QChar('.') && !fullSearch) {
516 						endMode = 4;
517 						break;
518 					} else if (str.at(i) == QChar('*')) {
519 						fullSearch = true;
520 						createCommand = &commandRem;
521 					}
522 					(*createCommand) += str.at(i);
523 					i++;
524 				}
525 				QFileInfo selectedFile = parseExtendedSelectFile(command, mainFile, currentFile);
526 				bool absPath = command.startsWith('a');
527 				//check only sane commands
528 				if (command == "ame")
529 					command = QDir::toNativeSeparators(selectedFile.absoluteFilePath());
530 				else if (command == "am") {
531 					command = QDir::toNativeSeparators(selectedFile.absoluteFilePath());
532 					if (selectedFile.suffix() != "") command.chop(1 + selectedFile.suffix().length());
533 				} else if (command == "a") {
534 					command = QDir::toNativeSeparators(selectedFile.absolutePath());
535 					if (!command.endsWith(QDir::separator())) command += QDir::separator();
536 				} else if (command == "rme")
537 					command = QDir::toNativeSeparators(mainFile.dir().relativeFilePath(selectedFile.absoluteFilePath()));
538 				else if (command == "rm") {
539 					command = QDir::toNativeSeparators(mainFile.dir().relativeFilePath(selectedFile.absoluteFilePath()));
540 					if (selectedFile.suffix() != "") command.chop(1 + selectedFile.suffix().length());
541 				} else if (command == "r") {
542 					command = QDir::toNativeSeparators(mainFile.dir().relativeFilePath(selectedFile.absolutePath()));
543 					if (command == "") command = ".";
544 					if (!command.endsWith(QDir::separator())) command += QDir::separator();
545 				} else if (command == "me") command = selectedFile.fileName();
546 				else if (command == "m") command = selectedFile.completeBaseName();
547 				else if (command == "e") command = selectedFile.suffix();
548 				else if (command.isEmpty() && !commandRem.isEmpty()); //empty search
549 				else continue; //invalid command
550 
551 				command.append(commandRem);
552 				switch (endMode) {
553 				case 2:
554 					command += " ";
555 					break;
556 				case 3:
557 					command = "\"" + command + "\"";
558 					break;
559 				case 4:
560 					command += ".";
561 					break;
562 				default:
563 					;
564 				}
565 				if (!fullSearch) add = command;
566 				else {
567 					QDir dir(QFileInfo(mainFile).absoluteDir());
568 					if (command.contains("/")) command = command.mid(command.lastIndexOf("/") + 1);
569 					if (command.contains(QDir::separator())) command = command.mid(command.lastIndexOf(QDir::separator()) + 1);
570 					QStringList commands = QDir(dir).entryList(QStringList() << command.trimmed(), QDir::Files);
571 					QString mid;
572 					if (absPath) {
573 						mid = QDir::toNativeSeparators(dir.canonicalPath());
574 						if (!mid.endsWith('/') && !mid.endsWith(QDir::separator())) mid += QDir::separator();
575 					}
576 					QStringList oldCommands = result;
577 					result.clear();
578 					for (int i = 0; i < oldCommands.size(); i++)
579 						for (int j = 0; j < commands.size(); j++)
580 							result.append(oldCommands[i] + mid + commands[j]);
581 				}
582 			}
583 		} else add = str.at(i);
584 		if (!add.isEmpty())
585 			for (int i = 0; i < result.size(); i++)
586 				result[i] += add;
587 	}
588 	//  QMessageBox::information(0,"",str+"->"+result,0);
589 	for (int i = 0; i < result.size(); i++) result[i] = result[i].trimmed(); //remove useless characters
590 	return result;
591 }
592 
593 /*
594  * Select a file which provides the pathname parts used by the "ame" expansions. Currently we can select
595  * one of the following files:
596  *
597  * - Master (root) .tex file (default).
598  * - Current .tex file. Selected by the c: prefix.
599  * - A file with the same complete basename as the master file and a chosen extension. The search for this
600  *   file is done in the master file directory and then the extra PDF directories. Selected by the p{ext}:
601  *   prefix.
602  *
603  * TODO: If selector ?p{ext}: is not flexible enough then maybe we should implement another selector:
604  * ?f{regexp_with_basename}:
605  *
606  * It will be processed like this:
607  *
608  * 1. regexp_with_basename undergoes %-token replacement
609  *    %m is replaced by the complete basename of the master file.
610  *    %% is replaced by %
611  * 2. The expression from 1 is used to search the master file directory, the current file
612  *    directory and the extra PDF directories.
613  * 3. If step 2 finds a matching file it is used as a selected file. If step 2 does not
614  *    find a file, then some reasonable default is used.
615  */
parseExtendedSelectFile(QString & command,const QFileInfo & mainFile,const QFileInfo & currentFile)616 QFileInfo BuildManager::parseExtendedSelectFile(QString &command, const QFileInfo &mainFile, const QFileInfo &currentFile)
617 {
618 	QFileInfo selectedFile;
619 	QRegExp rxPdf("^p\\{([^{}]+)\\}:");
620 
621 	if (command.startsWith("c:")) {
622 		selectedFile = currentFile.fileName().isEmpty() ? mainFile : currentFile;
623 		command = command.mid(2);
624 	} else if (rxPdf.indexIn(command) != -1) {
625 		QString compiledFilename = mainFile.completeBaseName() + '.' + rxPdf.cap(1);
626 		QString compiledFound = findCompiledFile(compiledFilename, mainFile);
627 		selectedFile = QFileInfo(compiledFound);
628 		command = command.mid(rxPdf.matchedLength());
629 	} else {
630 		selectedFile = mainFile;
631 	}
632 	return selectedFile;
633 }
634 
635 /*!
636  * \brief extracts the
637  * \param s
638  * \param stdOut output parameter
639  * \param stdErr output parameter
640  * \return a copy of s truncated to the first occurrence of an output redirection
641  */
extractOutputRedirection(const QString & commandLine,QString & stdOut,QString & stdErr)642 QString BuildManager::extractOutputRedirection(const QString &commandLine, QString &stdOut, QString &stdErr)
643 {
644 	QStringList args = ::extractOutputRedirection(tokenizeCommandLine(commandLine), stdOut, stdErr);
645 	if (stdOut.isEmpty() && stdErr.isEmpty()) {
646 		return commandLine;
647 	} else {
648 		return args.join(" ");
649 	}
650 }
651 
addPathDelimeter(const QString & a)652 QString addPathDelimeter(const QString &a)
653 {
654 	return ((a.endsWith("/") || a.endsWith("\\")) ? a : (a + QDir::separator()));
655 }
656 
findFileInPath(QString fileName)657 QString BuildManager::findFileInPath(QString fileName)
658 {
659 	foreach (QString path, getEnvironmentPathList()) {
660 		path = addPathDelimeter(path);
661         if (QFileInfo::exists(path + fileName)) return (path + fileName);
662 	}
663 	return "";
664 }
665 
666 #ifdef Q_OS_WIN32
667 typedef BOOL (__stdcall *AssocQueryStringAFunc)(DWORD, DWORD, const char *, const char *, char *, DWORD *);
W32_FileAssociation(QString ext)668 QString W32_FileAssociation(QString ext)
669 {
670 	if (ext == "") return "";
671 	if (ext[0] != QChar('.')) ext = '.' + ext;
672 	QString result = "";
673 	QByteArray ba = ext.toLocal8Bit();
674 	HMODULE mod = LoadLibraryA("shlwapi.dll");
675 	AssocQueryStringAFunc assoc = (AssocQueryStringAFunc)(GetProcAddress(mod, "AssocQueryStringA"));
676 	if (assoc) {
677 		const DWORD ASSOCSTR_COMMAND = 1;
678 		char buf[1024];
679 		DWORD buflen = 1023;
680 		if (assoc(0, ASSOCSTR_COMMAND, ba.data(), "open", &buf[0], &buflen) == S_OK) {
681 			buf[buflen] = 0;
682 			result = QString::fromLatin1(buf);
683 			result.replace("%1", "?am" + ext);
684 			//QMessageBox::information(0,result,result,0);
685 		}
686 	}
687 	FreeLibrary(mod);
688 	return result;
689 }
690 
getProgramFilesPaths()691 QStringList getProgramFilesPaths()
692 {
693 	QStringList res;
694 	QString a = getenv("PROGRAMFILES");
695 	if (!a.isEmpty()) res << addPathDelimeter(a);
696 	a = getenv("PROGRAMFILES(X86)");
697 	if (!a.isEmpty()) res << addPathDelimeter(a);
698 	if (a != "C:/Program Files" && QDir("C:/Program Files").exists()) res << "C:/Program Files/";
699 	if (a != "C:/Program Files (x86)" && QDir("C:/Program Files (x86)").exists()) res << "C:/Program Files (x86)/";
700 	if (a + " (x86)" != "C:/Program Files (x86)" && QDir(a + " (x86)").exists()) res << (a + " (x86)");
701 	return res;
702 }
703 
704 /*!
705  * \return the Uninstall string of the program from the registry
706  *
707  * Note: This won't get the path of 64bit installations when TXS running as a 32bit app due to wow6432 regristry redirection
708  * http://www.qtcentre.org/threads/36966-QSettings-on-64bit-Machine
709  * http://stackoverflow.com/questions/25392251/qsettings-registry-and-redirect-on-regedit-64bit-wow6432
710  * No workaround known, except falling back to the native Windows API. For the moment we'll rely on alternative detection methods.
711  */
getUninstallString(const QString & program)712 QString getUninstallString(const QString &program) {
713 	foreach (const QString &baseKey, QStringList() << "HKEY_LOCAL_MACHINE" << "HKEY_CURRENT_USER") {
714 		QSettings base(baseKey, QSettings::NativeFormat);
715 		QString s = base.value("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + program + "\\UninstallString").toString();
716 		if (!s.isEmpty())
717 			return s;
718 	}
719 	return QString();
720 }
721 
722 /*!
723  * \return an existing subdir of the from path\subDirFilter\subSubDir.
724  *
725  * Example:
726  * QStringList searchPaths = QStringList() << "C:\\" << "D:\\"
727  * findSubdir(searchPaths, "*miktex*", "miktex\\bin\\")
728  * will work for "C:\\MikTeX\\miktex\\bin\\" or "D:\\MikTeX 2.9\\miktex\\bin"
729  */
findSubDir(const QStringList & searchPaths,const QString & subDirFilter,const QString & subSubDir)730 QString findSubDir(const QStringList &searchPaths, const QString &subDirFilter, const QString &subSubDir) {
731 	qDebug() << searchPaths;
732 	foreach (const QString &path, searchPaths) {
733 		foreach (const QString &dir, QDir(path).entryList(QStringList(subDirFilter), QDir::AllDirs, QDir::Time)) {
734 			QDir fullPath(addPathDelimeter(path) + addPathDelimeter(dir) + subSubDir);
735 			if (fullPath.exists())
736 				return fullPath.absolutePath();
737 		}
738 	}
739 	return QString();
740 }
741 
742 /*!
743  * Returns the MikTeX bin path.
744  * This should not be called directly but only through getMiKTeXBinPath() to prevent multiple searches.
745  */
getMiKTeXBinPathInternal()746 QString getMiKTeXBinPathInternal()
747 {
748 	// search the registry
749 	QString mikPath = getUninstallString("MiKTeX 2.9");
750 	// Note: this does currently not work for MikTeX 64bit because of registry redirection (also we would have to parse the
751 	// uninstall string there for the directory). For the moment we'll fall back to other detection methods.
752 	if (!mikPath.isEmpty() && QDir(addPathDelimeter(mikPath) + "miktex\\bin\\").exists()) {
753 		mikPath = addPathDelimeter(mikPath) + "miktex\\bin\\";
754 	}
755 
756 	// search the PATH
757 	if (mikPath.isEmpty()) {
758 		foreach (QString path, getEnvironmentPathList()) {
759 			path = addPathDelimeter(path);
760 			if ((path.endsWith("\\miktex\\bin\\x64\\") || path.endsWith("\\miktex\\bin\\")) && QDir(path).exists())
761 				return path;
762 		 }
763 	}
764 
765 	// search all program file paths
766 	if (mikPath.isEmpty()) {
767 		mikPath = QDir::toNativeSeparators(findSubDir(getProgramFilesPaths(), "*miktex*", "miktex\\bin\\"));
768 	}
769 
770 	// search a fixed list of additional locations
771 	if (mikPath.isEmpty()) {
772 		static const QStringList candidates = QStringList() << "C:\\miktex\\miktex\\bin"
773 															<< "C:\\tex\\texmf\\miktex\\bin"
774 															<< "C:\\miktex\\bin"
775 															<< QString(qgetenv("LOCALAPPDATA")) + "\\Programs\\MiKTeX 2.9\\miktex\\bin";
776 		foreach (const QString &path, candidates)
777 			if (QDir(path).exists()) {
778 				mikPath = path;
779 				break;
780 			}
781 	}
782 
783     if(!mikPath.endsWith("\\")){
784         mikPath.append("\\");
785     }
786 	// post-process to detect 64bit installation
787 	if (!mikPath.isEmpty()) {
788         if (QDir(mikPath + "x64\\").exists())
789             return mikPath + "x64\\";
790 		else
791 			return mikPath;
792 	}
793 	return "";
794 }
795 
796 static QString miktexBinPath = "<search>";
797 /*!
798  * \return the MikTeX bin path. This uses caching so that the search is only performed once per session.
799  */
getMiKTeXBinPath()800 QString getMiKTeXBinPath()
801 {
802     if (miktexBinPath == "<search>") miktexBinPath = getMiKTeXBinPathInternal();
803 	return miktexBinPath;
804 }
805 
806 /*!
807  * Returns the TeXlive bin path.
808  * This should not be called directly but only through getTeXLiveWinBinPath() to prevent multiple searches.
809  */
getTeXLiveWinBinPathInternal()810 QString getTeXLiveWinBinPathInternal()
811 {
812 	//check for uninstall entry
813 	foreach (const QString &baseKey, QStringList() << "HKEY_CURRENT_USER" << "HKEY_LOCAL_MACHINE") {
814 		QSettings reg(baseKey + "\\Software", QSettings::NativeFormat);
815 		QString uninstall;
816         QDate date = QDate::currentDate();
817         for (int v = date.year(); v > 2008; v--) {
818 			uninstall = reg.value(QString("microsoft/windows/currentversion/uninstall/TeXLive%1/UninstallString").arg(v), "").toString();
819 			if (!uninstall.isEmpty()) {
820 				int p = uninstall.indexOf("\\tlpkg\\", 0, Qt::CaseInsensitive);
821 				QString path = p > 0 ? uninstall.left(p) : "";
822 				if (QDir(path + "\\bin\\win32").exists())
823 					return path + "\\bin\\win32\\";
824 			}
825 		}
826 	}
827 	//check for path
828 	QString pdftex = BuildManager::findFileInPath("pdftex.exe");
829 	int p = pdftex.indexOf("\\bin\\", 0, Qt::CaseInsensitive);
830 	if (p <= 0) return "";
831 	QString path = pdftex.left(p);
832 	if (!QFileInfo(path + "\\release-texlive.txt").exists()) return "";
833 	return path + "\\bin\\win32\\";
834 }
835 
836 static QString texliveWinBinPath = "<search>";
837 /*!
838  * \return the TeXlive bin path on windows. This uses caching so that the search is only performed once per session.
839  */
getTeXLiveWinBinPath()840 QString getTeXLiveWinBinPath()
841 {
842 	if (texliveWinBinPath == "<search>") texliveWinBinPath = getTeXLiveWinBinPathInternal();
843 	return texliveWinBinPath;
844 }
845 
846 
findGhostscriptDLL()847 QString findGhostscriptDLL()   //called dll, may also find an exe
848 {
849 	//registry
850 	foreach (QString program, QStringList() << "GPL Ghostscript" << "AFPL Ghostscript")
851 		foreach(QString hkeyBase, QStringList() << "HKEY_CURRENT_USER" << "HKEY_LOCAL_MACHINE") {
852 			QSettings reg(hkeyBase + "\\Software\\" + program, QSettings::NativeFormat);
853 			QStringList version = reg.childGroups();
854 			if (version.empty()) continue;
855 			version.sort();
856 			for (int i = version.size() - 1; i >= 0; i--) {
857 				QString dll = reg.value(version[i] + "/GS_DLL", "").toString();
858 				if (!dll.isEmpty()) return dll;
859 			}
860 		}
861 	//environment
862 	QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
863 	if (env.contains("GS_DLL")) {
864 		return env.value("GS_DLL");
865 	}
866 	//file search
867 	foreach (const QString &p, getProgramFilesPaths())
868 		if (QDir(p + "gs").exists())
869 			foreach (const QString &gsv, QDir(p + "gs").entryList(QStringList() << "gs*.*", QDir::Dirs, QDir::Time)) {
870 				QString x = p + "gs/" + gsv + "/bin/gswin32c.exe";
871 				if (QFile::exists(x)) return x;
872 			}
873 	return "";
874 }
875 #endif
876 
searchBaseCommand(const QString & cmd,QString options,QString texPath)877 QString searchBaseCommand(const QString &cmd, QString options, QString texPath)
878 {
879 	foreach(QString command, cmd.split(";")) {
880         QString fileName = command + onWin_Nix(".exe","");
881 		if (!options.startsWith(" ")) options = " " + options;
882         if (!texPath.isEmpty() && QFileInfo::exists(addPathDelimeter(texPath) + fileName)) {
883             return addPathDelimeter(texPath)+fileName+options; // found in texpath
884         }
885         if (!BuildManager::findFileInPath(fileName).isEmpty())
886             return fileName + options; //found in path
887         // additonal search path
888         QStringList addPaths=BuildManager::resolvePaths(BuildManager::additionalSearchPaths).split(";");
889         foreach(const QString& path, addPaths){
890             if (QFileInfo::exists(addPathDelimeter(path) + fileName)) {
891                 return addPathDelimeter(path)+fileName+options; // found in texpath
892             }
893         }
894 
895         //platform dependent mess
896 #ifdef Q_OS_WIN32
897         //Windows MikTex
898         QString mikPath = getMiKTeXBinPath();
899         if (!mikPath.isEmpty() && QFileInfo(mikPath + fileName).exists())
900             return "\"" + mikPath + fileName + "\" " + options; //found
901         //Windows TeX Live
902         QString livePath = getTeXLiveWinBinPath();
903         if (!livePath.isEmpty() && QFileInfo(livePath + fileName).exists())
904             return "\"" + livePath + fileName + "\" " + options; //found
905 #endif
906 #ifdef Q_OS_MAC
907         QStringList paths;
908         paths << "/usr/bin/texbin/" << "/usr/local/bin/" << "/usr/texbin/" << "/Library/TeX/texbin/" << "/Library/TeX/local/bin/" ;
909         paths << "/usr/local/teTeX/bin/i386-apple-darwin-current/" << "/usr/local/teTeX/bin/powerpc-apple-darwin-current/" << "/usr/local/teTeX/bin/x86_64-apple-darwin-current/";
910 
911         QDate date = QDate::currentDate();
912         for (int i = date.year(); i > 2008; i--) {
913             //paths << QString("/usr/texbin MACTEX/TEXLIVE%1").arg(i); from texmaker comment
914             paths << QString("/usr/local/texlive/%1/bin/x86_64-darwin/").arg(i);
915             paths << QString("/usr/local/texlive/%1/bin/i386-darwin/").arg(i);
916             paths << QString("/usr/local/texlive/%1/bin/powerpc-darwin/").arg(i);
917         }
918         foreach (const QString &p, paths)
919             if (QFileInfo(p + fileName).exists()) {
920                 if (cmd == "makeglossaries") {
921                     // workaround: makeglossaries calls makeindex or xindy and therefore has to be run in an environment that has these commands on the path
922                     return QString("sh -c \"PATH=$PATH:%1; %2%3\"").arg(p).arg(fileName).arg(options);
923                 } else {
924                     return p + fileName + options;
925                 }
926             }
927 #endif
928 	}
929 	return "";
930 }
931 
expandCommandLine(const QString & str,ExpandingOptions & options)932 ExpandedCommands BuildManager::expandCommandLine(const QString &str, ExpandingOptions &options)
933 {
934 	QRegExp re(QRegExp::escape(TXS_CMD_PREFIX) + "([^/ [{]+)(/?)((\\[[^\\]*]+\\]|\\{[^}]*\\})*) ?(.*)");
935 
936 	options.nestingDeep++;
937 	if (options.canceled) return ExpandedCommands();
938 	if (options.nestingDeep > maxExpandingNestingDeep) {
939 		if (!UtilsUi::txsConfirmWarning(tr("The command has been expanded to %1 levels. Do you want to continue expanding \"%2\"?").arg(options.nestingDeep).arg(str))) {
940 			options.canceled = true;
941 			return ExpandedCommands();
942 		}
943 	}
944 
945 	ExpandedCommands res;
946 	QStringList splitted = str.split("|");
947 	foreach (const QString &split, splitted) { //todo: ignore | in strings
948 		QString subcmd = split.trimmed();
949 
950 		if (!subcmd.startsWith(TXS_CMD_PREFIX))  {
951 			RunCommandFlags  flags = getSingleCommandFlags(subcmd);
952 
953 			if (options.override.removeAll)
954 				subcmd = CommandInfo::getProgramName(subcmd);
955 			if (!options.override.append.isEmpty())
956 				subcmd += " " + options.override.append.join(" "); //todo: simplify spaces???
957 			//Regexp matching parameters
958 			//Unescaped: .*(-abc(=([^ ]*|"([^"]|\"([^"])*\")*"))?).*
959 			//Doesn't support nesting deeper than \"
960             const QString parameterMatching = "(=([^ \"]+|\"([^\"]|\\\"([^\"])*\\\")*\"))?";
961 			for (int i = 0; i < options.override.remove.size(); i++) {
962 				const QString &rem = options.override.remove[i];
963                 QRegularExpression removalRegex(" (-?" + QRegularExpression::escape(rem) + (rem.contains("=") ? "" : parameterMatching) + ")");
964 				subcmd.replace(removalRegex, " ");
965 			}
966 			for (int i = 0; i < options.override.replace.size(); i++) {
967 				const QString &rem = options.override.replace[i].first;
968 				QRegExp replaceRegex(" (-?" + QRegExp::escape(rem) + parameterMatching + ")");
969 				int pos = replaceRegex.indexIn(subcmd);
970 				QString rep = " " + rem + options.override.replace[i].second;
971 				if (pos < 0) subcmd.insert(CommandInfo::getProgramName(subcmd).length(), rep);
972 				else {
973 					subcmd.replace(pos, replaceRegex.matchedLength(), rep);
974 					pos += rep.length();
975 					int newpos;
976 					while ( (newpos = replaceRegex.indexIn(subcmd, pos)) >= 0)
977 						subcmd.replace(newpos, replaceRegex.matchedLength(), " ");
978 				}
979 			}
980 
981 			foreach (const QString &c, parseExtendedCommandLine(subcmd, options.mainFile, options.currentFile, options.currentLine)) {
982 				CommandToRun temp(c);
983 				temp.flags = flags;
984 				res.commands << temp;
985 			}
986 		} else if (re.exactMatch(subcmd)) {
987 			const QString &cmdName = re.cap(1);
988 			const QString &slash = re.cap(2);
989 			QString modifiers = re.cap(3);
990 			QString parameters = re.cap(5);
991 			if (slash != "/" && !modifiers.isEmpty()) {
992 				UtilsUi::txsInformation(tr("You have used txs:///command[... or txs:///command{... modifiers, but we only support modifiers of the form txs:///command/[... or txs:///command/{... with an slash suffix to keep the syntax purer."));
993 				modifiers.clear();
994 			}
995 			if (options.override.removeAll) {
996 				parameters.clear();
997 				modifiers.clear();
998 			}
999 
1000 			bool user;
1001 			QString cmd = getCommandLine(cmdName, &user);
1002 			if (cmd.isEmpty()) {
1003 				if (options.nestingDeep == 1) UtilsUi::txsWarning(tr("Command %1 not defined").arg(subcmd));
1004 				else if (cmdName != "pre-compile") qDebug() << tr("Command %1 not defined").arg(subcmd); //pre-compile is expecte
1005 				if (cmdName != "pre-compile") {
1006 					res.commands << CommandToRun(""); // add empty command to provoke an error on higher level. Otherwise the missing of the command is simply ignoed e.g. txs:/quick without empty pdflatex
1007 					res.primaryCommand = "";
1008 				}
1009 				continue;
1010 			}
1011 
1012 			int space = cmd.indexOf(' ');
1013 			if (space == -1) space = cmd.size();
1014 			if (cmd.startsWith(TXS_CMD_PREFIX) && internalCommands.contains(cmd.left(space))) {
1015 				QStringList exp=parseExtendedCommandLine(cmd, options.mainFile, options.currentFile, options.currentLine);
1016 				res.commands << CommandToRun(exp.first()+" "+parameters);
1017 				res.commands.last().parentCommand = res.commands.last().command;
1018 				if (user) res.commands.last().flags |= RCF_CHANGE_PDF;
1019 				continue;
1020 			}
1021 
1022 			//parse command modifiers
1023 			bool removeAllActivated = false;
1024 			int replacePrepended = 0, removePrepended = 0;
1025 			if (!modifiers.isEmpty()) {
1026 				//matching combinations like [-abc][-foo=bar]{-xasa...}
1027 				QRegExp modifierRegexp("^((\\[([^=\\]]+)(=[^\\]]+)?\\])|(\\{([^}]*)\\}))");
1028 				while (modifierRegexp.indexIn(modifiers) >= 0) {
1029 					if (!modifierRegexp.cap(3).isEmpty()) {
1030 						replacePrepended++;
1031 						options.override.replace.prepend(QPair<QString, QString>(modifierRegexp.cap(3), modifierRegexp.cap(4)));
1032 						//qDebug() << "replace >" << options.override.replace.first().first << "< with >"<<options.override.replace.first().second<<"<";
1033 					} else if (!modifierRegexp.cap(5).isEmpty()) {
1034 						if (modifierRegexp.cap(6).isEmpty()) {
1035 							removeAllActivated = true; // qDebug() << "remove all";
1036 						} else {
1037 							removePrepended++;
1038 							options.override.remove.prepend(modifierRegexp.cap(6));
1039 							//qDebug() << "remove >" << options.override.remove.first() << "<";
1040 						}
1041 					}
1042 					modifiers.remove(0, modifierRegexp.matchedLength());
1043 				}
1044 			}
1045 			if (removeAllActivated) options.override.removeAll = true;
1046 			if (!parameters.isEmpty()) options.override.append.prepend(parameters);
1047 			//todo /(masterfile,currentfile) modifier ?
1048 
1049 			//recurse
1050 			ExpandedCommands ecNew = expandCommandLine(cmd, options);
1051 			if (ecNew.commands.length() > 1 && atomicCommands.contains(cmd)) {
1052 				UtilsUi::txsWarning(QString(tr("The command %1 is expected to be atomic. However, it is currently "
1053 				                      "defined as a command-chain containing %2 commands. This is beyond "
1054 				                      "the specification and may lead to surprising side-effects.\n\n"
1055 				                      "Please change your configuration and define command lists only at "
1056 				                      "'Options -> Configure TeXstudio -> Build' not at "
1057 				                      "'Options -> Configure TeXstudio -> Commands'.")).arg(cmd).arg(ecNew.commands.length()));
1058 			}
1059 			QList<CommandToRun> &newPart = ecNew.commands;
1060 
1061 			//clean up modifiers
1062 			if (removeAllActivated) options.override.removeAll = false;
1063 			if (!parameters.isEmpty()) options.override.append.removeFirst();
1064 			for (; replacePrepended > 0; replacePrepended--) options.override.replace.removeFirst();
1065 			for (; removePrepended > 0; removePrepended--) options.override.remove.removeFirst();
1066 
1067 			if (newPart.isEmpty()) continue;
1068 
1069 			if (commands.value(cmdName).rerunCompiler)
1070 				for (int i = 0; i < newPart.size(); i++)
1071 					newPart[i].flags |= RCF_RERUN;
1072 
1073 			for (int i = 0; i < newPart.size(); i++)
1074 				if (newPart[i].parentCommand.isEmpty()) {
1075 					newPart[i].parentCommand = cmdName;
1076 					if (user) {
1077 						newPart[i].flags |= RCF_SHOW_STDOUT;
1078 						newPart[i].flags |= RCF_CHANGE_PDF;
1079 					}
1080 				}
1081 
1082 			if (splitted.size() == 1)
1083 				res.primaryCommand = cmdName;
1084 
1085 			res.commands << newPart;
1086 		} else UtilsUi::txsWarning(tr("Failed to understand command %1").arg(subcmd));
1087 	}
1088 	options.nestingDeep--;
1089 	return res;
1090 }
1091 
similarCommandInList(const QString & cmd,const QStringList & list)1092 bool similarCommandInList(const QString &cmd, const QStringList &list)
1093 {
1094 	if (list.contains(cmd)) return true;
1095 	//compare the executable base name with all base names in the list
1096 	//(could be made faster by caching the list base names, but it is not really an issue)
1097 	QString fullCmd = CommandInfo::getProgramNameUnquoted(cmd).replace(QDir::separator(), '/');
1098 #ifdef Q_OS_WIN
1099 	fullCmd = fullCmd.toLower();
1100 	if (fullCmd.endsWith(".exe")) fullCmd = fullCmd.left(fullCmd.length() - 4);
1101 #endif
1102 	int lastPathSep = fullCmd.lastIndexOf('/');
1103 	QString relCmd = lastPathSep < 0 ? fullCmd : fullCmd.mid(lastPathSep + 1);
1104 	foreach (const QString &listCmd, list) {
1105 		QString fullListCmd = CommandInfo::getProgramNameUnquoted(listCmd).replace(QDir::separator(), '/');
1106 #ifdef Q_OS_WIN
1107 		fullListCmd = fullListCmd.toLower();
1108 		if (fullListCmd.endsWith(".exe")) fullListCmd = fullListCmd.left(fullListCmd.length() - 4);
1109 #endif
1110 		if (fullCmd == fullListCmd) return true;
1111 		int lastPathSepListCmd = fullListCmd.lastIndexOf('/');
1112 		QString relListCmd = lastPathSepListCmd < 0 ? fullListCmd : fullListCmd.mid(lastPathSepListCmd + 1);
1113 		if (lastPathSep < 0 || lastPathSepListCmd < 0) //do not compare relative, if both are absolute paths
1114 			if (relCmd == relListCmd) return true;
1115 	}
1116 	return false;
1117 }
1118 
getSingleCommandFlags(const QString & subcmd) const1119 RunCommandFlags BuildManager::getSingleCommandFlags(const QString &subcmd) const
1120 {
1121 	int result = 0;
1122 	if (similarCommandInList(subcmd, latexCommands)) result |= RCF_COMPILES_TEX;
1123 	if (similarCommandInList(subcmd, pdfCommands)) result |= RCF_CHANGE_PDF;
1124 	if (similarCommandInList(subcmd, rerunnableCommands)) result |= RCF_RERUNNABLE;
1125 	if (stdoutCommands.contains(subcmd)) result |= RCF_SHOW_STDOUT;
1126 	bool isAcrobat = false;
1127 #ifdef Q_OS_WIN
1128 	isAcrobat = subcmd.contains("Acrobat.exe") || subcmd.contains("AcroRd32.exe");
1129 #endif
1130 
1131 	if (viewerCommands.contains(subcmd) && !isAcrobat && singleViewerInstance) result |= RCF_SINGLE_INSTANCE;
1132 	return static_cast<RunCommandFlags>(result);
1133 }
1134 
hasCommandLine(const QString & program)1135 bool BuildManager::hasCommandLine(const QString &program)
1136 {
1137 	for (CommandMapping::const_iterator it = commands.constBegin(), end = commands.constEnd(); it != end; ++it)
1138 		if (it.value().commandLine == program) return true;
1139 	return false;
1140 }
1141 
1142 #if defined(Q_OS_MAC)
1143 
getCommandLineViewDvi()1144 QString getCommandLineViewDvi()
1145 {
1146 	return "open %.dvi > /dev/null";
1147 }
1148 
getCommandLineViewPs()1149 QString getCommandLineViewPs()
1150 {
1151 	return "open %.ps > /dev/null";
1152 }
1153 
getCommandLineViewPdfExternal()1154 QString getCommandLineViewPdfExternal()
1155 {
1156 	return "open %.pdf > /dev/null";
1157 }
1158 
getCommandLineGhostscript()1159 QString getCommandLineGhostscript()
1160 {
1161 	return "";
1162 }
1163 
1164 #elif defined(Q_OS_WIN32)
1165 
getCommandLineViewDvi()1166 QString getCommandLineViewDvi()
1167 {
1168 	const QString yapOptions = " -1 -s @?\"c:ame \"?am.dvi\"";
1169 	QString def = W32_FileAssociation(".dvi");
1170 	if (!def.isEmpty()) {
1171 		if (def.contains("yap.exe")) {
1172 			def = def.trimmed();
1173 			if (def.endsWith("\"?am.dvi\"")) {
1174 				def.replace("\"?am.dvi\"", yapOptions);
1175 			} else if (def.endsWith("?am.dvi")) {
1176 				def.replace("?am.dvi", yapOptions);
1177 			} else if (def.endsWith(" /dde")) {
1178 				def.replace(" /dde", yapOptions);
1179 			}
1180 		}
1181 		return def;
1182 	}
1183 	def = searchBaseCommand("yap", yapOptions); //miktex
1184 	if (!def.isEmpty()) return def;
1185 	def = searchBaseCommand("dviout", "%.dvi"); //texlive
1186 	if (!def.isEmpty()) return def;
1187 
1188 	if (QFileInfo("C:/texmf/miktex/bin/yap.exe").exists())
1189 		return "C:/texmf/miktex/bin/yap.exe " + yapOptions;
1190 
1191 	return "";
1192 }
1193 
getCommandLineViewPs()1194 QString getCommandLineViewPs()
1195 {
1196 	QString def = W32_FileAssociation(".ps");
1197 	if (!def.isEmpty())
1198 		return def;
1199 
1200 	QString livePath = getTeXLiveWinBinPath();
1201 	if (!livePath.isEmpty())
1202 		if (QFileInfo(livePath + "psv.exe").exists())
1203 			return "\"" + livePath + "psv.exe\"  \"?am.ps\"";
1204 
1205 
1206 	QString gsDll = findGhostscriptDLL().replace("/", "\\"); //gsview contains gs so x
1207 	int pos;
1208 	while ((pos = gsDll.lastIndexOf("\\")) > -1) {
1209 		gsDll = gsDll.mid(0, pos + 1);
1210 		if (QFileInfo(gsDll + "gsview32.exe").exists())
1211 			return "\"" + gsDll + "gsview32.exe\" -e \"?am.ps\"";
1212 		if (QFileInfo(gsDll + "gsview.exe").exists())
1213 			return "\"" + gsDll + "gsview.exe\" -e \"?am.ps\"";
1214 		gsDll = gsDll.mid(0, pos);
1215 	}
1216 
1217 	foreach (const QString &p, getProgramFilesPaths())
1218 		if (QFile::exists(p + "Ghostgum/gsview/gsview32.exe"))
1219 			return "\"" + p + "Ghostgum/gsview/gsview32.exe\" -e \"?am.ps\"";
1220 	return "";
1221 }
1222 
getCommandLineViewPdfExternal()1223 QString getCommandLineViewPdfExternal()
1224 {
1225 	QString def = W32_FileAssociation(".pdf");
1226 	if (!def.isEmpty())
1227 		return def;
1228 
1229 	foreach (const QString &p, getProgramFilesPaths())
1230 		if (QDir(p + "Adobe").exists()) {
1231 			QDirIterator it(p + "Adobe", QStringList() << "AcroRd32.exe", QDir::Files, QDirIterator::Subdirectories);
1232 			if (it.hasNext()) return "\"" + QDir::toNativeSeparators(it.next()) + "\" \"?am.pdf\"";
1233 		}
1234 	return "";
1235 }
1236 
getCommandLineGhostscript()1237 QString getCommandLineGhostscript()
1238 {
1239 	const QString gsArgs = " \"?am.ps\"";
1240 	QString livePath = getTeXLiveWinBinPath();
1241 	if (!livePath.isEmpty()) {
1242 		if (QFileInfo(livePath + "rungs.exe").exists())
1243 			return "\"" + livePath + "rungs.exe\"" + gsArgs;
1244 		if (QFileInfo(livePath + "rungs.bat").exists()) //tl 2008 (?)
1245 			return "\"" + livePath + "rungs.bat\"" + gsArgs;
1246 	}
1247 	QString dll = findGhostscriptDLL().replace("gsdll32.dll", "gswin32c.exe", Qt::CaseInsensitive);
1248 	if (dll.endsWith("gswin32c.exe")) return "\"" + dll + "\"" + gsArgs;
1249 	else if (QFileInfo("C:/Program Files/gs/gs8.64/bin/gswin32c.exe").exists())  //old behaviour
1250 		return "\"C:/Program Files/gs/gs8.64/bin/gswin32c.exe\"" + gsArgs;
1251 	else if (QFileInfo("C:/Program Files/gs/gs8.63/bin/gswin32c.exe").exists())  //old behaviour
1252 		return "\"C:/Program Files/gs/gs8.63/bin/gswin32c.exe\"" + gsArgs;
1253 	else if (QFileInfo("C:/Program Files/gs/gs8.61/bin/gswin32c.exe").exists())
1254 		return "\"C:/Program Files/gs/gs8.61/bin/gswin32c.exe\"" + gsArgs;
1255 	return "";
1256 }
1257 
1258 #elif defined(Q_WS_X11) || defined(Q_OS_LINUX) || defined(Q_OS_HAIKU)
1259 
1260 // xdvi %.dvi  -sourceposition @:%.tex
1261 // kdvi "file:%.dvi#src:@ %.tex"
getCommandLineViewDvi()1262 QString getCommandLineViewDvi()
1263 {
1264 	return "xdg-open %.dvi > /dev/null";
1265 }
1266 
getCommandLineViewPs()1267 QString getCommandLineViewPs()
1268 {
1269 	return "xdg-open %.ps > /dev/null";
1270 }
1271 
getCommandLineViewPdfExternal()1272 QString getCommandLineViewPdfExternal()
1273 {
1274 	return "xdg-open %.pdf > /dev/null";
1275 }
1276 
getCommandLineGhostscript()1277 QString getCommandLineGhostscript()
1278 {
1279 	return "";
1280 }
1281 
1282 #else
1283 
1284 #warning Unrecognized OS. Default viewers will probably be wrong
1285 
getCommandLineViewDvi()1286 QString getCommandLineViewDvi()
1287 {
1288 	return "xdvi %.dvi > /dev/null";
1289 }
1290 
getCommandLineViewPs()1291 QString getCommandLineViewPs()
1292 {
1293 	return "gv %.ps > /dev/null";
1294 }
1295 
getCommandLineViewPdfExternal()1296 QString getCommandLineViewPdfExternal()
1297 {
1298 	return "xpdf %.pdf > /dev/null";
1299 }
1300 
getCommandLineGhostscript()1301 QString getCommandLineGhostscript()
1302 {
1303 	return "";
1304 }
1305 
1306 #endif
1307 
1308 
1309 bool BuildManager_hadSuccessfulProcessStart;
1310 
registerOptions(ConfigManagerInterface & cmi)1311 void BuildManager::registerOptions(ConfigManagerInterface &cmi)
1312 {
1313 	cmi.registerOption("Tools/Quick Mode", &deprecatedQuickmode, -1);
1314 	cmi.registerOption("Tools/Max Expanding Nesting Deep", &maxExpandingNestingDeep, 10);
1315 	Q_ASSERT(sizeof(dvi2pngMode) == sizeof(int));
1316     cmi.registerOption("Tools/Dvi2Png Mode", reinterpret_cast<int *>(&dvi2pngMode), 3);
1317     cmi.registerOption("Files/Save Files Before Compiling", reinterpret_cast<int *>(&saveFilesBeforeCompiling), static_cast<int>(SFBC_ONLY_NAMED));
1318 	cmi.registerOption("Preview/Remove Beamer Class", &previewRemoveBeamer, true);
1319 	cmi.registerOption("Preview/Precompile Preamble", &previewPrecompilePreamble, true);
1320 
1321 	cmi.registerOption("Tools/Automatic Rerun Commands", &autoRerunCommands, "compile|latex|pdflatex|lualatex|xelatex");
1322 
1323 	cmi.registerOption("User/ToolNames", &deprecatedUserToolNames, QStringList());
1324 	cmi.registerOption("User/Tools", &deprecatedUserToolCommands, QStringList());
1325 
1326 	cmi.registerOption("Tools/Display Names", &userToolDisplayNames, QStringList());
1327 	cmi.registerOption("Tools/User Order", &userToolOrder, QStringList());
1328 	cmi.registerOption("Tools/Preview Compile Time Out", &previewCompileTimeOut, 15000); //hidden option, 15s predefined
1329 
1330 	cmi.registerOption("Tools/Had Successful Process Start", &BuildManager_hadSuccessfulProcessStart, false);
1331 }
1332 
removeDuplicateUserTools(QStringList & userToolOrder,QStringList & userToolDisplayNames)1333 void removeDuplicateUserTools(QStringList &userToolOrder, QStringList &userToolDisplayNames)
1334 {
1335 	// workaround to cleanup duplicates in usertools https://sourceforge.net/p/texstudio/discussion/907839/
1336 	// needed for some time even after that fix will be in place to catch the duplicates already created by previous versions
1337 	int i = 0;
1338 	QSet<QString> visitedTools;
1339 	while (i < qMin(userToolOrder.size(), userToolDisplayNames.size())) {
1340 		QString tool = userToolOrder[i];
1341 		if (visitedTools.contains(tool)) {
1342 			userToolOrder.removeAt(i);
1343 			userToolDisplayNames.removeAt(i);
1344 		} else {
1345 			visitedTools.insert(tool);
1346 			i++;
1347 		}
1348 	}
1349 }
1350 
readSettings(QSettings & settings)1351 void BuildManager::readSettings(QSettings &settings)
1352 {
1353 	QStringList rerunCommandsUnexpanded = autoRerunCommands.split("|");
1354 	for (int i = 0; i < rerunCommandsUnexpanded.size(); i++)
1355 		if (rerunCommandsUnexpanded[i].startsWith(TXS_CMD_PREFIX))
1356 			rerunCommandsUnexpanded[i] = rerunCommandsUnexpanded[i].mid(TXS_CMD_PREFIX.size());
1357 
1358 	removeDuplicateUserTools(userToolOrder, userToolDisplayNames);
1359 	settings.beginGroup("Tools");
1360 	settings.beginGroup("Commands");
1361 	QStringList cmds = settings.childKeys();
1362 
1363 	foreach (const QString &id, cmds) {
1364 		QString cmd = settings.value(id).toString();
1365 		CommandMapping::iterator it = commands.find(id);
1366 		if (it == commands.end()) {
1367 			// command not known by default -> user command
1368 			QString displayName(id);
1369 			int idx = userToolOrder.indexOf(id);
1370 			if (idx >= 0 && idx < userToolDisplayNames.length()) {
1371 				displayName = userToolDisplayNames[idx];
1372 			}
1373 			registerCommand(id, "", displayName, "", "", nullptr, true).commandLine = cmd;
1374 		} else {
1375 			// default command
1376 			it.value().commandLine = cmd;
1377 		}
1378 	}
1379 	settings.endGroup();
1380 	settings.endGroup();
1381 
1382 	//import old or choose default
1383 	for (CommandMapping::iterator it = commands.begin(), end = commands.end(); it != end; ++it) {
1384 		CommandInfo &cmd = it.value();
1385 		cmd.rerunCompiler = rerunCommandsUnexpanded.contains(cmd.id);
1386 		if (!cmd.commandLine.isEmpty()) continue;
1387 		if (!cmd.deprecatedConfigName.isEmpty()) {
1388 			QString import = settings.value(it.value().deprecatedConfigName).toString();
1389 			if (cmd.id == "quick") {
1390 				if (deprecatedQuickmode == 8)
1391 					cmd.commandLine = import;
1392 			} else cmd.commandLine = import;
1393 		}
1394 		if (!cmd.commandLine.isEmpty()) continue;
1395 		if (cmd.id == "quick") {
1396 			if (deprecatedQuickmode >= 1 && deprecatedQuickmode < cmd.metaSuggestionList.size() )
1397 				cmd.commandLine = cmd.metaSuggestionList[deprecatedQuickmode];
1398 			continue;
1399 		}
1400         cmd.commandLine = cmd.guessCommandLine();
1401 	}
1402 	if (commands.value("quick").commandLine.isEmpty()) {
1403 		//Choose suggestion that actually exists
1404 		CommandInfo &quick = commands.find("quick").value();
1405 		for (int i = 0; i < quick.metaSuggestionList.size() - 1; i++) {
1406 			QString referenced = quick.metaSuggestionList[i];
1407 			if (referenced.isEmpty()) continue;
1408 			QStringList subCommands = referenced.split("|");
1409 			bool hasAll = true;
1410 			foreach (const QString &s, subCommands) {
1411 				QString trimmed = s.trimmed();
1412 				trimmed.remove(0, TXS_CMD_PREFIX.length());
1413 				if (commands.value(trimmed).commandLine.isEmpty()) {
1414 					hasAll = false;
1415 					break;
1416 				}
1417 			}
1418 			if (hasAll) {
1419 				quick.commandLine = quick.metaSuggestionList[i];
1420 				break;
1421 			}
1422 		}
1423 		deprecatedQuickmode = -2;
1424 	}
1425 
1426 	int md = dvi2pngMode;
1427 #ifdef NO_POPPLER_PREVIEW
1428 	if (md == DPM_EMBEDDED_PDF)
1429 		md = -1;
1430 #endif
1431 	if (md < 0) {
1432 		if (isCommandDirectlyDefined(CMD_DVIPNG)) dvi2pngMode = DPM_DVIPNG; //best/fastest mode
1433 		else if (isCommandDirectlyDefined(CMD_DVIPS) && isCommandDirectlyDefined(CMD_GS)) dvi2pngMode = DPM_DVIPS_GHOSTSCRIPT; //compatible mode
1434 		else dvi2pngMode = DPM_DVIPNG; //won't work
1435 	}
1436 
1437 	setAllCommands(commands, userToolOrder);
1438 }
1439 
saveSettings(QSettings & settings)1440 void BuildManager::saveSettings(QSettings &settings)
1441 {
1442 	QStringList order = getCommandsOrder();
1443 	userToolOrder.clear();
1444 	userToolDisplayNames.clear();
1445 	settings.beginGroup("Tools");
1446 	settings.beginGroup("Commands");
1447 	settings.remove("");
1448 	QStringList rerunCmds;
1449 	for (int i = 0; i < order.size(); i++) {
1450 		CommandMapping::iterator it = commands.find(order[i]);
1451 		if (it == commands.end()) continue;
1452 		settings.setValue(it->id, it->commandLine);
1453 		if (it->user) {
1454 			userToolDisplayNames << it->displayName;
1455 			userToolOrder << it->id;
1456 		}
1457 		if (it->rerunCompiler) {
1458 			rerunCmds << it->id;
1459 		}
1460 	}
1461 	autoRerunCommands = rerunCmds.join("|");
1462 	settings.endGroup();
1463     settings.endGroup();
1464 }
1465 /*!
1466  * \brief reset command lines for all commands to default with texPath as default path
1467  * \param texPath
1468  */
resetDefaultCommands(const QString texPath)1469 void BuildManager::resetDefaultCommands(const QString texPath)
1470 {
1471     for (CommandMapping::iterator it = commands.begin(), end = commands.end(); it != end; ++it) {
1472         CommandInfo &cmd = it.value();
1473         if(cmd.meta) continue; // don't reset meta commands
1474         cmd.commandLine=cmd.guessCommandLine(texPath);
1475     }
1476 }
1477 
checkLatexConfiguration(bool & noWarnAgain)1478 void BuildManager::checkLatexConfiguration(bool &noWarnAgain)
1479 {
1480 	if (commands.contains("pdflatex") && commands["pdflatex"].commandLine.isEmpty()) {
1481 		QString message = tr("No LaTeX distribution was found on your system. As a result, the corresponding commands are not configured. This means, that you cannot compile your documents to the desired output format (e.g. pdf).");
1482 
1483 #ifdef Q_OS_WIN
1484 		message += "<br><br>"
1485 		           + tr("Popular LaTeX distributions on Windows are %1 and %2.").arg("<a href='http://miktex.org/'>MiKTeX</a>").arg("<a href='https://www.tug.org/texlive/'>TeX Live</a>")
1486 		           + "<br><br>"
1487 		           + tr("If you intend to work with LaTeX, you'll most certainly want to install one of those.");
1488 #elif defined(Q_OS_MAC)
1489 		message += "<br><br>"
1490 		           + tr("A popular LaTeX distribution on OSX is %1.").arg("<a href='https://tug.org/mactex/'>MacTeX</a>")
1491 		           + "<br><br>"
1492 		           + tr("If you intend to work with LaTeX, you'll most certainly want to install it.");
1493 #else
1494 		message += "<br><br>"
1495 		           + tr("If you intend to work with LaTeX, you'll most certainly want to install a LaTeX distribution.");
1496 #endif
1497 		UtilsUi::txsWarning(message, noWarnAgain);
1498 	}
1499 }
1500 
runCommand(const QString & unparsedCommandLine,const QFileInfo & mainFile,const QFileInfo & currentFile,int currentLine,QString * buffer,QTextCodec * codecForBuffer,QString * errorMsg)1501 bool BuildManager::runCommand(const QString &unparsedCommandLine, const QFileInfo &mainFile, const QFileInfo &currentFile, int currentLine, QString *buffer, QTextCodec *codecForBuffer , QString *errorMsg)
1502 {
1503 	if (waitingForProcess()) return false;
1504 
1505     emit clearLogs();
1506 
1507 	if (unparsedCommandLine.isEmpty()) {
1508 		emit processNotification(tr("Error: No command given"));
1509 		return false;
1510 	}
1511 	ExpandingOptions options(mainFile, currentFile, currentLine);
1512 	ExpandedCommands expansion = expandCommandLine(unparsedCommandLine, options);
1513 	if (options.canceled) return false;
1514 	if (!checkExpandedCommands(expansion)) return false;
1515 
1516 	bool latexCompiled = false, pdfChanged = false;
1517 	for (int i = 0; i < expansion.commands.size(); i++) {
1518 		latexCompiled |= expansion.commands[i].flags & RCF_COMPILES_TEX;
1519 		pdfChanged |= expansion.commands[i].flags & RCF_CHANGE_PDF;
1520 		if (buffer || i != expansion.commands.size() - 1)
1521 			expansion.commands[i].flags |= RCF_WAITFORFINISHED; // don't let buffer be destroyed before command is finished
1522 	}
1523 	if (latexCompiled) {
1524 		ExpandedCommands temp = expandCommandLine(CMD_INTERNAL_PRE_COMPILE, options);
1525 		for (int i = temp.commands.size() - 1; i >= 0; i--) expansion.commands.prepend(temp.commands[i]);
1526 	}
1527 
1528 	bool asyncPdf = !(expansion.commands.last().flags & RCF_WAITFORFINISHED) && (expansion.commands.last().flags & RCF_CHANGE_PDF);
1529 
1530 	emit beginRunningCommands(expansion.primaryCommand, latexCompiled, pdfChanged, asyncPdf);
1531     bool result = runCommandInternal(expansion, mainFile, buffer, codecForBuffer,errorMsg);
1532 	emit endRunningCommands(expansion.primaryCommand, latexCompiled, pdfChanged, asyncPdf);
1533 	return result;
1534 }
1535 
checkExpandedCommands(const ExpandedCommands & expansion)1536 bool BuildManager::checkExpandedCommands(const ExpandedCommands &expansion)
1537 {
1538 	if (expansion.commands.isEmpty()) {
1539 		emit processNotification(tr("Error: No command expanded"));
1540 		if (!BuildManager_hadSuccessfulProcessStart) {
1541 			emit processNotification("<br>" + tr("<b>Make sure that you have installed a (La)TeX distribution</b> e.g. MiKTeX or TeX Live, and have set the correct paths to this distribution on the command configuration page.<br>"
1542 			                                     "A (La)TeX editor like TeXstudio cannot work without the (La)TeX commands provided by such a distribution."));
1543 		}
1544 		return false;
1545 	}
1546 
1547 	// check if one command in the list is empty (expansion produced an error, e.g. txs:quick and compile is undefined
1548 	foreach (const CommandToRun elem, expansion.commands) {
1549 		if (elem.command.isEmpty()) {
1550 			emit processNotification(tr("Error: One command expansion invalid.") +
1551 			                         QString("\n    %1: %2").arg(tr("Parent Command"), elem.parentCommand) +
1552 			                         QString("\n    %1: %2").arg(tr("Primary Command"), expansion.primaryCommand));
1553 			if (!BuildManager_hadSuccessfulProcessStart) {
1554 				emit processNotification("<br>" + tr("<b>Make sure that you have installed a (La)TeX distribution</b> e.g. MiKTeX or TeX Live, and have set the correct paths to this distribution on the command configuration page.<br>"
1555 				                                     "A (La)TeX editor like TeXstudio cannot work without the (La)TeX commands provided by such a distribution."));
1556 			}
1557 			return false;
1558 		}
1559 	}
1560 	return true;
1561 }
1562 
runCommandInternal(const ExpandedCommands & expandedCommands,const QFileInfo & mainFile,QString * buffer,QTextCodec * codecForBuffer,QString * errorMsg)1563 bool BuildManager::runCommandInternal(const ExpandedCommands &expandedCommands, const QFileInfo &mainFile, QString *buffer, QTextCodec *codecForBuffer, QString *errorMsg)
1564 {
1565 	const QList<CommandToRun> &commands = expandedCommands.commands;
1566 
1567 	int remainingReRunCount = autoRerunLatex;
1568 	for (int i = 0; i < commands.size(); i++) {
1569 		CommandToRun cur = commands[i];
1570 		if (testAndRunInternalCommand(cur.command, mainFile))
1571 			continue;
1572 
1573 		bool singleInstance = cur.flags & RCF_SINGLE_INSTANCE;
1574 		if (singleInstance && runningCommands.contains(cur.command)) continue;
1575 		bool latexCompiler = cur.flags & RCF_COMPILES_TEX;
1576 		bool lastCommandToRun = i == commands.size() - 1;
1577 		bool waitForCommand = latexCompiler || (!lastCommandToRun && !singleInstance) || cur.flags & RCF_WAITFORFINISHED;
1578 
1579 		ProcessX *p = newProcessInternal(cur.command, mainFile, singleInstance);
1580 		REQUIRE_RET(p, false);
1581 		p->subCommandName = cur.parentCommand;
1582 		p->subCommandPrimary = expandedCommands.primaryCommand;
1583 		p->subCommandFlags = cur.flags;
1584         connect(p, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(emitEndRunningSubCommandFromProcessX(int)));
1585 
1586 
1587 		p->setStdoutBuffer(buffer);
1588         p->setStderrBuffer(errorMsg);
1589 		p->setStdoutCodec(codecForBuffer);
1590 
1591 		emit beginRunningSubCommand(p, expandedCommands.primaryCommand, cur.parentCommand, cur.flags);
1592 
1593         if (!waitForCommand) connect(p, SIGNAL(finished(int,QProcess::ExitStatus)), p, SLOT(deleteLater()));
1594 
1595 		p->startCommand();
1596 		if (!p->waitForStarted(1000)) return false;
1597 
1598 		if (latexCompiler || (!lastCommandToRun && !singleInstance) )
1599 			if (!waitForProcess(p)) {
1600 				p->deleteLater();
1601 				return false;
1602 			}
1603 
1604 		if (waitForCommand) { //what is this? does not really make any sense (waiting is done in the block above) and breaks multiple single-instance pdf viewer calls (30 sec delay)
1605 			p->waitForFinished();
1606 			p->deleteLater();
1607 		}
1608 
1609 		bool rerunnable = (cur.flags & RCF_RERUN) && (cur.flags & RCF_RERUNNABLE);
1610 		if (rerunnable || latexCompiler) {
1611 			LatexCompileResult result = LCR_NORMAL;
1612 			emit latexCompiled(&result);
1613 			if (result == LCR_ERROR) return false;
1614 			if (result == LCR_NORMAL || !rerunnable) continue;
1615 			if (remainingReRunCount <= 0) continue; //do not abort since the rerun condition might have been trigged accidentally
1616 			if (result == LCR_RERUN_WITH_BIBLIOGRAPHY) {
1617 				QString tempWaitForFinished; //if it does not wait on bibtex it will fail
1618 				runCommand(CMD_BIBLIOGRAPHY, mainFile, mainFile, 0, &tempWaitForFinished);
1619 				remainingReRunCount--;
1620 			}
1621 			REQUIRE_RET(result == LCR_RERUN || result == LCR_RERUN_WITH_BIBLIOGRAPHY, false);
1622 			remainingReRunCount--;
1623 			i--; //rerun
1624 			//qDebug() << "rerun";
1625 		}
1626 
1627 	}
1628 	return true;
1629 }
1630 
emitEndRunningSubCommandFromProcessX(int)1631 void BuildManager::emitEndRunningSubCommandFromProcessX(int)
1632 {
1633 	ProcessX *p = qobject_cast<ProcessX *>(sender());
1634 	REQUIRE(p); //p can be NULL (although sender() is not null) ! If multiple single instance viewers are in a command. Why? should not happen
1635 	emit endRunningSubCommand(p, p->subCommandPrimary, p->subCommandName, p->subCommandFlags);
1636 }
1637 
1638 
firstProcessOfDirectExpansion(const QString & command,const QFileInfo & mainFile,const QFileInfo & currentFile,int currentLine,bool nonstop)1639 ProcessX *BuildManager::firstProcessOfDirectExpansion(const QString &command, const QFileInfo &mainFile, const QFileInfo &currentFile, int currentLine,bool nonstop)
1640 {
1641 	ExpandingOptions options(mainFile, currentFile, currentLine);
1642 	if(nonstop){
1643 		options.nestingDeep=1; // tweak to avoid pop-up error messages
1644 	}
1645 	ExpandedCommands expansion = expandCommandLine(command, options);
1646 	if (options.canceled) return nullptr;
1647 
1648 	if (expansion.commands.isEmpty()) { return nullptr; }
1649 
1650 	foreach(CommandToRun elem,expansion.commands) {
1651 		if(elem.command.isEmpty()) {
1652 			return nullptr; // error in command expansion
1653 		}
1654 	}
1655 
1656 	return newProcessInternal(expansion.commands.first().command, mainFile);
1657 }
1658 
1659 //don't use this
newProcessInternal(const QString & cmd,const QFileInfo & mainFile,bool singleInstance)1660 ProcessX *BuildManager::newProcessInternal(const QString &cmd, const QFileInfo &mainFile, bool singleInstance)
1661 {
1662 	if (singleInstance && runningCommands.contains(cmd))
1663         return nullptr;
1664 
1665 	ProcessX *proc = new ProcessX(this, cmd, mainFile.absoluteFilePath());
1666 	connect(proc, SIGNAL(processNotification(QString)), SIGNAL(processNotification(QString)));
1667 	if (singleInstance) {
1668         connect(proc, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(singleInstanceCompleted(int))); //will free proc after the process has ended
1669 		runningCommands.insert(cmd, proc);
1670 	}
1671 	if (!mainFile.fileName().isEmpty())
1672 		proc->setWorkingDirectory(mainFile.absolutePath());
1673 	if (cmd.startsWith(TXS_CMD_PREFIX))
1674 		connect(proc, SIGNAL(startedX()), SLOT(runInternalCommandThroughProcessX()));
1675 
1676 	updatePathSettings(proc, resolvePaths(additionalSearchPaths));
1677 	return proc;
1678 }
1679 
waitForProcess(ProcessX * p)1680 bool BuildManager::waitForProcess(ProcessX *p)
1681 {
1682 	REQUIRE_RET(p, false);
1683 	REQUIRE_RET(!processWaitedFor, false);
1684 	// Waiting on a Qt event loop avoids spinlock and high CPU usage, and allows user interaction
1685 	// and UI responsiveness while compiling.
1686 	// We have to check the process running state before we start waiting for processFinished
1687 	// because it is possible that the process has already ended and we would wait forever.
1688 	// We have to start listening for processFinished before we check the running state in
1689 	// order to avoid a race condition.
1690 	QEventLoop loop;
1691 	connect(p, SIGNAL(processFinished()), &loop, SLOT(quit()));
1692 	if (p->isRunning()) {
1693 		processWaitedFor = p;
1694 		emit buildRunning(true);
1695 		loop.exec(); //exec will delay execution until the signal has arrived
1696 		emit buildRunning(false);
1697 		processWaitedFor = nullptr;
1698 	}
1699 	return true;
1700 }
1701 
waitingForProcess() const1702 bool BuildManager::waitingForProcess() const
1703 {
1704 	return processWaitedFor;
1705 }
1706 
killCurrentProcess()1707 void BuildManager::killCurrentProcess()
1708 {
1709 	if (!processWaitedFor) return;
1710 	processWaitedFor->kill();
1711     processWaitedFor = nullptr;
1712 }
1713 
createTemporaryFileName()1714 QString BuildManager::createTemporaryFileName()
1715 {
1716 	QTemporaryFile *temp = new QTemporaryFile(QDir::tempPath () + "/texstudio_XXXXXX.tex");
1717 	temp->open();
1718 	temp->setAutoRemove(false);
1719 	QString tempName = temp->fileName();
1720 	delete temp;
1721 	return tempName;
1722 }
1723 
addLaTeXInputPaths(ProcessX * p,const QStringList & paths)1724 void addLaTeXInputPaths(ProcessX *p, const QStringList &paths)
1725 {
1726 	if (paths.isEmpty()) return;
1727     static const QString SEP = onWin_Nix(";",":");
1728 	static const QStringList envNames = QStringList() << "TEXINPUTS" << "BIBINPUTS" << "BSTINPUTS" << "MFINPUTS" << "MPINPUTS" << "TFMFONTS";
1729 	QString addPath = paths.join(SEP) + SEP + "." + SEP;
1730 	QStringList env = p->environment();
1731 	env << QProcess::systemEnvironment();
1732 	foreach (const QString &envname, envNames) {
1733 		bool found = false;
1734 		for (int i = 0; i < env.size(); i++)
1735 			if (env[i].startsWith(envname + "=")) {
1736 				found = true;
1737 				env[i] = env[i] + SEP + addPath;
1738 				break;
1739 			}
1740 		if (!found)
1741 			env.append(envname + "=" + addPath);
1742 	}
1743 	p->setOverrideEnvironment(env);
1744 }
1745 
1746 //there are 3 ways to generate a preview png:
1747 //1. latex is called => dvipng is called after latex finished and converts the dvi
1748 //2. latex is called and dvipng --follow is called at the same time, and will manage the wait time on its own
1749 //3. latex is called => dvips converts .dvi to .ps => ghostscript is called and created final png
1750 //Then ghostscript to convert it to
preview(const QString & preamble,const PreviewSource & source,const QString & masterFile,QTextCodec * outputCodec)1751 void BuildManager::preview(const QString &preamble, const PreviewSource &source, const QString &masterFile, QTextCodec *outputCodec)
1752 {
1753 	QString tempPath = QDir::tempPath() + QDir::separator() + "." + QDir::separator();
1754 
1755 	//process preamble
1756 	QString preamble_mod = preamble;
1757     static const QRegularExpression beamerClass("^(\\s*%[^\\n]*\\n)*\\s*\\\\documentclass(\\[[^\\]]*\\])?\\{beamer\\}"); //detect the usage of the beamer class
1758 	if (previewRemoveBeamer && preamble_mod.contains(beamerClass)) {
1759 		//dvipng is very slow (>14s) and ghostscript is slow (1.4s) when handling beamer documents,
1760 		//after setting the class to article dvipng runs in 77ms
1761 		preamble_mod.remove(beamerClass);
1762 		preamble_mod.insert(0, "\\documentclass{article}\n\\usepackage{beamerarticle}");
1763         // remove \mode... as well (#1125)
1764         QRegularExpression beamerMode("\\\\mode.*\n");
1765         //beamerMode.setMinimal(true);
1766         preamble_mod.remove(beamerMode);
1767 	}
1768 
1769 	QString masterDir = QFileInfo(masterFile).dir().absolutePath();
1770 	QStringList addPaths;
1771 	addPaths << masterDir;
1772 	if (preamble_mod.contains("\\usepackage{import}")) {
1773 		QStringList imports = regExpFindAllMatches(preamble_mod, QRegExp("\\\\subimport\\{([^}\n]*)\\}\\{[^}\n]*\\}"), 1);
1774 		imports.sort();
1775 		for (int i = imports.size() - 1; i > 0; i--)
1776 			if (imports[i] == imports[i - 1]) imports.removeAt(i);
1777 		foreach (const QString &dir, imports)
1778 			addPaths << masterDir + QDir::separator() + dir;
1779 	}
1780 
1781 	QString preambleFormatFile;
1782 	if (previewPrecompilePreamble) {
1783 		preambleFormatFile = preambleHash.value(preamble_mod, "");
1784 		if (preambleFormatFile != "<failed>") {
1785 			if (!preambleFormatFile.isEmpty())
1786 				if (!QFile::exists(tempPath + preambleFormatFile + ".fmt"))
1787 					preambleFormatFile = "";
1788 			if (preambleFormatFile.isEmpty()) {
1789 				//write preamble
1790 				QTemporaryFile *tf = new QTemporaryFile(tempPath + "hXXXXXX.tex");
1791 				REQUIRE(tf);
1792 				tf->open();
1793 				QTextStream out(tf);
1794                 if (outputCodec) {
1795                     out << outputCodec->fromUnicode(preamble_mod);
1796                 }else{
1797                     out << preamble_mod;
1798                 }
1799 				tf->setAutoRemove(false);
1800 				tf->close();
1801 
1802 				//compile
1803 				QFileInfo fi(*tf);
1804 				preambleFormatFile = fi.completeBaseName();
1805 				previewFileNames.append(fi.absoluteFilePath());
1806 				ProcessX *p = nullptr;
1807 				if (dvi2pngMode == DPM_EMBEDDED_PDF) {
1808                     p = newProcessInternal(QString("%1 -interaction=nonstopmode -ini \"&pdflatex %2 \\dump\"").arg(getCommandInfo(CMD_PDFLATEX).getProgramName(),preambleFormatFile), QFileInfo(tf->fileName())); //no delete! goes automatically
1809 				} else {
1810                     p = newProcessInternal(QString("%1 -interaction=nonstopmode -ini \"&latex %2 \\dump\"").arg(getCommandInfo(CMD_LATEX).getProgramName(),preambleFormatFile), QFileInfo(tf->fileName())); //no delete! goes automatically
1811 				}
1812 
1813 				REQUIRE(p);
1814 				addLaTeXInputPaths(p, addPaths);
1815 				p->setProperty("preamble", preamble_mod);
1816 				p->setProperty("preambleFile", preambleFormatFile);
1817                 connect(p, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(preamblePrecompileCompleted(int)));
1818                 connect(p, SIGNAL(finished(int,QProcess::ExitStatus)), p, SLOT(deleteLater()));
1819 				tf->setParent(p); //free file when process is deleted
1820 
1821 				p->startCommand();
1822 
1823 				if (p->waitForStarted()) {
1824 					if (p->waitForFinished(800)) {
1825 						if (p->exitStatus() == QProcess::NormalExit && p->exitCode() == 0) {
1826 							preambleHash.insert(preamble_mod, preambleFormatFile);
1827 						} else {
1828 							preambleHash.insert(preamble_mod, "<failed>");
1829 							preambleFormatFile = "";
1830 						}
1831 					} else
1832 						preambleFormatFile = ""; //wait + normal compile while waiting
1833 
1834 				} else preambleFormatFile = ""; //compiling failed
1835 				//delete tf; // tex file needs to be freed
1836 			}
1837 		} else preambleFormatFile = "";
1838 	}
1839 
1840 	// write to temp file
1841 	// (place /./ after the temporary directory because it fails otherwise with qt4.3 on win and the tempdir "t:")
1842 	QTemporaryFile *tf = new QTemporaryFile(tempPath + "XXXXXX.tex");
1843 	if (!tf) return;
1844 	tf->open();
1845 
1846 	QTextStream out(tf);
1847     if (outputCodec) {
1848         if (preambleFormatFile.isEmpty()) out << outputCodec->fromUnicode(preamble_mod);
1849         else out << outputCodec->fromUnicode("%&" + preambleFormatFile + "\n");
1850         out << outputCodec->fromUnicode("\n\\begin{document}\n" + source.text + "\n\\end{document}\n");
1851     }else{
1852         if (preambleFormatFile.isEmpty()) out << preamble_mod;
1853         else out << "%&" << preambleFormatFile << "\n";
1854         out << "\n\\begin{document}\n" << source.text << "\n\\end{document}\n";
1855     }
1856 	// prepare commands/filenames
1857 	QFileInfo fi(*tf);
1858 	QString ffn = fi.absoluteFilePath();
1859 	previewFileNames.append(ffn);
1860 	previewFileNameToSource.insert(ffn, source);
1861 	tf->setAutoRemove(false);
1862 	tf->close();
1863 	delete tf; // tex file needs to be freed
1864     ProcessX *p1 = nullptr;
1865 	if (dvi2pngMode == DPM_EMBEDDED_PDF) {
1866 		// start conversion
1867 		// tex -> dvi
1868 		p1 = firstProcessOfDirectExpansion(CMD_PDFLATEX, QFileInfo(ffn)); //no delete! goes automatically
1869 	} else {
1870 		// start conversion
1871 		// tex -> dvi
1872 		p1 = firstProcessOfDirectExpansion(CMD_LATEX, QFileInfo(ffn)); //no delete! goes automatically
1873 	}
1874 	if (!p1) return; // command failed, not set ?
1875 	addLaTeXInputPaths(p1, addPaths);
1876     connect(p1, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(latexPreviewCompleted(int)));
1877 	p1->startCommand();
1878 	QTimer::singleShot(previewCompileTimeOut, p1, SLOT(kill()));
1879 
1880 	if (dvi2pngMode == DPM_DVIPNG_FOLLOW) {
1881 		p1->waitForStarted();
1882 		// dvi -> png
1883 		//follow mode is a tricky features which allows dvipng to run while tex isn't finished
1884         ProcessX *p2 = firstProcessOfDirectExpansion("txs:///dvipng/[--follow]", QFileInfo(ffn));
1885 		if (!p2) return; // command failed, not set ?
1886 		p1->setProperty("proc",QVariant::fromValue(p2));
1887         connect(p1,SIGNAL(finished(int,QProcess::ExitStatus)),this,SLOT(PreviewLatexCompleted(int)));
1888 		if (!p1->overrideEnvironment().isEmpty()) p2->setOverrideEnvironment(p1->overrideEnvironment());
1889         connect(p2, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(conversionPreviewCompleted(int)));
1890 		p2->startCommand();
1891 	}
1892 }
1893 
clearPreviewPreambleCache()1894 void BuildManager::clearPreviewPreambleCache()
1895 {
1896 	QHash<QString, QString>::const_iterator it = preambleHash.constBegin();
1897 	while (it != preambleHash.constEnd()) {
1898 		removePreviewFiles(it.value());
1899 		previewFileNames.removeAll(it.value());
1900 		++it;
1901 	}
1902 	preambleHash.clear();
1903 }
1904 
isCommandDirectlyDefined(const QString & id) const1905 bool BuildManager::isCommandDirectlyDefined(const QString &id) const
1906 {
1907 	if (id.startsWith(TXS_CMD_PREFIX)) return isCommandDirectlyDefined(id.mid(TXS_CMD_PREFIX.length()));
1908 	if (internalCommands.contains(TXS_CMD_PREFIX + id)) return true;
1909 	return !commands.value(id).commandLine.isEmpty();
1910 }
1911 
getCommandInfo(const QString & id) const1912 CommandInfo BuildManager::getCommandInfo(const QString &id) const
1913 {
1914 	if (id.startsWith(TXS_CMD_PREFIX)) return getCommandInfo(id.mid(TXS_CMD_PREFIX.length()));
1915 	CommandMapping::const_iterator it = commands.constFind(id);
1916 	if (it == commands.end()) return CommandInfo();
1917 	return *it;
1918 }
1919 
editCommandList(const QString & list,const QString & excludeId)1920 QString BuildManager::editCommandList(const QString &list, const QString &excludeId)
1921 {
1922 	QStringList ids = commandSortingsOrder, names, commands;
1923 	ids << userToolOrder;
1924 	ids.insert(ids.indexOf("view-pdf-external"), CMD_VIEW_PDF_INTERNAL);
1925 	ids << CMD_CONDITIONALLY_RECOMPILE_BIBLIOGRAPHY;
1926 	ids.removeAll(excludeId);
1927 	ids.removeAll(TXS_CMD_PREFIX + excludeId);
1928 	for (int i = 0; i < ids.size(); i++) {
1929 		CommandInfo ci = getCommandInfo(ids[i]);
1930 		names << (ci.displayName.isEmpty() ? ids[i] : ci.displayName);
1931 		if (names.last() == CMD_VIEW_PDF_INTERNAL) names.last() = tr("Internal Pdf Viewer");
1932 		commands << (ci.commandLine.isEmpty() ? ids[i] : ci.commandLine);
1933 		if (!ids[i].startsWith(TXS_CMD_PREFIX)) ids[i] = TXS_CMD_PREFIX + ids[i];
1934 	}
1935 
1936     UserQuickDialog uqd(nullptr, ids, names, commands);
1937 	uqd.setCommandList(list);
1938 	if (uqd.exec() == QDialog::Accepted) return uqd.getCommandList();
1939 	else return list;
1940 }
1941 
getAllCommands()1942 CommandMapping BuildManager::getAllCommands()
1943 {
1944 	return commands;
1945 }
1946 
getCommandsOrder()1947 QStringList BuildManager::getCommandsOrder()
1948 {
1949 	QStringList order = commandSortingsOrder;
1950 	order << userToolOrder;
1951     //foreach (const QString &more, commands.keys())
1952     for(auto it=commands.keyBegin();it!=commands.keyEnd();++it){
1953         if (!order.contains(*it))
1954             order << *it;
1955     }
1956 	return order;
1957 }
1958 
setAllCommands(const CommandMapping & cmds,const QStringList & userOrder)1959 void BuildManager::setAllCommands(const CommandMapping &cmds, const QStringList &userOrder)
1960 {
1961 	this->commands = cmds;
1962 	this->userToolOrder = userOrder;
1963 
1964 	for (CommandMapping::iterator it = commands.begin(), end = commands.end(); it != end; ++it)
1965 		if (it.value().commandLine == tr("<unknown>"))
1966 			it.value().commandLine = "";
1967 
1968 	static QStringList latexCommandsUnexpanded, rerunnableCommandsUnexpanded, pdfCommandsUnexpanded, stdoutCommandsUnexpanded, viewerCommandsUnexpanded;
1969 	ConfigManagerInterface::getInstance()->registerOption("Tools/Kind/LaTeX", &latexCommandsUnexpanded, QStringList() << "latex" << "pdflatex" << "xelatex" << "lualatex" << "latexmk" << "compile");
1970 	ConfigManagerInterface::getInstance()->registerOption("Tools/Kind/Rerunnable", &rerunnableCommandsUnexpanded, QStringList() << "latex" << "pdflatex" << "xelatex" << "lualatex");
1971 	ConfigManagerInterface::getInstance()->registerOption("Tools/Kind/Pdf", &pdfCommandsUnexpanded, QStringList() << "pdflatex" << "xelatex" << "lualatex" << "latexmk" << "dvipdf" << "ps2pdf");
1972 	ConfigManagerInterface::getInstance()->registerOption("Tools/Kind/Stdout", &stdoutCommandsUnexpanded, QStringList() << "bibtex" << "biber" << "bibtex8" << "bibliography");
1973 	ConfigManagerInterface::getInstance()->registerOption("Tools/Kind/Viewer", &viewerCommandsUnexpanded, QStringList() << "view-pdf" << "view-ps" << "view-dvi" << "view-pdf-internal" << "view-pdf-external" << "view");
1974 
1975 	QList<QStringList *> lists = QList<QStringList *>() << &latexCommands << &rerunnableCommands << &pdfCommands << &stdoutCommands << &viewerCommands;
1976 	QList<QStringList *> listsUnexpanded = QList<QStringList *>() << &latexCommandsUnexpanded << &rerunnableCommandsUnexpanded << &pdfCommandsUnexpanded << &stdoutCommandsUnexpanded << &viewerCommandsUnexpanded;
1977 	Q_ASSERT(lists.size() == listsUnexpanded.size());
1978 	for (int i = 0; i < lists.size(); i++) {
1979 		QStringList *sl = lists[i];
1980 		*sl = *listsUnexpanded.at(i);
1981 		for (int i = 0; i < sl->size(); i++) {
1982 			Q_ASSERT(commands.contains((*sl)[i]) || (*sl)[i] == "view-pdf-internal");
1983 			(*sl)[i] = getCommandInfo((*sl)[i]).commandLine.trimmed();
1984 		}
1985 	}
1986 }
1987 
1988 /*!
1989  * Returns a best guess compiler for a string given in "% !TeX TS-program = [program]" \ "% !TeX program = [program]"
1990  */
guessCompilerFromProgramMagicComment(const QString & program)1991 QString BuildManager::guessCompilerFromProgramMagicComment(const QString &program)
1992 {
1993 	if (program == "latex") return BuildManager::CMD_LATEX;
1994     if (program == "pdflatex") return BuildManager::CMD_PDFLATEX;
1995     if (program == "xelatex") return BuildManager::CMD_XELATEX;
1996     if (program == "luatex" || program == "lualatex") return BuildManager::CMD_LUALATEX;
1997     if (program.startsWith("user")){
1998         bool user;
1999         QString cmd=getCommandLine(program,&user);
2000         if(user){
2001             return cmd;
2002         }
2003     }
2004 	return QString();
2005 
2006 }
2007 
2008 /*!
2009  * Returns a best guess viewer for a string given in "% !TeX TS-program = [program]" \ "% !TeX program = [program]"
2010  */
guessViewerFromProgramMagicComment(const QString & program)2011 QString BuildManager::guessViewerFromProgramMagicComment(const QString &program)
2012 {
2013 	if (program == "latex")
2014 		return BuildManager::CMD_VIEW_DVI;
2015 	else if (program == "pdflatex" || program == "xelatex" || program == "luatex" || program == "lualatex") {
2016 		return CMD_VIEW_PDF;
2017 	}
2018 	return QString();
2019 }
2020 
singleInstanceCompleted(int status)2021 void BuildManager::singleInstanceCompleted(int status)
2022 {
2023 	Q_UNUSED(status)
2024 	QObject *s = sender();
2025 	REQUIRE(s);
2026 	for (QMap<QString, ProcessX *>::iterator it = runningCommands.begin(), end = runningCommands.end(); it != end;)
2027 		if (it.value() == s)
2028 			it = runningCommands.erase(it);
2029 		else ++it;
2030 }
2031 
preamblePrecompileCompleted(int status)2032 void BuildManager::preamblePrecompileCompleted(int status)
2033 {
2034 	Q_UNUSED(status)
2035 	QProcess *p = qobject_cast<QProcess *>(sender());
2036 	REQUIRE(p);
2037 	if (p->exitCode() != 0 || p->exitStatus() != QProcess::NormalExit) {
2038 		preambleHash.insert(p->property("preamble").toString(), "<failed>");
2039 	} else
2040 		preambleHash.insert(p->property("preamble").toString(), p->property("preambleFile").toString());
2041 }
2042 
2043 //latex has finished the dvi creation
2044 //now either dvips or dvipng is necessary if not already running
latexPreviewCompleted(int status)2045 void BuildManager::latexPreviewCompleted(int status)
2046 {
2047 	Q_UNUSED(status)
2048 	if (dvi2pngMode == DPM_DVIPNG) {
2049 		ProcessX *p1 = qobject_cast<ProcessX *> (sender());
2050 		if (!p1) return;
2051 		// dvi -> png
2052         ProcessX *p2 = firstProcessOfDirectExpansion(CMD_DVIPNG, QFileInfo(p1->getFile()),QFileInfo(),0,true);
2053 		if (!p2) return; //dvipng does not work
2054 		//REQUIRE(p2);
2055 		if (!p1->overrideEnvironment().isEmpty()) p2->setOverrideEnvironment(p1->overrideEnvironment());
2056         connect(p2, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(conversionPreviewCompleted(int)));
2057 		p2->startCommand();
2058 	}
2059 	if (dvi2pngMode == DPM_DVIPS_GHOSTSCRIPT) {
2060 		ProcessX *p1 = qobject_cast<ProcessX *> (sender());
2061 		if (!p1) return;
2062 		// dvi -> ps
2063         ProcessX *p2 = firstProcessOfDirectExpansion("txs:///dvips/[-E]", QFileInfo(p1->getFile()),QFileInfo(),0,true);
2064 		if (!p2) return; //dvips does not work
2065 		//REQUIRE(p2);
2066 		if (!p1->overrideEnvironment().isEmpty()) p2->setOverrideEnvironment(p1->overrideEnvironment());
2067         connect(p2, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(dvi2psPreviewCompleted(int)));
2068 		p2->startCommand();
2069 	}
2070 	if (dvi2pngMode == DPM_EMBEDDED_PDF) {
2071 		ProcessX *p1 = qobject_cast<ProcessX *> (sender());
2072 		if (!p1) return;
2073 		QString processedFile = p1->getFile();
2074 		if (processedFile.endsWith(".tex"))
2075             processedFile = QDir::fromNativeSeparators(parseExtendedCommandLine("?am.tex", QFileInfo(processedFile)).constFirst());
2076 			// TODO: fromNativeSeparators is a workaround to fix bug
2077 			// yields different dir separators depending on the context. This should be fixed (which direction?).
2078 			// Test (on win): switch preview between dvipng and pdflatex
2079         QString fn = parseExtendedCommandLine("?am).pdf", QFileInfo(processedFile)).constFirst();
2080         if (QFileInfo::exists(fn)) {
2081 			emit previewAvailable(fn, previewFileNameToSource[processedFile]);
2082 		}
2083 	}
2084 }
2085 
2086 //dvi to ps conversion is finished, call ghostscript to make a useable png from it
dvi2psPreviewCompleted(int status)2087 void BuildManager::dvi2psPreviewCompleted(int status)
2088 {
2089 	Q_UNUSED(status)
2090 	ProcessX *p2 = qobject_cast<ProcessX *> (sender());
2091 	if (!p2) return;
2092 	// ps -> png, ghostscript is quite, safe, will create 24-bit png
2093     QString filePs = parseExtendedCommandLine("?am.ps", QFileInfo(p2->getFile())).constFirst();
2094     ProcessX *p3 = firstProcessOfDirectExpansion("txs:///gs/[-q][-dSAFER][-dBATCH][-dNOPAUSE][-sDEVICE=png16m][-dEPSCrop][-sOutputFile=\"?am)1.png\"]", QFileInfo(filePs),QFileInfo(),0,true);
2095 	if (!p3) return; //gs does not work
2096 	if (!p2->overrideEnvironment().isEmpty()) p3->setOverrideEnvironment(p2->overrideEnvironment());
2097     connect(p3, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(conversionPreviewCompleted(int)));
2098 	p3->startCommand();
2099 }
PreviewLatexCompleted(int status)2100 void BuildManager::PreviewLatexCompleted(int status){
2101 	if(status!=0){
2102 		// latex compile failed, kill dvipng
2103 		ProcessX *p1 = qobject_cast<ProcessX *> (sender());
2104 		ProcessX *p2=p1->property("proc").value<ProcessX *>();
2105 		p2->terminate();
2106 	}
2107 }
2108 
conversionPreviewCompleted(int status)2109 void BuildManager::conversionPreviewCompleted(int status)
2110 {
2111 	Q_UNUSED(status)
2112 	ProcessX *p2 = qobject_cast<ProcessX *> (sender());
2113 	if (!p2) return;
2114 	// put image in preview
2115 	QString processedFile = p2->getFile();
2116     if (processedFile.endsWith(".ps")) processedFile = parseExtendedCommandLine("?am.tex", QFileInfo(processedFile)).constFirst();
2117     QString fn = parseExtendedCommandLine("?am)1.png", QFileInfo(processedFile)).constFirst();
2118     if (QFileInfo::exists(fn))
2119 		emit previewAvailable(fn, previewFileNameToSource[processedFile]);
2120 }
2121 
commandLineRequestedDefault(const QString & cmdId,QString * result,bool * user)2122 void BuildManager::commandLineRequestedDefault(const QString &cmdId, QString *result, bool *user)
2123 {
2124 	if (user) *user = false;
2125 	if (!result) return;
2126 	int space = cmdId.indexOf(' ');
2127 	if (space == -1) space = cmdId.size();
2128 	if (internalCommands.contains(cmdId.left(space)) || internalCommands.contains(TXS_CMD_PREFIX + cmdId.left(space))) {
2129 		*result = cmdId;
2130 		if (!result->startsWith(TXS_CMD_PREFIX)) *result = TXS_CMD_PREFIX + *result;
2131 		return;
2132 	}
2133 	CommandMapping::iterator it = commands.find(cmdId);
2134 	if (it != commands.end()) {
2135 		*result = it->commandLine;
2136 		if (user) *user = it->user;
2137 	}
2138 }
2139 
runInternalCommandThroughProcessX()2140 void BuildManager::runInternalCommandThroughProcessX()
2141 {
2142 	ProcessX *p = qobject_cast<ProcessX *>(sender());
2143 	REQUIRE(p);
2144 	REQUIRE(p->getCommandLine().startsWith(TXS_CMD_PREFIX));
2145     testAndRunInternalCommand(p->getCommandLine(), QFileInfo(p->getFile()));
2146 }
2147 
testAndRunInternalCommand(const QString & cmd,const QFileInfo & mainFile)2148 bool BuildManager::testAndRunInternalCommand(const QString &cmd, const QFileInfo &mainFile)
2149 {
2150 	int space = cmd.indexOf(' ');
2151 	QString cmdId, options;
2152 	if (space == -1 ) cmdId = cmd;
2153 	else {
2154 		cmdId = cmd.left(space);
2155 		options = cmd.mid(space + 1);
2156 	}
2157 	if (internalCommands.contains(cmdId)) {
2158 		emit runInternalCommand(cmdId, mainFile, options);
2159 		return true;
2160 	}
2161 	return false;
2162 }
2163 
findCompiledFile(const QString & compiledFilename,const QFileInfo & mainFile)2164 QString BuildManager::findCompiledFile(const QString &compiledFilename, const QFileInfo &mainFile)
2165 {
2166 	QString mainDir(mainFile.absolutePath());
2167 	FindInDirs findInDirs(true, false, mainDir);
2168 	findInDirs.loadDirs(mainDir);
2169 	findInDirs.loadDirs(resolvePaths(additionalPdfPaths));
2170 	QString foundPathname = findInDirs.findAbsolute(compiledFilename);
2171 	if (foundPathname == "") {
2172 		// If searched filename is relative prepend the mainFile directory
2173 		// so PDF viewer shows a reasonable error message
2174 		foundPathname = QFileInfo(mainFile.absoluteDir(), compiledFilename).absoluteFilePath();
2175 	}
2176 	return (foundPathname);
2177 }
2178 
removePreviewFiles(QString elem)2179 void BuildManager::removePreviewFiles(QString elem)
2180 {
2181 	QDir currentDir(QFileInfo(elem).absoluteDir());
2182 	elem = QFileInfo(elem).completeBaseName();
2183 	QStringList files;
2184 	files = currentDir.entryList(QStringList(elem + "*"),
2185 	                             QDir::Files | QDir::NoSymLinks);
2186 	foreach (const QString &file, files)
2187 		QFile::remove(currentDir.absolutePath() + "/" + file);
2188 }
2189 
2190 //DDE things
2191 #ifdef Q_OS_WIN32
2192 #include "windows.h"
executeDDE(QString ddePseudoURL)2193 bool BuildManager::executeDDE(QString ddePseudoURL)
2194 {
2195 	//parse URL
2196 	if (ddePseudoURL.startsWith("dde:///")) ddePseudoURL.remove(0, 7);
2197 	else if (ddePseudoURL.startsWith("dde://")) {
2198 		UtilsUi::txsInformation(tr("You have used a dde:// command with two slashes, which is deprecated. Please change it to a triple slash command dde:/// by adding another slash."));
2199 		ddePseudoURL.remove(0, 6);
2200 	} else return false;
2201 
2202 	if (ddePseudoURL.length() < 3) return false;
2203 	QString serviceEXEPath;
2204 	if (ddePseudoURL[1] == ':' || (ddePseudoURL[0] == '"' && ddePseudoURL[2] == ':')) { //extended dde of format dde:///<path>:control/commands
2205 		int pathLength = ddePseudoURL.indexOf(':', 3);
2206 		serviceEXEPath = ddePseudoURL.left(pathLength);
2207 		ddePseudoURL.remove(0, pathLength + 1);
2208 	}
2209 
2210 	int slash = ddePseudoURL.indexOf("/");
2211 	if (slash == -1) return false;
2212 	QString service = ddePseudoURL.left(slash);
2213 	ddePseudoURL.remove(0, slash + 1);
2214 	slash = ddePseudoURL.indexOf("/");
2215 	if (slash == -1) return false;
2216 	QString topic = ddePseudoURL.left(slash);
2217 	ddePseudoURL.remove(0, slash + 1);
2218 	QStringList commands = ddePseudoURL.split("[", Qt::SkipEmptyParts);
2219 	if (commands.isEmpty()) return false;
2220 
2221 
2222 	//connect to server/topic
2223 
2224 	if (pidInst == 0)
2225 		if (DdeInitializeA(&pidInst, NULL, APPCLASS_STANDARD | APPCMD_CLIENTONLY | CBF_SKIP_ALLNOTIFICATIONS, 0L) != DMLERR_NO_ERROR)
2226 			return false;
2227 
2228 	QCoreApplication::processEvents();
2229 
2230 	HSZ hszService = DdeCreateStringHandleA(pidInst, service.toLocal8Bit().data() , CP_WINANSI);
2231 	if (!hszService) return false;
2232 	HSZ hszTopic = DdeCreateStringHandleA(pidInst, topic.toLocal8Bit().data(), CP_WINANSI);
2233 	if (!hszTopic) {
2234 		DdeFreeStringHandle(pidInst, hszService);
2235 		return false;
2236 	}
2237 	HCONV hConv = DdeConnect(pidInst, hszService, hszTopic, NULL);
2238 	if (!hConv && !serviceEXEPath.isEmpty()) {
2239 		if (!serviceEXEPath.contains('"') && serviceEXEPath.contains(' ') && QFileInfo(serviceEXEPath).exists())
2240 			serviceEXEPath = "\"" + serviceEXEPath + "\"";
2241 		//connecting failed; start the service if necessary
2242 		QProcess *p = new QProcess(QCoreApplication::instance()); //application is parent, to close the service if txs is closed
2243 		p->start(serviceEXEPath);
2244 		if (p->waitForStarted(5000)) {
2245 			connect(p, SIGNAL(finished(int, QProcess::ExitStatus)), p, SLOT(deleteLater())); //will free proc after the process has ended
2246 			//try again to connect (repeatedly 2s long)
2247 			DWORD startTime = GetTickCount();
2248 			while (!hConv && GetTickCount() - startTime < 1000) {
2249 				hConv = DdeConnect(pidInst, hszService, hszTopic, NULL);
2250 				Sleep(100);
2251 			}
2252 		} else delete p;
2253 	}
2254 
2255 	QCoreApplication::processEvents();
2256 
2257 	DdeFreeStringHandle(pidInst, hszService);
2258 	DdeFreeStringHandle(pidInst, hszTopic);
2259 	if (!hConv) return false;
2260 
2261 	//execute requests
2262 	foreach (const QString s, commands) {
2263 		QString temp = ("[" + s.trimmed());
2264 		QByteArray ba = temp.toLocal8Bit();
2265 		HDDEDATA req = DdeCreateDataHandle(pidInst, (LPBYTE) ba.data(), ba.size() + 1, 0, 0, CF_TEXT, 0);
2266 		if (req) {
2267 			HDDEDATA recData = DdeClientTransaction((BYTE *)req, (DWORD) - 1, hConv, 0, 0, XTYP_EXECUTE, 1000, 0);
2268 			DdeFreeDataHandle(req);
2269 			if (recData) DdeFreeDataHandle(recData);
2270 		}
2271 		//else QMessageBox::information(0,"TeXstudio",QObject::tr("DDE command %1 failed").arg("["+s),0); //break; send all commands
2272 	}
2273 
2274 	QCoreApplication::processEvents();
2275 
2276 	//disconnect
2277 	DdeDisconnect(hConv);
2278 
2279 	QCoreApplication::processEvents();
2280 
2281 	return true;
2282 }
2283 #endif
2284 
ProcessX(BuildManager * parent,const QString & assignedCommand,const QString & fileToCompile)2285 ProcessX::ProcessX(BuildManager *parent, const QString &assignedCommand, const QString &fileToCompile):
2286     QProcess(parent), cmd(assignedCommand.trimmed()), file(fileToCompile), isStarted(false), ended(false), stderrEnabled(true), stdoutEnabled(true), stdoutEnabledOverrideOn(false), stdoutBuffer(nullptr),stderrBuffer(nullptr), stdoutCodec(nullptr)
2287 {
2288 
2289 	QString stdoutRedirection, stderrRedirection;
2290 	cmd = BuildManager::extractOutputRedirection(cmd, stdoutRedirection, stderrRedirection);
2291 	if (stdoutRedirection == "/dev/null" || stdoutRedirection == "nul") {
2292 		stdoutEnabled = false;
2293 	} else if (stdoutRedirection == "txs:///messages") {
2294 		stdoutEnabledOverrideOn = true;
2295 	} else if (!stdoutRedirection.isEmpty()) {
2296         emit parent->processNotification(tr("The specified stdout redirection is not supported: \"%1\". Please see the manual for details.").arg("> " + stdoutRedirection));
2297 	}
2298 	if (stderrRedirection == "/dev/null" || stderrRedirection == "nul") {
2299 		stderrEnabled = false;
2300 	} else if (stderrRedirection == "txs:///messages") {
2301 		// nothing to do because stderr goes to messages by default
2302 	} else if (stderrRedirection == "&1") {
2303 		stderrEnabled = stdoutEnabled || stdoutEnabledOverrideOn;
2304 	} else if (!stderrRedirection.isEmpty()) {
2305         emit parent->processNotification(tr("The specified stderr redirection is not supported: \"%1\". Please see the manual for details.").arg("2> " + stderrRedirection));
2306 	}
2307 	connect(this, SIGNAL(started()), SLOT(onStarted()));
2308     connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(onFinished(int)));
2309     connect(this, SIGNAL(errorOccurred(QProcess::ProcessError)), SLOT(onError(QProcess::ProcessError)));
2310 }
2311 
2312 /*!
2313  * Reformats shell-style literal quotes (\") to QProcess-style literal quotes (""")
2314  * e.g. "Epic 12\" singles" -> "Epic 12""" singles"
2315  */
reformatShellLiteralQuotes(QString cmd)2316 QString ProcessX::reformatShellLiteralQuotes(QString cmd)
2317 {
2318 	return cmd.replace("\\\"", "\"\"\"");
2319 }
2320 
startCommand()2321 void ProcessX::startCommand()
2322 {
2323 	ended = false;
2324 
2325 #ifdef Q_OS_WIN32
2326 	if (cmd.startsWith("dde://") || cmd.startsWith("dde:///")) {
2327 		onStarted();
2328 		BuildManager *manager = qobject_cast<BuildManager *>(parent());
2329 		if (!manager) {
2330 			emit finished(1, NormalExit);
2331 			return;
2332 		}
2333 		bool ok = manager->executeDDE(cmd);
2334 		emit finished(ok ? 0 : 1, NormalExit);
2335 		return;
2336 	}
2337 #endif
2338 
2339 	if (cmd.startsWith("txs:///")) {
2340 		onStarted();
2341 		emit startedX();
2342 		emit finished(0, NormalExit);
2343 		return;
2344 	}
2345 	if (stdoutEnabled || stdoutBuffer)
2346 		connect(this, SIGNAL(readyReadStandardOutput()), this, SLOT(readFromStandardOutput()));
2347 	if (stderrEnabled)
2348 		connect(this, SIGNAL(readyReadStandardError()), this, SLOT(readFromStandardError()));
2349 
2350 	ExecProgram execProgram(cmd, BuildManager::resolvePaths(BuildManager::additionalSearchPaths));
2351 	execProgram.execAndNoWait(*this);
2352 
2353 	if (error() == FailedToStart || error() == Crashed)
2354 		isStarted = ended = true; //prevent call of waitForStarted, if it failed to start (see QTBUG-33021)
2355 
2356 #ifdef PROFILE_PROCESSES
2357 	connect(this, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(finished()));
2358 	time.start();
2359 #endif
2360 }
2361 
waitForStarted(int timeOut)2362 bool ProcessX::waitForStarted(int timeOut)
2363 {
2364 	if (isStarted) return true;
2365 	return QProcess::waitForStarted(timeOut);
2366 }
2367 
getFile()2368 const QString &ProcessX::getFile()
2369 {
2370 	return file;
2371 }
getCommandLine()2372 const QString &ProcessX::getCommandLine()
2373 {
2374 	return cmd;
2375 }
2376 
showStdout() const2377 bool ProcessX::showStdout() const
2378 {
2379 	return stdoutEnabled;
2380 }
2381 
setShowStdout(bool show)2382 void ProcessX::setShowStdout(bool show)
2383 {
2384 	if (stdoutEnabledOverrideOn) show = true;
2385 	stdoutEnabled = show;
2386 }
2387 
getStdoutBuffer()2388 QString *ProcessX::getStdoutBuffer()
2389 {
2390     return stdoutBuffer;
2391 }
2392 
getStdout()2393 QString ProcessX::getStdout()
2394 {
2395     if(stdoutBuffer)
2396         return *stdoutBuffer;
2397     return QString();
2398 }
2399 
setStdoutBuffer(QString * buffer)2400 void ProcessX::setStdoutBuffer(QString *buffer)
2401 {
2402 	stdoutBuffer = buffer;
2403 }
2404 
setStderrBuffer(QString * buffer)2405 void ProcessX::setStderrBuffer(QString *buffer)
2406 {
2407     stderrBuffer = buffer;
2408 }
2409 
setStdoutCodec(QTextCodec * codec)2410 void ProcessX::setStdoutCodec(QTextCodec *codec)
2411 {
2412 	stdoutCodec = codec;
2413 }
2414 
showStderr() const2415 bool ProcessX::showStderr() const
2416 {
2417 	return stderrEnabled;
2418 }
2419 
setShowStderr(bool show)2420 void ProcessX::setShowStderr(bool show)
2421 {
2422 	stderrEnabled = show;
2423 }
2424 
setOverrideEnvironment(const QStringList & env)2425 void ProcessX::setOverrideEnvironment(const QStringList &env)
2426 {
2427 	overriddenEnvironment = env;
2428 	setEnvironment(env);
2429 }
2430 
overrideEnvironment()2431 const QStringList &ProcessX::overrideEnvironment()
2432 {
2433 	return overriddenEnvironment;
2434 }
2435 
2436 
exitStatus() const2437 int ProcessX::exitStatus() const
2438 {
2439 	return QProcess::exitStatus();
2440 }
2441 
exitCode() const2442 int ProcessX::exitCode() const
2443 {
2444 	return QProcess::exitCode();
2445 }
2446 
readAllStandardOutputStr()2447 QString ProcessX::readAllStandardOutputStr()
2448 {
2449 	return stdoutCodec ? stdoutCodec->toUnicode(QProcess::readAllStandardOutput()) : QString::fromLocal8Bit(QProcess::readAllStandardOutput());
2450 }
2451 
readAllStandardErrorStr()2452 QString ProcessX::readAllStandardErrorStr()
2453 {
2454 	return stdoutCodec ? stdoutCodec->toUnicode(QProcess::readAllStandardError()) : QString::fromLocal8Bit(QProcess::readAllStandardError());
2455 }
2456 
waitForFinished(int msecs)2457 bool ProcessX::waitForFinished( int msecs )
2458 {
2459 	return QProcess::waitForFinished(msecs);
2460 }
2461 
isRunning() const2462 bool ProcessX::isRunning() const
2463 {
2464 	return isStarted && !ended;
2465 }
2466 
onStarted()2467 void ProcessX::onStarted()
2468 {
2469 	if (isStarted) return; //why am I called twice?
2470 	isStarted = true;
2471 	BuildManager_hadSuccessfulProcessStart = true;
2472 	emit startedX();
2473 	emit processNotification(tr("Process started: %1").arg(cmd));
2474 }
2475 
onError(ProcessError error)2476 void ProcessX::onError(ProcessError error)
2477 {
2478 	if (error == FailedToStart) {
2479 		emit processNotification(tr("Error: Could not start the command: %1").arg(cmd));
2480 
2481 		if (!BuildManager_hadSuccessfulProcessStart)
2482 			emit processNotification("<br>" + tr("<b>Make sure that you have installed a (La)TeX distribution</b> e.g. MiKTeX or TeX Live, and have set the correct paths to this distribution on the command configuration page.<br>"
2483 			                                     "A (La)TeX editor like TeXstudio cannot work without the (La)TeX commands provided by such a distribution."));
2484 
2485 	} else if (error == Crashed)
2486 		emit processNotification(tr("Error: Command crashed: %1").arg(cmd));
2487 }
2488 
onFinished(int error)2489 void ProcessX::onFinished(int error)
2490 {
2491 	if (cmd.contains("AcroRd32.exe") && error == 1) error = 0; // fix for Adobe Reader: It returns 1 on success
2492 	if (error) {
2493 		emit processNotification(tr("Process exited with error(s)"));
2494 		readFromStandardError(true);
2495 	} else {
2496 		emit processNotification(tr("Process exited normally"));
2497 		readFromStandardOutput();
2498 		readFromStandardError();
2499 	}
2500 	ended = true;
2501 	emit processFinished();
2502 }
2503 
2504 #ifdef PROFILE_PROCESSES
finished()2505 void ProcessX::finished()
2506 {
2507 	qDebug() << "Process: " << qPrintable(cmd) << "  Running time: " << time.elapsed();
2508 }
2509 #endif
2510 
readFromStandardOutput()2511 void ProcessX::readFromStandardOutput()
2512 {
2513 	if (!stdoutEnabled && !stdoutBuffer) return;
2514 	QString t = readAllStandardOutputStr().trimmed();
2515 	if (stdoutBuffer) stdoutBuffer->append(t);
2516 	emit standardOutputRead(t);
2517 }
2518 
readFromStandardError(bool force)2519 void ProcessX::readFromStandardError(bool force)
2520 {
2521 	if (!stderrEnabled && !force) return;
2522 	QString t = readAllStandardErrorStr().simplified();
2523     if (stderrBuffer) stderrBuffer->append(t);
2524 	emit standardErrorRead(t);
2525 }
2526