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 ¤tFile, 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 ¤tFile, 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 ¤tFile)
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 ¤tFile, 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 ¤tFile, 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