1 /*
2 This is part of TeXworks, an environment for working with TeX documents
3 Copyright (C) 2007-2016 Jonathan Kew, Stefan Löffler, Charlie Sharpsteen
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18 For links to further information, or to contact the authors,
19 see <http://www.tug.org/texworks/>.
20 */
21
22 #include "TeXDocument.h"
23 #include "TeXHighlighter.h"
24 #include "TeXDocks.h"
25 #include "FindDialog.h"
26 #include "TemplateDialog.h"
27 #include "TWApp.h"
28 #include "TWUtils.h"
29 #include "PDFDocument.h"
30 #include "ConfirmDelete.h"
31 #include "HardWrapDialog.h"
32 #include "DefaultPrefs.h"
33
34 #include <QCloseEvent>
35 #include <QFileDialog>
36 #include <QMessageBox>
37 #include <QTextStream>
38 #include <QStatusBar>
39 #include <QFontDialog>
40 #include <QInputDialog>
41 #include <QDesktopWidget>
42 #include <QClipboard>
43 #include <QStringList>
44 #include <QUrl>
45 #include <QComboBox>
46 #include <QRegExp>
47 #include <QProcess>
48 #include <QAbstractItemView>
49 #include <QScrollBar>
50 #include <QActionGroup>
51 #include <QTextCodec>
52 #include <QSignalMapper>
53 #include <QDockWidget>
54 #include <QAbstractButton>
55 #include <QPushButton>
56 #include <QFileSystemWatcher>
57 #include <QTextBrowser>
58 #include <QAbstractTextDocumentLayout>
59
60 #if defined(Q_OS_WIN)
61 #include <windows.h>
62 #endif
63
64 #define kLineEnd_Mask 0x00FF
65 #define kLineEnd_LF 0x0000
66 #define kLineEnd_CRLF 0x0001
67 #define kLineEnd_CR 0x0002
68
69 #define kLineEnd_Flags_Mask 0xFF00
70 #define kLineEnd_Mixed 0x0100
71
72 const int kHardWrapDefaultWidth = 64;
73
74 QList<TeXDocument*> TeXDocument::docList;
75
TeXDocument()76 TeXDocument::TeXDocument()
77 {
78 init();
79 statusBar()->showMessage(tr("New document"), kStatusMessageDuration);
80 }
81
TeXDocument(const QString & fileName,bool asTemplate)82 TeXDocument::TeXDocument(const QString &fileName, bool asTemplate)
83 {
84 init();
85 loadFile(fileName, asTemplate);
86 }
87
~TeXDocument()88 TeXDocument::~TeXDocument()
89 {
90 docList.removeAll(this);
91 updateWindowMenu();
92 }
93
dictActionLessThan(const QAction * a1,const QAction * a2)94 static bool dictActionLessThan(const QAction * a1, const QAction * a2) {
95 return a1->text().toLower() < a2->text().toLower();
96 }
97
init()98 void TeXDocument::init()
99 {
100 codec = TWApp::instance()->getDefaultCodec();
101 pdfDoc = NULL;
102 process = NULL;
103 highlighter = NULL;
104 pHunspell = NULL;
105 utf8BOM = false;
106 #if defined(Q_OS_WIN)
107 lineEndings = kLineEnd_CRLF;
108 #else
109 lineEndings = kLineEnd_LF;
110 #endif
111
112 setupUi(this);
113 #if defined(Q_OS_WIN)
114 TWApp::instance()->createMessageTarget(this);
115 #endif
116
117 setAttribute(Qt::WA_DeleteOnClose, true);
118 setAttribute(Qt::WA_MacNoClickThrough, true);
119
120 setContextMenuPolicy(Qt::NoContextMenu);
121
122 makeUntitled();
123 hideConsole();
124 keepConsoleOpen = false;
125 connect(consoleTabs, SIGNAL(requestClose()), actionShow_Hide_Console, SLOT(trigger()));
126
127 statusBar()->addPermanentWidget(lineEndingLabel = new ClickableLabel());
128 lineEndingLabel->setFrameStyle(QFrame::StyledPanel);
129 lineEndingLabel->setFont(statusBar()->font());
130 connect(lineEndingLabel, SIGNAL(mouseLeftClick(QMouseEvent*)), this, SLOT(lineEndingLabelClick(QMouseEvent*)));
131 showLineEndingSetting();
132
133 statusBar()->addPermanentWidget(encodingLabel = new ClickableLabel());
134 encodingLabel->setFrameStyle(QFrame::StyledPanel);
135 encodingLabel->setFont(statusBar()->font());
136 connect(encodingLabel, SIGNAL(mouseLeftClick(QMouseEvent*)), this, SLOT(encodingLabelClick(QMouseEvent*)));
137 showEncodingSetting();
138
139 statusBar()->addPermanentWidget(lineNumberLabel = new ClickableLabel());
140 lineNumberLabel->setFrameStyle(QFrame::StyledPanel);
141 lineNumberLabel->setFont(statusBar()->font());
142 connect(lineNumberLabel, SIGNAL(mouseLeftClick(QMouseEvent*)), this, SLOT(doLineDialog()));
143 showCursorPosition();
144
145 engineActions = new QActionGroup(this);
146 connect(engineActions, SIGNAL(triggered(QAction*)), this, SLOT(selectedEngine(QAction*)));
147
148 codec = TWApp::instance()->getDefaultCodec();
149 engineName = TWApp::instance()->getDefaultEngine().name();
150 engine = new QComboBox(this);
151 engine->setEditable(false);
152 engine->setFocusPolicy(Qt::NoFocus);
153 engine->setSizeAdjustPolicy(QComboBox::AdjustToContents);
154 #if defined(Q_OS_DARWIN) && (QT_VERSION >= 0x040600)
155 engine->setStyleSheet("padding:4px;");
156 engine->setMinimumWidth(150);
157 #endif
158 toolBar_run->addWidget(engine);
159 updateEngineList();
160 connect(engine, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(selectedEngine(const QString&)));
161
162 connect(TWApp::instance(), SIGNAL(engineListChanged()), this, SLOT(updateEngineList()));
163
164 connect(actionNew, SIGNAL(triggered()), this, SLOT(newFile()));
165 connect(actionNew_from_Template, SIGNAL(triggered()), this, SLOT(newFromTemplate()));
166 connect(actionOpen, SIGNAL(triggered()), this, SLOT(open()));
167 connect(actionAbout_TW, SIGNAL(triggered()), qApp, SLOT(about()));
168 connect(actionSettings_and_Resources, SIGNAL(triggered()), qApp, SLOT(doResourcesDialog()));
169 connect(actionGoToHomePage, SIGNAL(triggered()), qApp, SLOT(goToHomePage()));
170 connect(actionWriteToMailingList, SIGNAL(triggered()), qApp, SLOT(writeToMailingList()));
171
172 connect(actionSave, SIGNAL(triggered()), this, SLOT(save()));
173 connect(actionSave_As, SIGNAL(triggered()), this, SLOT(saveAs()));
174 connect(actionSave_All, SIGNAL(triggered()), this, SLOT(saveAll()));
175 connect(actionRevert_to_Saved, SIGNAL(triggered()), this, SLOT(revert()));
176 connect(actionClose, SIGNAL(triggered()), this, SLOT(close()));
177
178 connect(actionRemove_Aux_Files, SIGNAL(triggered()), this, SLOT(removeAuxFiles()));
179
180 connect(actionQuit_TeXworks, SIGNAL(triggered()), TWApp::instance(), SLOT(maybeQuit()));
181
182 connect(actionClear, SIGNAL(triggered()), this, SLOT(clear()));
183
184 connect(actionFont, SIGNAL(triggered()), this, SLOT(doFontDialog()));
185 connect(actionGo_to_Line, SIGNAL(triggered()), this, SLOT(doLineDialog()));
186 connect(actionFind, SIGNAL(triggered()), this, SLOT(doFindDialog()));
187 connect(actionFind_Again, SIGNAL(triggered()), this, SLOT(doFindAgain()));
188 connect(actionReplace, SIGNAL(triggered()), this, SLOT(doReplaceDialog()));
189 connect(actionReplace_Again, SIGNAL(triggered()), this, SLOT(doReplaceAgain()));
190
191 connect(actionCopy_to_Find, SIGNAL(triggered()), this, SLOT(copyToFind()));
192 connect(actionCopy_to_Replace, SIGNAL(triggered()), this, SLOT(copyToReplace()));
193 connect(actionFind_Selection, SIGNAL(triggered()), this, SLOT(findSelection()));
194
195 connect(actionShow_Selection, SIGNAL(triggered()), this, SLOT(showSelection()));
196
197 connect(actionIndent, SIGNAL(triggered()), this, SLOT(doIndent()));
198 connect(actionUnindent, SIGNAL(triggered()), this, SLOT(doUnindent()));
199
200 connect(actionComment, SIGNAL(triggered()), this, SLOT(doComment()));
201 connect(actionUncomment, SIGNAL(triggered()), this, SLOT(doUncomment()));
202
203 connect(actionHard_Wrap, SIGNAL(triggered()), this, SLOT(doHardWrapDialog()));
204
205 connect(actionTo_Uppercase, SIGNAL(triggered()), this, SLOT(toUppercase()));
206 connect(actionTo_Lowercase, SIGNAL(triggered()), this, SLOT(toLowercase()));
207 connect(actionToggle_Case, SIGNAL(triggered()), this, SLOT(toggleCase()));
208
209 connect(actionBalance_Delimiters, SIGNAL(triggered()), this, SLOT(balanceDelimiters()));
210
211 connect(textEdit->document(), SIGNAL(modificationChanged(bool)), this, SLOT(setWindowModified(bool)));
212 connect(textEdit->document(), SIGNAL(modificationChanged(bool)), this, SLOT(maybeEnableSaveAndRevert(bool)));
213 connect(textEdit->document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(contentsChanged(int,int,int)));
214 connect(textEdit, SIGNAL(cursorPositionChanged()), this, SLOT(showCursorPosition()));
215 connect(textEdit, SIGNAL(selectionChanged()), this, SLOT(showCursorPosition()));
216 connect(textEdit, SIGNAL(syncClick(int, int)), this, SLOT(syncClick(int, int)));
217 connect(this, SIGNAL(syncFromSource(const QString&, int, int, bool)), qApp, SIGNAL(syncPdf(const QString&, int, int, bool)));
218
219 connect(QApplication::clipboard(), SIGNAL(dataChanged()), this, SLOT(clipboardChanged()));
220 clipboardChanged();
221
222 connect(actionTypeset, SIGNAL(triggered()), this, SLOT(typeset()));
223
224 updateRecentFileActions();
225 connect(qApp, SIGNAL(recentFileActionsChanged()), this, SLOT(updateRecentFileActions()));
226 connect(qApp, SIGNAL(windowListChanged()), this, SLOT(updateWindowMenu()));
227 connect(actionClear_Recent_Files, SIGNAL(triggered()), TWApp::instance(), SLOT(clearRecentFiles()));
228
229 connect(qApp, SIGNAL(hideFloatersExcept(QWidget*)), this, SLOT(hideFloatersUnlessThis(QWidget*)));
230 connect(this, SIGNAL(activatedWindow(QWidget*)), qApp, SLOT(activatedWindow(QWidget*)));
231
232 connect(actionStack, SIGNAL(triggered()), qApp, SLOT(stackWindows()));
233 connect(actionTile, SIGNAL(triggered()), qApp, SLOT(tileWindows()));
234 connect(actionSide_by_Side, SIGNAL(triggered()), this, SLOT(sideBySide()));
235 connect(actionPlace_on_Left, SIGNAL(triggered()), this, SLOT(placeOnLeft()));
236 connect(actionPlace_on_Right, SIGNAL(triggered()), this, SLOT(placeOnRight()));
237 connect(actionShow_Hide_Console, SIGNAL(triggered()), this, SLOT(toggleConsoleVisibility()));
238 connect(actionGo_to_Preview, SIGNAL(triggered()), this, SLOT(goToPreview()));
239
240 connect(this, SIGNAL(destroyed()), qApp, SLOT(updateWindowMenus()));
241
242 connect(actionPreferences, SIGNAL(triggered()), qApp, SLOT(preferences()));
243
244 connect(menuEdit, SIGNAL(aboutToShow()), this, SLOT(editMenuAboutToShow()));
245
246 #if defined(Q_OS_DARWIN)
247 textEdit->installEventFilter(CmdKeyFilter::filter());
248 #endif
249
250 connect(inputLine, SIGNAL(returnPressed()), this, SLOT(acceptInputLine()));
251
252 QSETTINGS_OBJECT(settings);
253 TWUtils::applyToolbarOptions(this, settings.value("toolBarIconSize", 2).toInt(), settings.value("toolBarShowText", false).toBool());
254
255 QFont font = textEdit->font();
256 if (settings.contains("font")) {
257 QString fontString = settings.value("font").toString();
258 if (fontString != "") {
259 font.fromString(fontString);
260 textEdit->setFont(font);
261 }
262 }
263 font.setPointSize(font.pointSize() - 1);
264 inputLine->setFont(font);
265 inputLine->setLayoutDirection(Qt::LeftToRight);
266 textEdit_console->setFont(font);
267 textEdit_console->setLayoutDirection(Qt::LeftToRight);
268
269 bool b = settings.value("wrapLines", true).toBool();
270 actionWrap_Lines->setChecked(b);
271 setWrapLines(b);
272
273 b = settings.value("lineNumbers", false).toBool();
274 actionLine_Numbers->setChecked(b);
275 setLineNumbers(b);
276
277 QStringList options = TeXHighlighter::syntaxOptions();
278
279 QSignalMapper *syntaxMapper = new QSignalMapper(this);
280 connect(syntaxMapper, SIGNAL(mapped(int)), this, SLOT(setSyntaxColoring(int)));
281 syntaxMapper->setMapping(actionSyntaxColoring_None, -1);
282 connect(actionSyntaxColoring_None, SIGNAL(triggered()), syntaxMapper, SLOT(map()));
283
284 QActionGroup *syntaxGroup = new QActionGroup(this);
285 syntaxGroup->addAction(actionSyntaxColoring_None);
286
287 int index = 0;
288 foreach (const QString& opt, options) {
289 QAction *action = menuSyntax_Coloring->addAction(opt, syntaxMapper, SLOT(map()));
290 action->setCheckable(true);
291 syntaxGroup->addAction(action);
292 syntaxMapper->setMapping(action, index);
293 ++index;
294 }
295
296 // kDefault_TabWidth is defined in DefaultPrefs.h
297 textEdit->setTabStopWidth(settings.value("tabWidth", kDefault_TabWidth).toInt());
298
299 // It is VITAL that this connection is queued! Calling showMessage directly
300 // from TeXDocument::contentsChanged would otherwise result in a seg fault
301 // (for whatever reason)
302 connect(this, SIGNAL(asyncFlashStatusBarMessage(QString, int)), statusBar(), SLOT(showMessage(QString, int)), Qt::QueuedConnection);
303
304 QString indentOption = settings.value("autoIndent").toString();
305 options = CompletingEdit::autoIndentModes();
306
307 QSignalMapper *indentMapper = new QSignalMapper(this);
308 connect(indentMapper, SIGNAL(mapped(int)), textEdit, SLOT(setAutoIndentMode(int)));
309 indentMapper->setMapping(actionAutoIndent_None, -1);
310 connect(actionAutoIndent_None, SIGNAL(triggered()), indentMapper, SLOT(map()));
311
312 QActionGroup *indentGroup = new QActionGroup(this);
313 indentGroup->addAction(actionAutoIndent_None);
314
315 index = 0;
316 foreach (const QString& opt, options) {
317 QAction *action = menuAuto_indent_Mode->addAction(opt, indentMapper, SLOT(map()));
318 action->setCheckable(true);
319 indentGroup->addAction(action);
320 indentMapper->setMapping(action, index);
321 if (opt == indentOption) {
322 action->setChecked(true);
323 textEdit->setAutoIndentMode(index);
324 }
325 ++index;
326 }
327
328 QString quotesOption = settings.value("smartQuotes").toString();
329 options = CompletingEdit::smartQuotesModes();
330
331 QSignalMapper *quotesMapper = new QSignalMapper(this);
332 connect(quotesMapper, SIGNAL(mapped(int)), textEdit, SLOT(setSmartQuotesMode(int)));
333 quotesMapper->setMapping(actionSmartQuotes_None, -1);
334 connect(actionSmartQuotes_None, SIGNAL(triggered()), quotesMapper, SLOT(map()));
335
336 QActionGroup *quotesGroup = new QActionGroup(this);
337 quotesGroup->addAction(actionSmartQuotes_None);
338
339 menuSmart_Quotes_Mode->removeAction(actionApply_to_Selection);
340 index = 0;
341 foreach (const QString& opt, options) {
342 QAction *action = menuSmart_Quotes_Mode->addAction(opt, quotesMapper, SLOT(map()));
343 action->setCheckable(true);
344 quotesGroup->addAction(action);
345 quotesMapper->setMapping(action, index);
346 if (opt == quotesOption) {
347 action->setChecked(true);
348 textEdit->setSmartQuotesMode(index);
349 }
350 ++index;
351 }
352 if (options.size() > 0)
353 menuSmart_Quotes_Mode->addSeparator();
354 menuSmart_Quotes_Mode->addAction(actionApply_to_Selection);
355 connect(actionApply_to_Selection, SIGNAL(triggered()), textEdit, SLOT(smartenQuotes()));
356
357 connect(actionLine_Numbers, SIGNAL(triggered(bool)), this, SLOT(setLineNumbers(bool)));
358 connect(actionWrap_Lines, SIGNAL(triggered(bool)), this, SLOT(setWrapLines(bool)));
359
360 connect(actionNone, SIGNAL(triggered()), &dictSignalMapper, SLOT(map()));
361 dictSignalMapper.setMapping(actionNone, QString());
362 connect(&dictSignalMapper, SIGNAL(mapped(const QString&)), this, SLOT(setLangInternal(const QString&)));
363
364 QActionGroup *group = new QActionGroup(this);
365 group->addAction(actionNone);
366
367 reloadSpellcheckerMenu();
368 connect(TWApp::instance(), SIGNAL(dictionaryListChanged()), this, SLOT(reloadSpellcheckerMenu()));
369
370 menuShow->addAction(toolBar_run->toggleViewAction());
371 menuShow->addAction(toolBar_edit->toggleViewAction());
372 menuShow->addSeparator();
373
374 TWUtils::zoomToHalfScreen(this);
375
376 QDockWidget *dw = new TagsDock(this);
377 dw->hide();
378 addDockWidget(Qt::LeftDockWidgetArea, dw);
379 menuShow->addAction(dw->toggleViewAction());
380 deferTagListChanges = false;
381
382 watcher = new QFileSystemWatcher(this);
383 connect(watcher, SIGNAL(fileChanged(const QString&)), this, SLOT(reloadIfChangedOnDisk()), Qt::QueuedConnection);
384 connect(watcher, SIGNAL(directoryChanged(const QString&)), this, SLOT(reloadIfChangedOnDisk()), Qt::QueuedConnection);
385
386 docList.append(this);
387
388 TWApp::instance()->updateWindowMenus();
389
390 initScriptable(menuScripts, actionAbout_Scripts, actionManage_Scripts,
391 actionUpdate_Scripts, actionShow_Scripts_Folder);
392
393 TWUtils::insertHelpMenuItems(menuHelp);
394 TWUtils::installCustomShortcuts(this);
395 #if QT_VERSION < 0x050000
396 QTimer::singleShot(1000, this, SLOT(delayedInit()));
397 #else
398 delayedInit();
399 #endif
400 }
401
changeEvent(QEvent * event)402 void TeXDocument::changeEvent(QEvent *event)
403 {
404 if (event->type() == QEvent::LanguageChange) {
405 QString title = windowTitle();
406 retranslateUi(this);
407 TWUtils::insertHelpMenuItems(menuHelp);
408 setWindowTitle(title);
409 showCursorPosition();
410 }
411 else if (event->type() == QEvent::ActivationChange) {
412 // If this window was activated, inform the linked pdf (if any) of it
413 // so that future "Goto Source" actions point here.
414 if (this == QApplication::activeWindow() && pdfDoc)
415 pdfDoc->texActivated(this);
416 }
417 QMainWindow::changeEvent(event);
418 }
419
setLangInternal(const QString & lang)420 void TeXDocument::setLangInternal(const QString& lang)
421 {
422 // called internally by the spelling menu actions;
423 // not for use from scripts as it won't update the menu
424 QTextCodec *spellingCodec;
425 Hunhandle* pOldHunspell = pHunspell;
426 pHunspell = TWUtils::getDictionary(lang);
427 // if the dictionary hasn't change, don't reset the spell checker as that
428 // can result in a serious delay for long documents
429 // NB: Don't delete the hunspell handles; the pointers are kept by TWUtils
430 if (pOldHunspell == pHunspell)
431 return;
432
433 if (pHunspell != NULL) {
434 spellingCodec = QTextCodec::codecForName(Hunspell_get_dic_encoding(pHunspell));
435 if (spellingCodec == NULL)
436 spellingCodec = QTextCodec::codecForLocale(); // almost certainly wrong, if we couldn't find the actual name!
437 }
438 else
439 spellingCodec = NULL;
440 textEdit->setSpellChecker(pHunspell, spellingCodec);
441 if (highlighter)
442 highlighter->setSpellChecker(pHunspell, spellingCodec);
443 }
444
setSpellcheckLanguage(const QString & lang)445 void TeXDocument::setSpellcheckLanguage(const QString& lang)
446 {
447 // this is called by the %!TEX spellcheck... line, or by scripts;
448 // it searches the menu for the given language code, and triggers it if available
449
450 // Determine all aliases for the specified lang
451 QList<QString> langAliases;
452 foreach (const QString& dictKey, TWUtils::getDictionaryList()->uniqueKeys()) {
453 if(TWUtils::getDictionaryList()->values(dictKey).contains(lang))
454 langAliases += TWUtils::getDictionaryList()->values(dictKey);
455 }
456 langAliases.removeAll(lang);
457 langAliases.prepend(lang);
458
459 bool found = false;
460 if (menuSpelling) {
461 QAction *chosen = menuSpelling->actions()[0]; // default is None
462 foreach (QAction *act, menuSpelling->actions()) {
463 foreach(QString alias, langAliases) {
464 if (act->text() == alias || act->text().contains("(" + alias + ")")) {
465 chosen = act;
466 found = true;
467 break;
468 }
469 }
470 if(found) break;
471 }
472 chosen->trigger();
473 }
474 }
475
spellcheckLanguage() const476 QString TeXDocument::spellcheckLanguage() const
477 {
478 return TWUtils::getLanguageForDictionary(pHunspell);
479 }
480
reloadSpellcheckerMenu()481 void TeXDocument::reloadSpellcheckerMenu()
482 {
483 Q_ASSERT(menuSpelling != NULL);
484 Q_ASSERT(menuSpelling->actions().size() > 0);
485
486 QActionGroup * group = menuSpelling->actions()[0]->actionGroup();
487 Q_ASSERT(group != NULL);
488
489 // Remove all but the first menu item ("None") from the action group
490 int i = 0;
491 QString oldSelected;
492 foreach (QAction * act, group->actions()) {
493 if (act->isChecked())
494 oldSelected = act->text();
495 if (i > 0) {
496 group->removeAction(act);
497 act->deleteLater();
498 }
499 ++i;
500 }
501
502 QList<QAction*> dictActions;
503 foreach (const QString& dictKey, TWUtils::getDictionaryList()->uniqueKeys()) {
504 QAction *act;
505 QString dict, label;
506 QLocale loc;
507
508 foreach (dict, TWUtils::getDictionaryList()->values(dictKey)) {
509 loc = QLocale(dict);
510 if (loc.language() != QLocale::C) break;
511 }
512
513 if (loc.language() == QLocale::C)
514 label = dict;
515 else {
516 label = QLocale::languageToString(loc.language());
517 QLocale::Country country = loc.country();
518 if (country != QLocale::AnyCountry)
519 label += " - " + QLocale::countryToString(country);
520 label += " (" + dict + ")";
521 }
522
523 act = new QAction(label, NULL);
524 act->setCheckable(true);
525 if (!oldSelected.isEmpty() && label == oldSelected)
526 act->setChecked(true);
527 connect(act, SIGNAL(triggered()), &dictSignalMapper, SLOT(map()));
528 dictSignalMapper.setMapping(act, dict);
529 group->addAction(act);
530 dictActions << act;
531 }
532 qSort(dictActions.begin(), dictActions.end(), dictActionLessThan);
533 foreach (QAction* dictAction, dictActions)
534 menuSpelling->addAction(dictAction);
535 }
536
clipboardChanged()537 void TeXDocument::clipboardChanged()
538 {
539 actionPaste->setEnabled(textEdit->canPaste());
540 }
541
editMenuAboutToShow()542 void TeXDocument::editMenuAboutToShow()
543 {
544 // undoAction->setText(tr("Undo ") + undoStack->undoText());
545 // redoAction->setText(tr("Redo ") + undoStack->redoText());
546 actionSelect_All->setEnabled(!textEdit->document()->isEmpty());
547 }
548
newFile()549 void TeXDocument::newFile()
550 {
551 TeXDocument *doc = new TeXDocument;
552 doc->selectWindow();
553 doc->textEdit->updateLineNumberAreaWidth(0);
554 doc->runHooks("NewFile");
555 }
556
newFromTemplate()557 void TeXDocument::newFromTemplate()
558 {
559 QString templateName = TemplateDialog::doTemplateDialog();
560 if (!templateName.isEmpty()) {
561 TeXDocument *doc = NULL;
562 if (isUntitled && textEdit->document()->isEmpty() && !isWindowModified()) {
563 loadFile(templateName, true);
564 doc = this;
565 }
566 else {
567 doc = new TeXDocument(templateName, true);
568 }
569 if (doc != NULL) {
570 doc->makeUntitled();
571 doc->selectWindow();
572 doc->textEdit->updateLineNumberAreaWidth(0);
573 doc->runHooks("NewFromTemplate");
574 }
575 }
576 }
577
makeUntitled()578 void TeXDocument::makeUntitled()
579 {
580 setCurrentFile("");
581 actionRemove_Aux_Files->setEnabled(false);
582 }
583
open()584 void TeXDocument::open()
585 {
586 QFileDialog::Options options = 0;
587 #if defined(Q_OS_DARWIN)
588 /* use a sheet if we're calling Open from an empty, untitled, untouched window; otherwise use a separate dialog */
589 if (!(isUntitled && textEdit->document()->isEmpty() && !isWindowModified()))
590 options = QFileDialog::DontUseSheet;
591 #elif defined(Q_OS_WIN)
592 if(TWApp::GetWindowsVersion() < 0x06000000) options |= QFileDialog::DontUseNativeDialog;
593 #endif
594 QSETTINGS_OBJECT(settings);
595 QString lastOpenDir = settings.value("openDialogDir").toString();
596 if (lastOpenDir.isEmpty())
597 lastOpenDir = QDir::homePath();
598 QStringList files = QFileDialog::getOpenFileNames(this, QString(tr("Open File")), lastOpenDir, TWUtils::filterList()->join(";;"), NULL, options);
599 foreach (QString fileName, files) {
600 if (!fileName.isEmpty()) {
601 TWApp::instance()->openFile(fileName); // not TeXDocument::open() - give the app a chance to open as PDF
602 }
603 }
604 }
605
open(const QString & fileName)606 TeXDocument* TeXDocument::open(const QString &fileName)
607 {
608 TeXDocument *doc = NULL;
609 if (!fileName.isEmpty()) {
610 doc = findDocument(fileName);
611 if (doc == NULL) {
612 if (isUntitled && textEdit->document()->isEmpty() && !isWindowModified()) {
613 loadFile(fileName);
614 doc = this;
615 }
616 else {
617 doc = new TeXDocument(fileName);
618 if (doc->isUntitled) {
619 delete doc;
620 doc = NULL;
621 }
622 }
623 }
624 }
625 if (doc != NULL)
626 doc->selectWindow();
627 return doc;
628 }
629
openDocument(const QString & fileName,bool activate,bool raiseWindow,int lineNo,int selStart,int selEnd)630 TeXDocument* TeXDocument::openDocument(const QString &fileName, bool activate, bool raiseWindow, int lineNo, int selStart, int selEnd) // static
631 {
632 TeXDocument *doc = findDocument(fileName);
633 if (doc == NULL) {
634 if (docList.count() == 1) {
635 doc = docList[0];
636 doc = doc->open(fileName); // open into existing window if untitled/empty
637 }
638 else {
639 doc = new TeXDocument(fileName);
640 if (doc->isUntitled) {
641 delete doc;
642 doc = NULL;
643 }
644 }
645 }
646 if (doc != NULL) {
647 if (activate)
648 doc->selectWindow();
649 else {
650 doc->show();
651 if (raiseWindow) {
652 doc->raise();
653 if (doc->isMinimized())
654 doc->showNormal();
655 }
656 }
657 if (lineNo > 0)
658 doc->goToLine(lineNo, selStart, selEnd);
659 }
660 return doc;
661 }
662
closeEvent(QCloseEvent * event)663 void TeXDocument::closeEvent(QCloseEvent *event)
664 {
665 if (process != NULL) {
666 if (QMessageBox::question(this, tr("Abort typesetting?"), tr("A typesetting process is still running and must be stopped before closing this window.\nDo you want to stop it now?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes) == QMessageBox::No) {
667 event->ignore();
668 return;
669 }
670 else
671 interrupt();
672 }
673
674 if (maybeSave()) {
675 event->accept();
676 saveRecentFileInfo();
677 deleteLater();
678 }
679 else
680 event->ignore();
681 }
682
event(QEvent * event)683 bool TeXDocument::event(QEvent *event) // based on example at doc.trolltech.com/qq/qq18-macfeatures.html
684 {
685 switch (event->type()) {
686 case QEvent::IconDrag:
687 if (isActiveWindow()) {
688 event->accept();
689 Qt::KeyboardModifiers mods = qApp->keyboardModifiers();
690 if (mods == Qt::NoModifier) {
691 QDrag *drag = new QDrag(this);
692 QMimeData *data = new QMimeData();
693 data->setUrls(QList<QUrl>() << QUrl::fromLocalFile(curFile));
694 drag->setMimeData(data);
695 QPixmap dragIcon(":/images/images/TeXworks-doc-48.png");
696 drag->setPixmap(dragIcon);
697 drag->setHotSpot(QPoint(dragIcon.width() - 5, 5));
698 drag->start(Qt::LinkAction | Qt::CopyAction);
699 }
700 else if (mods == Qt::ShiftModifier) {
701 QMenu menu(this);
702 connect(&menu, SIGNAL(triggered(QAction*)), this, SLOT(openAt(QAction*)));
703 QFileInfo info(curFile);
704 QAction *action = menu.addAction(info.fileName());
705 action->setIcon(QIcon(":/images/images/TeXworks-doc.png"));
706 QStringList folders = info.absolutePath().split('/');
707 QStringListIterator it(folders);
708 it.toBack();
709 while (it.hasPrevious()) {
710 QString str = it.previous();
711 QIcon icon;
712 if (!str.isEmpty()) {
713 icon = style()->standardIcon(QStyle::SP_DirClosedIcon, 0, this);
714 }
715 else {
716 str = "/";
717 icon = style()->standardIcon(QStyle::SP_DriveHDIcon, 0, this);
718 }
719 action = menu.addAction(str);
720 action->setIcon(icon);
721 #if defined(Q_OS_DARWIN)
722 action->setIconVisibleInMenu(true);
723 #endif
724 }
725 QPoint pos(QCursor::pos().x() - 20, frameGeometry().y());
726 menu.exec(pos);
727 }
728 else {
729 event->ignore();
730 }
731 return true;
732 }
733
734 case QEvent::WindowActivate:
735 showFloaters();
736 emit activatedWindow(this);
737 break;
738
739 default:
740 break;
741 }
742 return QMainWindow::event(event);
743 }
744
openAt(QAction * action)745 void TeXDocument::openAt(QAction *action)
746 {
747 QString path = curFile.left(curFile.indexOf(action->text())) + action->text();
748 if (path == curFile)
749 return;
750 QProcess proc;
751 proc.start("/usr/bin/open", QStringList() << path, QIODevice::ReadOnly);
752 proc.waitForFinished();
753 }
754
save()755 bool TeXDocument::save()
756 {
757 if (isUntitled)
758 return saveAs();
759 else
760 return saveFile(curFile);
761 }
762
saveAll()763 bool TeXDocument::saveAll()
764 {
765 bool savedAll = true;
766 foreach (TeXDocument* doc, docList) {
767 if (doc->textEdit->document()->isModified()) {
768 if (!doc->save()) {
769 savedAll = false;
770 }
771 }
772 }
773 return savedAll;
774 }
775
saveAs()776 bool TeXDocument::saveAs()
777 {
778 QFileDialog::Options options = 0;
779 #if defined(Q_OS_WIN)
780 if(TWApp::GetWindowsVersion() < 0x06000000) options |= QFileDialog::DontUseNativeDialog;
781 #endif
782 QString selectedFilter = TWUtils::chooseDefaultFilter(curFile, *(TWUtils::filterList()));;
783
784 // for untitled docs, default to the last dir used, or $HOME if no saved value
785 QSETTINGS_OBJECT(settings);
786 QString lastSaveDir = settings.value("saveDialogDir").toString();
787 if (lastSaveDir.isEmpty() || !QDir(lastSaveDir).exists())
788 lastSaveDir = QDir::homePath();
789 QString fileName = QFileDialog::getSaveFileName(this, tr("Save File"),
790 isUntitled ? lastSaveDir + "/" + curFile : curFile,
791 TWUtils::filterList()->join(";;"),
792 &selectedFilter, options);
793 if (fileName.isEmpty())
794 return false;
795
796 // save the old document in "Recent Files"
797 saveRecentFileInfo();
798
799 // add extension from the selected filter, if unique and not already present
800 QRegExp re("\\(\\*(\\.[^ *]+)\\)");
801 if (re.indexIn(selectedFilter) >= 0) {
802 QString ext = re.cap(1);
803 if (!fileName.endsWith(ext, Qt::CaseInsensitive) && !fileName.endsWith("."))
804 fileName.append(ext);
805 }
806
807 if (fileName != curFile && pdfDoc) {
808 // For the pdf, it is as if it's source doc was closed
809 // Note that this may result in the pdf being closed!
810 pdfDoc->texClosed(this);
811 // The pdf connection is no longer (necessarily) valid. Detach it for
812 // now (the correct connection will be reestablished on next typeset).
813 detachPdf();
814 }
815
816 QFileInfo info(fileName);
817 settings.setValue("saveDialogDir", info.absolutePath());
818
819 return saveFile(fileName);
820 }
821
maybeSave()822 bool TeXDocument::maybeSave()
823 {
824 if (textEdit->document()->isModified()) {
825 QMessageBox::StandardButton ret;
826 QMessageBox msgBox(QMessageBox::Warning, tr(TEXWORKS_NAME),
827 tr("The document \"%1\" has been modified.\n"
828 "Do you want to save your changes?")
829 .arg(TWUtils::strippedName(curFile)),
830 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
831 this);
832 msgBox.button(QMessageBox::Discard)->setShortcut(QKeySequence(tr("Ctrl+D", "shortcut: Don't Save")));
833 msgBox.setWindowModality(Qt::WindowModal);
834 ret = (QMessageBox::StandardButton)msgBox.exec();
835 if (ret == QMessageBox::Save)
836 return save();
837 else if (ret == QMessageBox::Cancel)
838 return false;
839 }
840 return true;
841 }
842
saveFilesHavingRoot(const QString & aRootFile)843 bool TeXDocument::saveFilesHavingRoot(const QString& aRootFile)
844 {
845 foreach (TeXDocument* doc, docList) {
846 if (doc->getRootFilePath() == aRootFile) {
847 if (doc->textEdit->document()->isModified() && !doc->save())
848 return false;
849 }
850 }
851 return true;
852 }
853
getRootFilePath()854 const QString& TeXDocument::getRootFilePath()
855 {
856 findRootFilePath();
857 return rootFilePath;
858 }
859
revert()860 void TeXDocument::revert()
861 {
862 if (!isUntitled) {
863 QMessageBox messageBox(QMessageBox::Warning, tr(TEXWORKS_NAME),
864 tr("Do you want to discard all changes to the document \"%1\", and revert to the last saved version?")
865 .arg(TWUtils::strippedName(curFile)), QMessageBox::Cancel, this);
866 QAbstractButton *revertButton = messageBox.addButton(tr("Revert"), QMessageBox::DestructiveRole);
867 revertButton->setShortcut(QKeySequence(tr("Ctrl+R", "shortcut: Revert")));
868 messageBox.setDefaultButton(QMessageBox::Cancel);
869 messageBox.setWindowModality(Qt::WindowModal);
870 messageBox.exec();
871 if (messageBox.clickedButton() == revertButton)
872 loadFile(curFile);
873 }
874 }
875
maybeEnableSaveAndRevert(bool modified)876 void TeXDocument::maybeEnableSaveAndRevert(bool modified)
877 {
878 actionSave->setEnabled(modified || isUntitled);
879 actionRevert_to_Saved->setEnabled(modified && !isUntitled);
880 }
881
882 static const char* texshopSynonyms[] = {
883 "MacOSRoman", "Apple Roman",
884 "IsoLatin", "ISO 8859-1",
885 "IsoLatin2", "ISO 8859-2",
886 "IsoLatin5", "ISO 8859-5",
887 "IsoLatin9", "ISO 8859-9",
888 // "MacJapanese", "",
889 // "DOSJapanese", "",
890 "SJIS_X0213", "Shift-JIS",
891 "EUC_JP", "EUC-JP",
892 // "JISJapanese", "",
893 // "MacKorean", "",
894 "UTF-8 Unicode", "UTF-8",
895 "Standard Unicode", "UTF-16",
896 // "Mac Cyrillic", "",
897 // "DOS Cyrillic", "",
898 // "DOS Russian", "",
899 "Windows Cyrillic", "Windows-1251",
900 "KOI8_R", "KOI8-R",
901 // "Mac Chinese Traditional", "",
902 // "Mac Chinese Simplified", "",
903 // "DOS Chinese Traditional", "",
904 // "DOS Chinese Simplified", "",
905 // "GBK", "",
906 // "GB 2312", "",
907 "GB 18030", "GB18030-0",
908 NULL
909 };
910
scanForEncoding(const QString & peekStr,bool & hasMetadata,QString & reqName)911 QTextCodec *TeXDocument::scanForEncoding(const QString &peekStr, bool &hasMetadata, QString &reqName)
912 {
913 // peek at the file for %!TEX encoding = ....
914 QRegExp re("% *!TEX +encoding *= *([^\\r\\n\\x2029]+)[\\r\\n\\x2029]", Qt::CaseInsensitive);
915 int pos = re.indexIn(peekStr);
916 QTextCodec *reqCodec = NULL;
917 if (pos > -1) {
918 hasMetadata = true;
919 reqName = re.cap(1).trimmed();
920 reqCodec = QTextCodec::codecForName(reqName.toLatin1());
921 if (reqCodec == NULL) {
922 static QHash<QString,QString> *synonyms = NULL;
923 if (synonyms == NULL) {
924 synonyms = new QHash<QString,QString>;
925 for (int i = 0; texshopSynonyms[i] != NULL; i += 2)
926 synonyms->insert(QString(texshopSynonyms[i]).toLower(), texshopSynonyms[i+1]);
927 }
928 if (synonyms->contains(reqName.toLower()))
929 reqCodec = QTextCodec::codecForName(synonyms->value(reqName.toLower()).toLatin1());
930 }
931 }
932 else
933 hasMetadata = false;
934 return reqCodec;
935 }
936
937 #define PEEK_LENGTH 1024
938
readFile(const QString & fileName,QTextCodec ** codecUsed,int * lineEndings,QTextCodec * forceCodec)939 QString TeXDocument::readFile(const QString &fileName,
940 QTextCodec **codecUsed,
941 int *lineEndings,
942 QTextCodec * forceCodec)
943 // reads the text from a file, after checking for %!TEX encoding.... metadata
944 // sets codecUsed to the QTextCodec used to read the text
945 // returns a null (not just empty) QString on failure
946 {
947 if (lineEndings != NULL) {
948 // initialize to default for the platform
949 #if defined(Q_OS_WIN)
950 *lineEndings = kLineEnd_CRLF;
951 #else
952 *lineEndings = kLineEnd_LF;
953 #endif
954 }
955
956 utf8BOM = false;
957 QFile file(fileName);
958 // Not using QFile::Text because this prevents us reading "classic" Mac files
959 // with CR-only line endings. See issue #242.
960 if (!file.open(QFile::ReadOnly)) {
961 QMessageBox::warning(this, tr(TEXWORKS_NAME),
962 tr("Cannot read file \"%1\":\n%2")
963 .arg(fileName)
964 .arg(file.errorString()));
965 return QString();
966 }
967
968 QByteArray peekBytes(file.peek(PEEK_LENGTH));
969
970 QString reqName;
971 bool hasMetadata;
972 if (forceCodec)
973 *codecUsed = forceCodec;
974 else {
975 *codecUsed = scanForEncoding(QString::fromUtf8(peekBytes), hasMetadata, reqName);
976 if (*codecUsed == NULL) {
977 *codecUsed = TWApp::instance()->getDefaultCodec();
978 if (hasMetadata) {
979 if (QMessageBox::warning(this, tr("Unrecognized encoding"),
980 tr("The text encoding %1 used in %2 is not supported.\n\n"
981 "It will be interpreted as %3 instead, which may result in incorrect text.")
982 .arg(reqName)
983 .arg(fileName)
984 .arg(QString::fromLatin1((*codecUsed)->name())),
985 QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok) == QMessageBox::Cancel)
986 return QString();
987 }
988 }
989 }
990
991 // When using the UTF-8 codec (mib = 106), byte order marks (BOMs) are
992 // ignored during reading and not produced when writing. To keep them in
993 // files that have them, we need to check for them ourselves.
994 if ((*codecUsed)->mibEnum() == 106 && peekBytes.size() >= 3 && peekBytes[0] == '\xEF' && peekBytes[1] == '\xBB' && peekBytes[2] == '\xBF')
995 utf8BOM = true;
996
997 if (file.atEnd())
998 return QString("");
999 else {
1000 QTextStream in(&file);
1001 in.setCodec(*codecUsed);
1002 QString text = in.readAll();
1003
1004 if (lineEndings != NULL) {
1005 if (text.contains("\r\n")) {
1006 text.replace("\r\n", "\n");
1007 *lineEndings = kLineEnd_CRLF;
1008 }
1009 else if (text.contains("\r") && !text.contains("\n")) {
1010 text.replace("\r", "\n");
1011 *lineEndings = kLineEnd_CR;
1012 }
1013 else
1014 *lineEndings = kLineEnd_LF;
1015
1016 if (text.contains("\r")) {
1017 text.replace("\r", "\n");
1018 *lineEndings |= kLineEnd_Mixed;
1019 }
1020 }
1021
1022 return text;
1023 }
1024 }
1025
loadFile(const QString & fileName,bool asTemplate,bool inBackground,bool reload,QTextCodec * forceCodec)1026 void TeXDocument::loadFile(const QString &fileName, bool asTemplate /* = false */, bool inBackground /* = false */, bool reload /* = false */, QTextCodec * forceCodec /* = NULL */)
1027 {
1028 QString fileContents = readFile(fileName, &codec, &lineEndings, forceCodec);
1029 showLineEndingSetting();
1030 showEncodingSetting();
1031
1032 if (fileContents.isNull())
1033 return;
1034
1035 QApplication::setOverrideCursor(Qt::WaitCursor);
1036
1037 deferTagListChanges = true;
1038 tagListChanged = false;
1039 textEdit->setPlainText(fileContents);
1040 deferTagListChanges = false;
1041 if (tagListChanged)
1042 emit tagListUpdated();
1043
1044 // Ensure the window is shown early (before setPlainText()).
1045 // - this ensures it is shown before the PDF (if opening a new doc)
1046 // - this avoids problems during layouting (which can be broken if the
1047 // geometry, highlighting, ... is changed before the window is shown)
1048 if (!reload)
1049 show();
1050 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
1051
1052 {
1053 // Try to work around QTBUG-20354
1054 // It seems that adding additionalFormats (as is done automatically on
1055 // setPlainText() by the syntax highlighter) can disturb the layouting
1056 // process, leaving some blocks with size zero. This causes the
1057 // corresponding lines to "disappear" and can even crash the application
1058 // in connection with the "highlight current line" feature.
1059 QTextDocument * doc = textEdit->document();
1060 Q_ASSERT(doc != NULL);
1061 QAbstractTextDocumentLayout * docLayout = doc->documentLayout();
1062 Q_ASSERT(docLayout != NULL);
1063
1064 int tries;
1065 for (tries = 0; tries < 10; ++tries) {
1066 bool isLayoutOK = true;
1067 for (QTextBlock b = doc->firstBlock(); b.isValid(); b = b.next()) {
1068 if (docLayout-> blockBoundingRect(b).isEmpty()) {
1069 isLayoutOK = false;
1070 break;
1071 }
1072 }
1073 if (isLayoutOK) break;
1074 // Re-setting the document content naturally triggers a relayout
1075 // (also a rehighlight). Note that layouting only works sensibly
1076 // once show() was called, or else there is no valid widget geometry
1077 // to act as bounding box.
1078 doc->setPlainText(doc->toPlainText());
1079 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
1080 }
1081 if (tries >= 10) {
1082 QMessageBox::warning(this, tr("Layout Problem"), tr("A problem occured while laying out the loaded document in the editor. This is caused by an issue in the underlying Qt framework and can cause TeXworks to crash under certain circumstances. The symptoms of this problem are hidden or overlapping lines. To work around this, please try one of the following:\n -) Turn syntax highlighting off and on\n -) Turn line numbers off and on\n -) Resize the window\n\nWe are sorry for the inconvenience."));
1083 }
1084 }
1085
1086 QApplication::restoreOverrideCursor();
1087
1088 if (asTemplate) {
1089 lastModified = QDateTime();
1090 }
1091 else {
1092 setCurrentFile(fileName);
1093 if (!reload) {
1094 QSETTINGS_OBJECT(settings);
1095 if (!inBackground && settings.value("openPDFwithTeX", kDefault_OpenPDFwithTeX).toBool()) {
1096 openPdfIfAvailable(false);
1097 // Note: openPdfIfAvailable() enables/disables actionGo_to_Preview
1098 // automatically.
1099 }
1100 else {
1101 QString previewFileName;
1102 actionGo_to_Preview->setEnabled(getPreviewFileName(previewFileName));
1103 }
1104 // set openDialogDir after openPdfIfAvailable as we want the .tex file's
1105 // path to end up in that variable (which might be touched/changed when
1106 // loading the pdf
1107 QFileInfo info(fileName);
1108 settings.setValue("openDialogDir", info.canonicalPath());
1109 }
1110
1111 statusBar()->showMessage(tr("File \"%1\" loaded").arg(TWUtils::strippedName(curFile)),
1112 kStatusMessageDuration);
1113 setupFileWatcher();
1114 }
1115 maybeEnableSaveAndRevert(false);
1116
1117 if (!reload) {
1118 bool autoPlace = true;
1119 QMap<QString,QVariant> properties = TWApp::instance()->getFileProperties(curFile);
1120 if (properties.contains("geometry")) {
1121 restoreGeometry(properties.value("geometry").toByteArray());
1122 autoPlace = false;
1123 }
1124 if (properties.contains("state"))
1125 restoreState(properties.value("state").toByteArray(), kTeXWindowStateVersion);
1126
1127 if (properties.contains("selStart")) {
1128 QTextCursor c(textEdit->document());
1129 c.setPosition(properties.value("selStart").toInt());
1130 c.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, properties.value("selLength", 0).toInt());
1131 textEdit->setTextCursor(c);
1132 }
1133
1134 if (properties.contains("quotesMode"))
1135 setSmartQuotesMode(properties.value("quotesMode").toString());
1136 if (properties.contains("indentMode"))
1137 setAutoIndentMode(properties.value("indentMode").toString());
1138 if (properties.contains("syntaxMode"))
1139 setSyntaxColoringMode(properties.value("syntaxMode").toString());
1140 if (properties.contains("wrapLines"))
1141 setWrapLines(properties.value("wrapLines").toBool());
1142 if (properties.contains("lineNumbers"))
1143 setLineNumbers(properties.value("lineNumbers").toBool());
1144
1145 if (pdfDoc) {
1146 if (properties.contains("pdfgeometry")) {
1147 pdfDoc->restoreGeometry(properties.value("pdfgeometry").toByteArray());
1148 autoPlace = false;
1149 }
1150 if (properties.contains("pdfstate"))
1151 pdfDoc->restoreState(properties.value("pdfstate").toByteArray(), kPDFWindowStateVersion);
1152 }
1153
1154 if (autoPlace)
1155 sideBySide();
1156
1157 if (pdfDoc)
1158 pdfDoc->show();
1159
1160 selectWindow();
1161 saveRecentFileInfo();
1162 }
1163
1164 editor()->updateLineNumberAreaWidth(0);
1165
1166 runHooks("LoadFile");
1167 }
1168
delayedInit()1169 void TeXDocument::delayedInit()
1170 {
1171 if (!highlighter) {
1172 QSETTINGS_OBJECT(settings);
1173
1174 highlighter = new TeXHighlighter(textEdit->document(), this);
1175 connect(textEdit, SIGNAL(rehighlight()), highlighter, SLOT(rehighlight()));
1176
1177 // set up syntax highlighting
1178 // First, use the current file's syntaxMode property (if available)
1179 QMap<QString,QVariant> properties = TWApp::instance()->getFileProperties(curFile);
1180 if (properties.contains("syntaxMode"))
1181 setSyntaxColoringMode(properties.value("syntaxMode").toString());
1182 // Secondly, try the global settings
1183 else if (settings.contains("syntaxColoring"))
1184 setSyntaxColoringMode(settings.value("syntaxColoring").toString());
1185 // Lastly, use the default setting
1186 else {
1187 // This should mimick the code in PrefsDialog::doPrefsDialog()
1188 QStringList syntaxOptions = TeXHighlighter::syntaxOptions();
1189 if (kDefault_SyntaxColoring < syntaxOptions.count())
1190 setSyntaxColoringMode(syntaxOptions[kDefault_SyntaxColoring]);
1191 else
1192 setSyntaxColoringMode("");
1193 }
1194
1195 // set the default spell checking language
1196 setSpellcheckLanguage(settings.value("language").toString());
1197
1198 // contentsChanged() parses the modlines (thus possibly overrinding the spell checking language)
1199 contentsChanged(0, 0, 0);
1200 }
1201 }
1202
1203 #define FILE_MODIFICATION_ACCURACY 1000 // in msec
reloadIfChangedOnDisk()1204 void TeXDocument::reloadIfChangedOnDisk()
1205 {
1206 if (isUntitled || !lastModified.isValid())
1207 return;
1208
1209 QDateTime fileModified = QFileInfo(curFile).lastModified();
1210 if (!fileModified.isValid() || fileModified == lastModified)
1211 return;
1212
1213 clearFileWatcher(); // stop watching until next save or reload
1214 if (textEdit->document()->isModified()) {
1215 if (QMessageBox::warning(this, tr("File changed on disk"),
1216 tr("%1 has been modified by another program.\n\n"
1217 "Do you want to discard your current changes, and reload the file from disk?")
1218 .arg(curFile),
1219 QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel) {
1220 lastModified = QDateTime(); // invalidate the timestamp
1221 return;
1222 }
1223 }
1224 // user chose to discard, or there were no local changes
1225 // save the current cursor position
1226 QTextCursor cur;
1227 int oldSelStart, oldSelEnd, oldBlockStart, oldBlockEnd;
1228 int xPos = 0, yPos = 0;
1229 QString oldSel;
1230
1231 // Store the selection (note that oldSelStart == oldSelEnd if there is
1232 // no selection)
1233 cur = textEdit->textCursor();
1234 oldSelStart = cur.selectionStart();
1235 oldSelEnd = cur.selectionEnd();
1236 oldSel = cur.selectedText();
1237
1238 // Get the block number and the offset in the block of the start of the
1239 // selection
1240 cur.setPosition(oldSelStart);
1241 oldBlockStart = cur.blockNumber();
1242 oldSelStart -= cur.block().position();
1243
1244 // Get the block number and the offset in the block of the end of the
1245 // selection
1246 cur.setPosition(oldSelEnd);
1247 oldBlockEnd = cur.blockNumber();
1248 oldSelEnd -= cur.block().position();
1249
1250 // Get the values of the scroll bars so we can later restore the view
1251 if (textEdit->horizontalScrollBar())
1252 xPos = textEdit->horizontalScrollBar()->value();
1253 if (textEdit->verticalScrollBar())
1254 yPos = textEdit->verticalScrollBar()->value();
1255
1256 // Reload the file from the disk
1257 // Note that the file may change again before the system watcher is enabled
1258 // again, so we should catch that case (this sometimes occurs with version
1259 // control systems during commits)
1260 unsigned int i;
1261 // Limit this to avoid infinite loops
1262 for (i = 0; i < 10; ++i) {
1263 clearFileWatcher(); // stop watching until next save or reload
1264 // Only reload files at full seconds to avoid problems with limited
1265 // accuracy of the file system modification timestamps (if the file changes
1266 // twice in one second, the modification timestamp is not altered and we may
1267 // miss the second change otherwise)
1268 while (QDateTime::currentDateTime() <= QFileInfo(curFile).lastModified().addMSecs(FILE_MODIFICATION_ACCURACY))
1269 ; // do nothing
1270 loadFile(curFile, false, true, true);
1271 // one final safety check - if the file has not changed, we can safely end this
1272 if (QDateTime::currentDateTime() > QFileInfo(curFile).lastModified().addMSecs(FILE_MODIFICATION_ACCURACY))
1273 break;
1274 }
1275 if (i == 10) { // the file has been changing constantly - give up and inform the user
1276 QMessageBox::information(this, tr("File changed on disk"),
1277 tr("%1 is constantly being modified by another program.\n\n"
1278 "Please use \"File > Revert to Saved\" manually when the external process has finished.")
1279 .arg(curFile),
1280 QMessageBox::Ok, QMessageBox::Ok);
1281 }
1282
1283 // restore the cursor position
1284 cur = textEdit->textCursor();
1285
1286 // move the cursor to the beginning (this should actually be the case,
1287 // but one never knows)
1288 cur.setPosition(0);
1289
1290 // move the cursor to the starting block
1291 cur.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, oldBlockStart);
1292 cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor);
1293 cur.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, oldSelStart);
1294
1295 // move the cursor to the end block
1296 cur.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor, oldBlockEnd - oldBlockStart);
1297 cur.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
1298 cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, oldSelEnd);
1299
1300 // if the current selection doesn't match the stored selection, collapse
1301 // to the beginning position
1302 if (cur.selectedText() != oldSel)
1303 cur.setPosition(cur.selectionStart());
1304
1305 textEdit->setTextCursor(cur);
1306
1307 // restore the view
1308 if (textEdit->horizontalScrollBar())
1309 textEdit->horizontalScrollBar()->setValue(xPos);
1310 if (textEdit->verticalScrollBar())
1311 textEdit->verticalScrollBar()->setValue(yPos);
1312 }
1313
1314 // get expected name of the Preview file, and return whether it exists
getPreviewFileName(QString & pdfName)1315 bool TeXDocument::getPreviewFileName(QString &pdfName)
1316 {
1317 findRootFilePath();
1318 if (rootFilePath == "")
1319 return false;
1320 QFileInfo fi(rootFilePath);
1321 pdfName = fi.canonicalPath() + "/" + fi.completeBaseName() + ".pdf";
1322 fi.setFile(pdfName);
1323 return fi.exists();
1324 }
1325
openPdfIfAvailable(bool show)1326 bool TeXDocument::openPdfIfAvailable(bool show)
1327 {
1328 detachPdf();
1329 actionSide_by_Side->setEnabled(false);
1330 actionGo_to_Preview->setEnabled(false);
1331
1332 QString pdfName;
1333 if (getPreviewFileName(pdfName)) {
1334 PDFDocument *existingPdf = PDFDocument::findDocument(pdfName);
1335 if (existingPdf != NULL) {
1336 pdfDoc = existingPdf;
1337 pdfDoc->selectWindow();
1338 pdfDoc->linkToSource(this);
1339 }
1340 else {
1341 pdfDoc = new PDFDocument(pdfName, this);
1342 if (show)
1343 pdfDoc->show();
1344 }
1345 }
1346
1347 if (pdfDoc != NULL) {
1348 actionSide_by_Side->setEnabled(true);
1349 actionGo_to_Preview->setEnabled(true);
1350 connect(pdfDoc, SIGNAL(destroyed()), this, SLOT(pdfClosed()));
1351 connect(this, SIGNAL(destroyed(QObject*)), pdfDoc, SLOT(texClosed(QObject*)));
1352 return true;
1353 }
1354
1355 return false;
1356 }
1357
pdfClosed()1358 void TeXDocument::pdfClosed()
1359 {
1360 pdfDoc = NULL;
1361 actionSide_by_Side->setEnabled(false);
1362 }
1363
saveFile(const QString & fileName)1364 bool TeXDocument::saveFile(const QString &fileName)
1365 {
1366 QFileInfo fileInfo(fileName);
1367 QDateTime fileModified = fileInfo.lastModified();
1368 if (fileName == curFile && fileModified.isValid() && fileModified != lastModified) {
1369 if (QMessageBox::warning(this, tr("File changed on disk"),
1370 tr("%1 has been modified by another program.\n\n"
1371 "Do you want to proceed with saving this file, overwriting the version on disk?")
1372 .arg(fileName),
1373 QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel) {
1374 notSaved:
1375 statusBar()->showMessage(tr("Document \"%1\" was not saved")
1376 .arg(TWUtils::strippedName(curFile)),
1377 kStatusMessageDuration);
1378 return false;
1379 }
1380 }
1381
1382 QString theText = textEdit->toPlainText();
1383 switch (lineEndings & kLineEnd_Mask) {
1384 case kLineEnd_CR:
1385 theText.replace("\n", "\r");
1386 break;
1387 case kLineEnd_LF:
1388 break;
1389 case kLineEnd_CRLF:
1390 theText.replace("\n", "\r\n");
1391 break;
1392 }
1393
1394 if (!codec)
1395 codec = TWApp::instance()->getDefaultCodec();
1396 if (!codec->canEncode(theText)) {
1397 if (QMessageBox::warning(this, tr("Text cannot be converted"),
1398 tr("This document contains characters that cannot be represented in the encoding %1.\n\n"
1399 "If you proceed, they will be replaced with default codes. "
1400 "Alternatively, you may wish to use a different encoding (such as UTF-8) to avoid loss of data.")
1401 .arg(QString(codec->name())),
1402 QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel) == QMessageBox::Cancel)
1403 goto notSaved;
1404 }
1405
1406 clearFileWatcher();
1407
1408 {
1409 QFile file(fileName);
1410 if (!file.open(QFile::WriteOnly)) {
1411 QMessageBox::warning(this, tr(TEXWORKS_NAME),
1412 tr("Cannot write file \"%1\":\n%2")
1413 .arg(fileName)
1414 .arg(file.errorString()));
1415 setupFileWatcher();
1416 goto notSaved;
1417 }
1418
1419 QApplication::setOverrideCursor(Qt::WaitCursor);
1420
1421 // When using the UTF-8 codec (mib = 106), byte order marks (BOMs) are
1422 // ignored during reading and not produced when writing. To keep them in
1423 // files that have them (or the user wants them), we need to write them
1424 // ourselves.
1425 if (codec->mibEnum() == 106 && utf8BOM)
1426 file.write("\xEF\xBB\xBF");
1427
1428 if (file.write(codec->fromUnicode(theText)) == -1) {
1429 QApplication::restoreOverrideCursor();
1430 QMessageBox::warning(this, tr("Error writing file"),
1431 tr("An error may have occurred while saving the file. "
1432 "You might like to save a copy in a different location."),
1433 QMessageBox::Ok);
1434 goto notSaved;
1435 }
1436 QApplication::restoreOverrideCursor();
1437 }
1438
1439 setCurrentFile(fileName);
1440 statusBar()->showMessage(tr("File \"%1\" saved")
1441 .arg(TWUtils::strippedName(curFile)),
1442 kStatusMessageDuration);
1443
1444 QTimer::singleShot(0, this, SLOT(setupFileWatcher()));
1445 return true;
1446 }
1447
clearFileWatcher()1448 void TeXDocument::clearFileWatcher()
1449 {
1450 const QStringList files = watcher->files();
1451 if (files.count() > 0)
1452 watcher->removePaths(files);
1453 const QStringList dirs = watcher->directories();
1454 if (dirs.count() > 0)
1455 watcher->removePaths(dirs);
1456 }
1457
setupFileWatcher()1458 void TeXDocument::setupFileWatcher()
1459 {
1460 clearFileWatcher();
1461 if (!isUntitled) {
1462 QFileInfo info(curFile);
1463 lastModified = info.lastModified();
1464 watcher->addPath(curFile);
1465 watcher->addPath(info.canonicalPath());
1466 }
1467 }
1468
setCurrentFile(const QString & fileName)1469 void TeXDocument::setCurrentFile(const QString &fileName)
1470 {
1471 static int sequenceNumber = 1;
1472
1473 curFile = QFileInfo(fileName).canonicalFilePath();
1474 isUntitled = curFile.isEmpty();
1475 if (isUntitled) {
1476 curFile = tr("untitled-%1.tex").arg(sequenceNumber++);
1477 setWindowIcon(QApplication::windowIcon());
1478 }
1479 else {
1480 QIcon winIcon;
1481 #if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
1482 // The Compiz window manager doesn't seem to support icons larger than
1483 // 128x128, so we add a suitable one first
1484 winIcon.addFile(":/images/images/TeXworks-doc-128.png");
1485 #endif
1486 winIcon.addFile(":/images/images/TeXworks-doc.png");
1487 setWindowIcon(winIcon);
1488 }
1489
1490 textEdit->document()->setModified(false);
1491 setWindowModified(false);
1492
1493 setWindowTitle(tr("%1[*] - %2").arg(TWUtils::strippedName(curFile)).arg(tr(TEXWORKS_NAME)));
1494
1495 actionRemove_Aux_Files->setEnabled(!isUntitled);
1496
1497 TWApp::instance()->updateWindowMenus();
1498 }
1499
saveRecentFileInfo()1500 void TeXDocument::saveRecentFileInfo()
1501 {
1502 if (isUntitled)
1503 return;
1504
1505 QMap<QString,QVariant> fileProperties;
1506
1507 fileProperties.insert("path", curFile);
1508 fileProperties.insert("geometry", saveGeometry());
1509 fileProperties.insert("state", saveState(kTeXWindowStateVersion));
1510 fileProperties.insert("selStart", selectionStart());
1511 fileProperties.insert("selLength", selectionLength());
1512 fileProperties.insert("quotesMode", textEdit->getQuotesMode());
1513 fileProperties.insert("indentMode", textEdit->getIndentMode());
1514 if (highlighter)
1515 fileProperties.insert("syntaxMode", highlighter->getSyntaxMode());
1516 fileProperties.insert("lineNumbers", textEdit->getLineNumbersVisible());
1517 fileProperties.insert("wrapLines", textEdit->wordWrapMode() == QTextOption::WordWrap);
1518
1519 if (pdfDoc) {
1520 fileProperties.insert("pdfgeometry", pdfDoc->saveGeometry());
1521 fileProperties.insert("pdfstate", pdfDoc->saveState(kPDFWindowStateVersion));
1522 }
1523
1524 TWApp::instance()->addToRecentFiles(fileProperties);
1525 }
1526
updateRecentFileActions()1527 void TeXDocument::updateRecentFileActions()
1528 {
1529 TWUtils::updateRecentFileActions(this, recentFileActions, menuOpen_Recent, actionClear_Recent_Files);
1530 }
1531
updateWindowMenu()1532 void TeXDocument::updateWindowMenu()
1533 {
1534 TWUtils::updateWindowMenu(this, menuWindow);
1535 }
1536
updateEngineList()1537 void TeXDocument::updateEngineList()
1538 {
1539 engine->disconnect(this);
1540 while (menuRun->actions().count() > 2)
1541 menuRun->removeAction(menuRun->actions().last());
1542 while (engineActions->actions().count() > 0)
1543 engineActions->removeAction(engineActions->actions().last());
1544 engine->clear();
1545 foreach (Engine e, TWApp::instance()->getEngineList()) {
1546 QAction *newAction = new QAction(e.name(), engineActions);
1547 newAction->setCheckable(true);
1548 menuRun->addAction(newAction);
1549 engine->addItem(e.name());
1550 }
1551 connect(engine, SIGNAL(currentIndexChanged(const QString&)), this, SLOT(selectedEngine(const QString&)));
1552 int index = engine->findText(engineName, Qt::MatchFixedString);
1553 if (index < 0)
1554 index = engine->findText(TWApp::instance()->getDefaultEngine().name(), Qt::MatchFixedString);
1555 if (index >= 0)
1556 engine->setCurrentIndex(index);
1557 }
1558
selectedEngine(QAction * engineAction)1559 void TeXDocument::selectedEngine(QAction* engineAction) // sent by actions in menubar menu; update toolbar combo box
1560 {
1561 engineName = engineAction->text();
1562 for (int i = 0; i < engine->count(); ++i)
1563 if (engine->itemText(i) == engineName) {
1564 engine->setCurrentIndex(i);
1565 break;
1566 }
1567 }
1568
selectedEngine(const QString & name)1569 void TeXDocument::selectedEngine(const QString& name) // sent by toolbar combo box; need to update menu
1570 {
1571 engineName = name;
1572 foreach (QAction *act, engineActions->actions()) {
1573 if (act->text() == name) {
1574 act->setChecked(true);
1575 break;
1576 }
1577 }
1578 }
1579
showCursorPosition()1580 void TeXDocument::showCursorPosition()
1581 {
1582 QTextCursor cursor = textEdit->textCursor();
1583 cursor.setPosition(cursor.selectionStart());
1584 int line = cursor.blockNumber() + 1;
1585 int total = textEdit->document()->blockCount();
1586 int col = cursor.position() - textEdit->document()->findBlock(cursor.selectionStart()).position();
1587 lineNumberLabel->setText(tr("Line %1 of %2; col %3").arg(line).arg(total).arg(col));
1588 if (actionAuto_Follow_Focus->isChecked())
1589 emit syncFromSource(curFile, line, col, false);
1590 }
1591
showLineEndingSetting()1592 void TeXDocument::showLineEndingSetting()
1593 {
1594 QString lineEndStr;
1595 switch (lineEndings & kLineEnd_Mask) {
1596 case kLineEnd_LF:
1597 lineEndStr = "LF";
1598 break;
1599 case kLineEnd_CRLF:
1600 lineEndStr = "CRLF";
1601 break;
1602 case kLineEnd_CR:
1603 lineEndStr = "CR";
1604 break;
1605 }
1606 if ((lineEndings & kLineEnd_Mixed) != 0)
1607 lineEndStr += "*";
1608 lineEndingLabel->setText(lineEndStr);
1609 }
1610
lineEndingPopup(const QPoint loc)1611 void TeXDocument::lineEndingPopup(const QPoint loc)
1612 {
1613 QMenu menu;
1614 QAction *cr, *lf, *crlf;
1615 menu.addAction(lf = new QAction("LF (Unix, Mac OS X)", &menu));
1616 menu.addAction(crlf = new QAction("CRLF (Windows)", &menu));
1617 menu.addAction(cr = new QAction("CR (Mac Classic)", &menu));
1618 QAction *result = menu.exec(lineEndingLabel->mapToGlobal(loc));
1619 int newSetting = (lineEndings & kLineEnd_Mask);
1620 if (result == lf)
1621 newSetting = kLineEnd_LF;
1622 else if (result == crlf)
1623 newSetting = kLineEnd_CRLF;
1624 else if (result == cr)
1625 newSetting = kLineEnd_CR;
1626 if (newSetting != (lineEndings & kLineEnd_Mask)) {
1627 lineEndings = newSetting;
1628 showLineEndingSetting();
1629 textEdit->document()->setModified();
1630 }
1631 }
1632
showEncodingSetting()1633 void TeXDocument::showEncodingSetting()
1634 {
1635 encodingLabel->setText(codec ? codec->name() : "");
1636 }
1637
encodingPopup(const QPoint loc)1638 void TeXDocument::encodingPopup(const QPoint loc)
1639 {
1640 QMenu menu;
1641 //: Item in the encoding popup menu
1642 QAction * reloadAction = new QAction(tr("Reload using selected encoding"), &menu);
1643 //: Tooltip for "Reload using selected encoding"
1644 reloadAction->setToolTip(tr("Reloads the current file with the encoding selected from this menu.\n\nThe selected encoding replaces the default one and overrides all \"%!TEX encoding\" lines."));
1645 QAction * BOMAction = new QAction(tr("Write UTF-8 byte order mark"), &menu);
1646 BOMAction->setCheckable(true);
1647 BOMAction->setChecked(utf8BOM);
1648 // Only enable this option if we are currently using the UTF-8 codec
1649 BOMAction->setEnabled(codec && codec->mibEnum() == 106);
1650 QAction * a;
1651
1652 if (!isUntitled)
1653 menu.addAction(reloadAction);
1654 menu.addAction(BOMAction);
1655 menu.addSeparator();
1656
1657 foreach (QTextCodec *codec, *TWUtils::findCodecs()) {
1658 a = new QAction(codec->name(), &menu);
1659 a->setCheckable(true);
1660 if (codec == this->codec)
1661 a->setChecked(true);
1662 menu.addAction(a);
1663 }
1664 QAction *result = menu.exec(encodingLabel->mapToGlobal(loc));
1665 if (result) {
1666 if (result == reloadAction) {
1667 if (textEdit->document()->isModified()) {
1668 if (QMessageBox::warning(this, tr("Unsaved changes"),
1669 tr("The file you are trying to reload has unsaved changes.\n\n"
1670 "Do you want to discard your current changes, and reload the file from disk with the encoding %1?")
1671 .arg(QString(codec->name())),
1672 QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::No) {
1673 return;
1674 }
1675 }
1676 clearFileWatcher(); // stop watching until next save or reload
1677 loadFile(curFile, false, true, true, codec);
1678 ; // FIXME
1679 }
1680 else if (result == BOMAction) {
1681 utf8BOM = BOMAction->isChecked();
1682 // If the UTF-8 codec is selected, changing utf8BOM actually
1683 // modifies how the file is saved. In all other cases, it does not
1684 // take effect until the UTF-8 codec is selected (in which case the
1685 // modified flag is set anyway).
1686 if (codec && codec->mibEnum() == 106)
1687 textEdit->document()->setModified();
1688 }
1689 else {
1690 QTextCodec *newCodec = QTextCodec::codecForName(result->text().toLatin1());
1691 if (newCodec && newCodec != codec) {
1692 codec = newCodec;
1693 showEncodingSetting();
1694 textEdit->document()->setModified();
1695 }
1696 }
1697 }
1698 }
1699
sideBySide()1700 void TeXDocument::sideBySide()
1701 {
1702 if (pdfDoc != NULL) {
1703 TWUtils::sideBySide(this, pdfDoc);
1704 pdfDoc->selectWindow(false);
1705 selectWindow();
1706 }
1707 else
1708 placeOnLeft();
1709 }
1710
findDocument(const QString & fileName)1711 TeXDocument *TeXDocument::findDocument(const QString &fileName)
1712 {
1713 QString canonicalFilePath = QFileInfo(fileName).canonicalFilePath();
1714 if (canonicalFilePath.isEmpty())
1715 canonicalFilePath = fileName;
1716 // file doesn't exist (probably from find-results in a new untitled doc),
1717 // so just use the name as-is
1718
1719 foreach (QWidget *widget, qApp->topLevelWidgets()) {
1720 TeXDocument *theDoc = qobject_cast<TeXDocument*>(widget);
1721 if (theDoc && theDoc->curFile == canonicalFilePath)
1722 return theDoc;
1723 }
1724 return NULL;
1725 }
1726
clear()1727 void TeXDocument::clear()
1728 {
1729 textEdit->textCursor().removeSelectedText();
1730 }
1731
getLineText(int lineNo) const1732 QString TeXDocument::getLineText(int lineNo) const
1733 {
1734 QTextDocument* doc = textEdit->document();
1735 if (lineNo < 1 || lineNo > doc->blockCount())
1736 return QString();
1737 #if QT_VERSION >= 0x040400
1738 return doc->findBlockByNumber(lineNo - 1).text();
1739 #else
1740 QTextBlock block = doc->findBlock(0);
1741 while (--lineNo > 0)
1742 block = block.next();
1743 return block.text();
1744 #endif
1745 }
1746
goToLine(int lineNo,int selStart,int selEnd)1747 void TeXDocument::goToLine(int lineNo, int selStart, int selEnd)
1748 {
1749 QTextDocument* doc = textEdit->document();
1750 if (lineNo < 1 || lineNo > doc->blockCount())
1751 return;
1752 int oldScrollValue = -1;
1753 if (textEdit->verticalScrollBar() != NULL)
1754 oldScrollValue = textEdit->verticalScrollBar()->value();
1755 #if QT_VERSION >= 0x040400
1756 QTextCursor cursor(doc->findBlockByNumber(lineNo - 1));
1757 #else
1758 QTextBlock block = doc->findBlock(0);
1759 while (--lineNo > 0)
1760 block = block.next();
1761 QTextCursor cursor(block);
1762 #endif
1763 if (selStart >= 0 && selEnd >= selStart) {
1764 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, selStart);
1765 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, selEnd - selStart);
1766 }
1767 else
1768 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
1769 textEdit->setTextCursor(cursor);
1770 maybeCenterSelection(oldScrollValue);
1771 }
1772
maybeCenterSelection(int oldScrollValue)1773 void TeXDocument::maybeCenterSelection(int oldScrollValue)
1774 {
1775 if (oldScrollValue != -1 && textEdit->verticalScrollBar() != NULL) {
1776 int newScrollValue = textEdit->verticalScrollBar()->value();
1777 if (newScrollValue != oldScrollValue) {
1778 int delta = (textEdit->height() - textEdit->cursorRect().height()) / 2;
1779 if (newScrollValue < oldScrollValue)
1780 delta = -delta;
1781 textEdit->verticalScrollBar()->setValue(newScrollValue + delta);
1782 }
1783 }
1784 }
1785
doFontDialog()1786 void TeXDocument::doFontDialog()
1787 {
1788 bool ok;
1789 QFont font = QFontDialog::getFont(&ok, textEdit->font());
1790 if (ok) {
1791 textEdit->setFont(font);
1792 font.setPointSize(font.pointSize() - 1);
1793 textEdit_console->setFont(font);
1794 inputLine->setFont(font);
1795 }
1796 }
1797
doLineDialog()1798 void TeXDocument::doLineDialog()
1799 {
1800 QTextCursor cursor = textEdit->textCursor();
1801 cursor.setPosition(cursor.selectionStart());
1802 bool ok;
1803 #if QT_VERSION >= 0x050000
1804 int lineNo = QInputDialog::getInt(this, tr("Go to Line"),
1805 tr("Line number:"), cursor.blockNumber() + 1,
1806 1, textEdit->document()->blockCount(), 1, &ok);
1807 #else
1808 int lineNo = QInputDialog::getInteger(this, tr("Go to Line"),
1809 tr("Line number:"), cursor.blockNumber() + 1,
1810 1, textEdit->document()->blockCount(), 1, &ok);
1811 #endif
1812 if (ok)
1813 goToLine(lineNo);
1814 }
1815
doFindDialog()1816 void TeXDocument::doFindDialog()
1817 {
1818 if (FindDialog::doFindDialog(textEdit) == QDialog::Accepted)
1819 doFindAgain(true);
1820 }
1821
doReplaceDialog()1822 void TeXDocument::doReplaceDialog()
1823 {
1824 ReplaceDialog::DialogCode result;
1825 if ((result = ReplaceDialog::doReplaceDialog(textEdit)) != ReplaceDialog::Cancel)
1826 doReplace(result);
1827 }
1828
prefixLines(const QString & prefix)1829 void TeXDocument::prefixLines(const QString &prefix)
1830 {
1831 QTextCursor cursor = textEdit->textCursor();
1832 cursor.beginEditBlock();
1833 int selStart = cursor.selectionStart();
1834 int selEnd = cursor.selectionEnd();
1835 cursor.setPosition(selStart);
1836 if (!cursor.atBlockStart()) {
1837 cursor.movePosition(QTextCursor::StartOfBlock);
1838 selStart = cursor.position();
1839 }
1840 cursor.setPosition(selEnd);
1841 if (!cursor.atBlockStart() || selEnd == selStart) {
1842 cursor.movePosition(QTextCursor::NextBlock);
1843 selEnd = cursor.position();
1844 }
1845 if (selEnd == selStart)
1846 goto handle_end_of_doc; // special case - cursor in blank line at end of doc
1847 if (!cursor.atBlockStart()) {
1848 cursor.movePosition(QTextCursor::StartOfBlock);
1849 goto handle_end_of_doc; // special case - unterminated last line
1850 }
1851 while (cursor.position() > selStart) {
1852 cursor.movePosition(QTextCursor::PreviousBlock);
1853 handle_end_of_doc:
1854 cursor.insertText(prefix);
1855 cursor.movePosition(QTextCursor::StartOfBlock);
1856 selEnd += prefix.length();
1857 }
1858 cursor.setPosition(selStart);
1859 cursor.setPosition(selEnd, QTextCursor::KeepAnchor);
1860 textEdit->setTextCursor(cursor);
1861 cursor.endEditBlock();
1862 }
1863
doIndent()1864 void TeXDocument::doIndent()
1865 {
1866 prefixLines("\t");
1867 }
1868
doComment()1869 void TeXDocument::doComment()
1870 {
1871 prefixLines("%");
1872 }
1873
unPrefixLines(const QString & prefix)1874 void TeXDocument::unPrefixLines(const QString &prefix)
1875 {
1876 QTextCursor cursor = textEdit->textCursor();
1877 cursor.beginEditBlock();
1878 int selStart = cursor.selectionStart();
1879 int selEnd = cursor.selectionEnd();
1880 cursor.setPosition(selStart);
1881 if (!cursor.atBlockStart()) {
1882 cursor.movePosition(QTextCursor::StartOfBlock);
1883 selStart = cursor.position();
1884 }
1885 cursor.setPosition(selEnd);
1886 if (!cursor.atBlockStart() || selEnd == selStart) {
1887 cursor.movePosition(QTextCursor::NextBlock);
1888 selEnd = cursor.position();
1889 }
1890 if (!cursor.atBlockStart()) {
1891 cursor.movePosition(QTextCursor::StartOfBlock);
1892 goto handle_end_of_doc; // special case - unterminated last line
1893 }
1894 while (cursor.position() > selStart) {
1895 cursor.movePosition(QTextCursor::PreviousBlock);
1896 handle_end_of_doc:
1897 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
1898 QString str = cursor.selectedText();
1899 if (str == prefix) {
1900 cursor.removeSelectedText();
1901 selEnd -= prefix.length();
1902 }
1903 else
1904 cursor.movePosition(QTextCursor::PreviousCharacter);
1905 }
1906 cursor.setPosition(selStart);
1907 cursor.setPosition(selEnd, QTextCursor::KeepAnchor);
1908 textEdit->setTextCursor(cursor);
1909 cursor.endEditBlock();
1910 }
1911
doUnindent()1912 void TeXDocument::doUnindent()
1913 {
1914 unPrefixLines("\t");
1915 }
1916
doUncomment()1917 void TeXDocument::doUncomment()
1918 {
1919 unPrefixLines("%");
1920 }
1921
toUppercase()1922 void TeXDocument::toUppercase()
1923 {
1924 replaceSelection(textEdit->textCursor().selectedText().toUpper());
1925 }
1926
toLowercase()1927 void TeXDocument::toLowercase()
1928 {
1929 replaceSelection(textEdit->textCursor().selectedText().toLower());
1930 }
1931
toggleCase()1932 void TeXDocument::toggleCase()
1933 {
1934 QString theText = textEdit->textCursor().selectedText();
1935 for (int i = 0; i < theText.length(); ++i) {
1936 QCharRef ch = theText[i];
1937 if (ch.isLower())
1938 ch = ch.toUpper();
1939 else
1940 ch = ch.toLower();
1941 }
1942 replaceSelection(theText);
1943 }
1944
replaceSelection(const QString & newText)1945 void TeXDocument::replaceSelection(const QString& newText)
1946 {
1947 QTextCursor cursor = textEdit->textCursor();
1948 int start = cursor.selectionStart();
1949 cursor.insertText(newText);
1950 int end = cursor.selectionEnd();
1951 cursor.setPosition(start);
1952 cursor.setPosition(end, QTextCursor::KeepAnchor);
1953 textEdit->setTextCursor(cursor);
1954 }
1955
selectRange(int start,int length)1956 void TeXDocument::selectRange(int start, int length)
1957 {
1958 QTextCursor c = textCursor();
1959 c.setPosition(start);
1960 c.setPosition(start + length, QTextCursor::KeepAnchor);
1961 editor()->setTextCursor(c);
1962 }
1963
insertText(const QString & text)1964 void TeXDocument::insertText(const QString& text)
1965 {
1966 textCursor().insertText(text);
1967 }
1968
balanceDelimiters()1969 void TeXDocument::balanceDelimiters()
1970 {
1971 const QString text = textEdit->toPlainText();
1972 QTextCursor cursor = textEdit->textCursor();
1973 int openPos = TWUtils::findOpeningDelim(text, cursor.selectionStart());
1974 if (openPos >= 0 && openPos < text.length() - 1) {
1975 do {
1976 int closePos = TWUtils::balanceDelim(text, openPos + 1, TWUtils::closerMatching(text[openPos]), 1);
1977 if (closePos < 0)
1978 break;
1979 if (closePos >= cursor.selectionEnd()) {
1980 cursor.setPosition(openPos);
1981 cursor.setPosition(closePos + 1, QTextCursor::KeepAnchor);
1982 textEdit->setTextCursor(cursor);
1983 return;
1984 }
1985 if (openPos > 0)
1986 openPos = TWUtils::findOpeningDelim(text, openPos - 1);
1987 else
1988 break;
1989 } while (openPos >= 0);
1990 }
1991 QApplication::beep();
1992 }
1993
doHardWrapDialog()1994 void TeXDocument::doHardWrapDialog()
1995 {
1996 HardWrapDialog dlg(this);
1997
1998 dlg.show();
1999 if (dlg.exec()) {
2000 dlg.saveSettings();
2001 doHardWrap(dlg.mode(), dlg.lineWidth(), dlg.rewrap());
2002 }
2003 }
2004
doHardWrap(int mode,int lineWidth,bool rewrap)2005 void TeXDocument::doHardWrap(int mode, int lineWidth, bool rewrap)
2006 {
2007 if (mode == kHardWrapMode_Window) {
2008 // fudge this for now.... not accurate with proportional fonts, ignores tabs,....
2009 QFontMetrics fm(textEdit->currentFont());
2010 lineWidth = textEdit->width() / fm.averageCharWidth();
2011 }
2012 else if (mode == kHardWrapMode_Unwrap) {
2013 lineWidth = INT_MAX;
2014 rewrap = true;
2015 }
2016 if (lineWidth == 0)
2017 return;
2018
2019 QTextCursor cur = textEdit->textCursor();
2020 if (!cur.hasSelection())
2021 cur.select(QTextCursor::Document);
2022
2023 int selStart = cur.selectionStart();
2024 int selEnd = cur.selectionEnd();
2025
2026 cur.setPosition(selStart);
2027 if (!cur.atBlockStart()) {
2028 cur.movePosition(QTextCursor::StartOfBlock);
2029 selStart = cur.position();
2030 }
2031
2032 cur.setPosition(selEnd);
2033 if (!cur.atBlockStart()) {
2034 cur.movePosition(QTextCursor::NextBlock);
2035 selEnd = cur.position();
2036 }
2037
2038 cur.setPosition(selStart);
2039 cur.setPosition(selEnd, QTextCursor::KeepAnchor);
2040
2041 QString oldText = cur.selectedText();
2042 QRegExp breakPattern("\\s+");
2043 QString newText;
2044
2045 while (!oldText.isEmpty()) {
2046 int eol = oldText.indexOf(QChar::ParagraphSeparator);
2047 if (eol == -1)
2048 eol = oldText.length();
2049 else
2050 eol += 1;
2051 QString line = oldText.left(eol);
2052 oldText.remove(0, eol);
2053
2054 if (rewrap && line.trimmed().length() > 0) {
2055 while (!oldText.isEmpty()) {
2056 eol = oldText.indexOf(QChar::ParagraphSeparator);
2057 if (eol == -1)
2058 eol = oldText.length();
2059 QString nextLine = oldText.left(eol).trimmed();
2060 if (nextLine.isEmpty())
2061 break;
2062 line = line.trimmed().append(QChar(' ')).append(nextLine);
2063 oldText.remove(0, eol + 1);
2064 }
2065 }
2066
2067 if (line.length() <= lineWidth) {
2068 newText.append(line);
2069 continue;
2070 }
2071
2072 line = line.trimmed();
2073 if (line.length() <= lineWidth) {
2074 newText.append(line);
2075 continue;
2076 }
2077
2078 int curLength = 0;
2079 while (!line.isEmpty()) {
2080 int breakPoint = line.indexOf(breakPattern);
2081 int matchLen = breakPattern.matchedLength();
2082 if (breakPoint == -1) {
2083 breakPoint = line.length();
2084 matchLen = 0;
2085 }
2086 if (curLength > 0 && curLength + breakPoint >= lineWidth) {
2087 newText.append(QChar::ParagraphSeparator);
2088 curLength = 0;
2089 }
2090 if (curLength > 0) {
2091 newText.append(QChar(' '));
2092 curLength += 1;
2093 }
2094 newText.append(line.left(breakPoint));
2095 curLength += breakPoint;
2096 line.remove(0, breakPoint + matchLen);
2097 }
2098 newText.append(QChar::ParagraphSeparator);
2099 }
2100
2101 cur.insertText(newText);
2102
2103 selEnd = cur.position();
2104 cur.setPosition(selStart);
2105 cur.setPosition(selEnd, QTextCursor::KeepAnchor);
2106 textEdit->setTextCursor(cur);
2107 }
2108
2109
setLineNumbers(bool displayNumbers)2110 void TeXDocument::setLineNumbers(bool displayNumbers)
2111 {
2112 actionLine_Numbers->setChecked(displayNumbers);
2113 textEdit->setLineNumberDisplay(displayNumbers);
2114 }
2115
setWrapLines(bool wrap)2116 void TeXDocument::setWrapLines(bool wrap)
2117 {
2118 actionWrap_Lines->setChecked(wrap);
2119 textEdit->setWordWrapMode(wrap ? QTextOption::WordWrap : QTextOption::NoWrap);
2120 }
2121
setSyntaxColoring(int index)2122 void TeXDocument::setSyntaxColoring(int index)
2123 {
2124 if (highlighter)
2125 highlighter->setActiveIndex(index);
2126 }
2127
setSyntaxColoringMode(const QString & mode)2128 void TeXDocument::setSyntaxColoringMode(const QString& mode)
2129 {
2130 QList<QAction*> actionList = menuSyntax_Coloring->actions();
2131
2132 if (mode == "") {
2133 Q_ASSERT(actionSyntaxColoring_None != NULL);
2134 actionSyntaxColoring_None->trigger();
2135 return;
2136 }
2137 for (int i = 0; i < actionList.count(); ++i) {
2138 if (actionList[i]->isCheckable() && actionList[i]->text().compare(mode, Qt::CaseInsensitive) == 0) {
2139 actionList[i]->trigger();
2140 return;
2141 }
2142 }
2143 }
2144
setSmartQuotesMode(const QString & mode)2145 void TeXDocument::setSmartQuotesMode(const QString& mode)
2146 {
2147 QList<QAction*> actionList = menuSmart_Quotes_Mode->actions();
2148 for (int i = 0; i < actionList.count(); ++i) {
2149 if (actionList[i]->isCheckable() && actionList[i]->text().compare(mode, Qt::CaseInsensitive) == 0) {
2150 actionList[i]->trigger();
2151 return;
2152 }
2153 }
2154 if (mode.isEmpty()) {
2155 actionSmartQuotes_None->trigger();
2156 return;
2157 }
2158 }
2159
setAutoIndentMode(const QString & mode)2160 void TeXDocument::setAutoIndentMode(const QString& mode)
2161 {
2162 QList<QAction*> actionList = menuAuto_indent_Mode->actions();
2163 for (int i = 0; i < actionList.count(); ++i) {
2164 if (actionList[i]->isCheckable() && actionList[i]->text().compare(mode, Qt::CaseInsensitive) == 0) {
2165 actionList[i]->trigger();
2166 return;
2167 }
2168 }
2169 if (mode.isEmpty()) {
2170 actionAutoIndent_None->trigger();
2171 return;
2172 }
2173 }
2174
doFindAgain(bool fromDialog)2175 void TeXDocument::doFindAgain(bool fromDialog)
2176 {
2177 QSETTINGS_OBJECT(settings);
2178 QString searchText = settings.value("searchText").toString();
2179 if (searchText.isEmpty())
2180 return;
2181
2182 QTextDocument::FindFlags flags = (QTextDocument::FindFlags)settings.value("searchFlags").toInt();
2183
2184 QRegExp *regex = NULL;
2185 if (settings.value("searchRegex").toBool()) {
2186 regex = new QRegExp(searchText, ((flags & QTextDocument::FindCaseSensitively) != 0)
2187 ? Qt::CaseSensitive : Qt::CaseInsensitive);
2188 if (!regex->isValid()) {
2189 qApp->beep();
2190 statusBar()->showMessage(tr("Invalid regular expression"), kStatusMessageDuration);
2191 delete regex;
2192 return;
2193 }
2194 }
2195
2196 if (fromDialog && (settings.value("searchFindAll").toBool() || settings.value("searchAllFiles").toBool())) {
2197 bool singleFile = true;
2198 QList<SearchResult> results;
2199 flags &= ~QTextDocument::FindBackward;
2200 int docListIndex = 0;
2201 TeXDocument* theDoc = this;
2202 while (1) {
2203 QTextCursor curs(theDoc->textDoc());
2204 curs.movePosition(QTextCursor::End);
2205 int rangeStart = 0;
2206 int rangeEnd = curs.position();
2207 while (1) {
2208 curs = doSearch(theDoc->textDoc(), searchText, regex, flags, rangeStart, rangeEnd);
2209 if (curs.isNull())
2210 break;
2211 int blockStart = curs.block().position();
2212 results.append(SearchResult(theDoc, curs.blockNumber() + 1,
2213 curs.selectionStart() - blockStart, curs.selectionEnd() - blockStart));
2214 if ((flags & QTextDocument::FindBackward) != 0)
2215 rangeEnd = curs.selectionStart();
2216 else
2217 rangeStart = curs.selectionEnd();
2218 }
2219
2220 if (settings.value("searchAllFiles").toBool() == false)
2221 break;
2222 // go to next document
2223 next_doc:
2224 if (docList[docListIndex] == theDoc)
2225 docListIndex++;
2226 if (docListIndex == docList.count())
2227 break;
2228 theDoc = docList[docListIndex];
2229 if (theDoc == this)
2230 goto next_doc;
2231 singleFile = false;
2232 }
2233
2234 if (results.count() == 0) {
2235 qApp->beep();
2236 statusBar()->showMessage(tr("Not found"), kStatusMessageDuration);
2237 }
2238 else {
2239 SearchResults::presentResults(searchText, results, this, singleFile);
2240 statusBar()->showMessage(tr("Found %n occurrence(s)", "", results.count()), kStatusMessageDuration);
2241 }
2242 }
2243 else {
2244 QTextCursor curs = textEdit->textCursor();
2245 if (settings.value("searchSelection").toBool() && curs.hasSelection()) {
2246 int rangeStart = curs.selectionStart();
2247 int rangeEnd = curs.selectionEnd();
2248 curs = doSearch(textEdit->document(), searchText, regex, flags, rangeStart, rangeEnd);
2249 }
2250 else {
2251 if ((flags & QTextDocument::FindBackward) != 0) {
2252 int rangeStart = 0;
2253 int rangeEnd = curs.selectionStart();
2254 curs = doSearch(textEdit->document(), searchText, regex, flags, rangeStart, rangeEnd);
2255 if (curs.isNull() && settings.value("searchWrap").toBool()) {
2256 curs = QTextCursor(textEdit->document());
2257 curs.movePosition(QTextCursor::End);
2258 curs = doSearch(textEdit->document(), searchText, regex, flags, 0, curs.position());
2259 }
2260 }
2261 else {
2262 int rangeStart = curs.selectionEnd();
2263 curs.movePosition(QTextCursor::End);
2264 int rangeEnd = curs.position();
2265 curs = doSearch(textEdit->document(), searchText, regex, flags, rangeStart, rangeEnd);
2266 if (curs.isNull() && settings.value("searchWrap").toBool())
2267 curs = doSearch(textEdit->document(), searchText, regex, flags, 0, rangeEnd);
2268 }
2269 }
2270
2271 if (curs.isNull()) {
2272 qApp->beep();
2273 statusBar()->showMessage(tr("Not found"), kStatusMessageDuration);
2274 }
2275 else
2276 textEdit->setTextCursor(curs);
2277 }
2278
2279 if (regex != NULL)
2280 delete regex;
2281 }
2282
doReplaceAgain()2283 void TeXDocument::doReplaceAgain()
2284 {
2285 doReplace(ReplaceDialog::ReplaceOne);
2286 }
2287
doReplace(ReplaceDialog::DialogCode mode)2288 void TeXDocument::doReplace(ReplaceDialog::DialogCode mode)
2289 {
2290 QSETTINGS_OBJECT(settings);
2291
2292 QString searchText = settings.value("searchText").toString();
2293 if (searchText.isEmpty())
2294 return;
2295
2296 QTextDocument::FindFlags flags = (QTextDocument::FindFlags)settings.value("searchFlags").toInt();
2297
2298 QRegExp *regex = NULL;
2299 if (settings.value("searchRegex").toBool()) {
2300 regex = new QRegExp(searchText, ((flags & QTextDocument::FindCaseSensitively) != 0)
2301 ? Qt::CaseSensitive : Qt::CaseInsensitive);
2302 if (!regex->isValid()) {
2303 qApp->beep();
2304 statusBar()->showMessage(tr("Invalid regular expression"), kStatusMessageDuration);
2305 delete regex;
2306 return;
2307 }
2308 }
2309
2310 QString replacement = settings.value("replaceText").toString();
2311 if (regex != NULL) {
2312 QRegExp escapedChar("\\\\([nt\\\\]|x([0-9A-Fa-f]{4}))");
2313 int index = -1;
2314 while ((index = replacement.indexOf(escapedChar, index + 1)) >= 0) {
2315 QChar ch;
2316 if (escapedChar.cap(1).length() == 1) {
2317 // single-char escape code newline/tab/backslash
2318 ch = escapedChar.cap(1)[0];
2319 switch (ch.unicode()) {
2320 case 'n':
2321 ch = '\n';
2322 break;
2323 case 't':
2324 ch = '\t';
2325 break;
2326 case '\\':
2327 ch = '\\';
2328 break;
2329 default:
2330 // should not happen!
2331 break;
2332 }
2333 }
2334 else {
2335 // Unicode char number \xHHHH
2336 bool ok;
2337 ch = (QChar)escapedChar.cap(2).toUInt(&ok, 16);
2338 }
2339 replacement.replace(index, escapedChar.matchedLength(), ch);
2340 }
2341 }
2342
2343 bool allFiles = (mode == ReplaceDialog::ReplaceAll) && settings.value("searchAllFiles").toBool();
2344
2345 bool searchWrap = settings.value("searchWrap").toBool();
2346 bool searchSel = settings.value("searchSelection").toBool();
2347
2348 int rangeStart, rangeEnd;
2349 QTextCursor searchRange = textCursor();
2350 if (allFiles) {
2351 searchRange.select(QTextCursor::Document);
2352 rangeStart = searchRange.selectionStart();
2353 rangeEnd = searchRange.selectionEnd();
2354 }
2355 else if (searchSel) {
2356 rangeStart = searchRange.selectionStart();
2357 rangeEnd = searchRange.selectionEnd();
2358 }
2359 else {
2360 // Note: searchWrap is handled separately below
2361 if ((flags & QTextDocument::FindBackward) != 0) {
2362 rangeStart = 0;
2363 rangeEnd = searchRange.selectionEnd();
2364 }
2365 else {
2366 rangeStart = searchRange.selectionStart();
2367 searchRange.select(QTextCursor::Document);
2368 rangeEnd = searchRange.selectionEnd();
2369 }
2370 }
2371
2372 if (mode == ReplaceDialog::ReplaceOne) {
2373 QTextCursor curs = doSearch(textEdit->document(), searchText, regex, flags, rangeStart, rangeEnd);
2374 if (curs.isNull() && searchWrap) {
2375 // If we haven't found anything and wrapping is enabled, try again
2376 // with a "wrapped" search range
2377 if ((flags & QTextDocument::FindBackward) != 0) {
2378 rangeStart = rangeEnd;
2379 searchRange.select(QTextCursor::Document);
2380 rangeEnd = searchRange.selectionEnd();
2381 }
2382 else {
2383 rangeEnd = rangeStart;
2384 rangeStart = 0;
2385 }
2386 curs = doSearch(textEdit->document(), searchText, regex, flags, rangeStart, rangeEnd);
2387 }
2388 if (curs.isNull()) {
2389 qApp->beep();
2390 statusBar()->showMessage(tr("Not found"), kStatusMessageDuration);
2391 }
2392 else {
2393 // do replacement
2394 QString target;
2395 if (regex != NULL)
2396 target = textEdit->document()->toPlainText()
2397 .mid(curs.selectionStart(), curs.selectionEnd() - curs.selectionStart()).replace(*regex, replacement);
2398 else
2399 target = replacement;
2400 curs.insertText(target);
2401 textEdit->setTextCursor(curs);
2402 }
2403 }
2404 else if (mode == ReplaceDialog::ReplaceAll) {
2405 if (allFiles) {
2406 int replacements = 0;
2407 foreach (TeXDocument* doc, docList)
2408 replacements += doc->doReplaceAll(searchText, regex, replacement, flags);
2409 QString numOccurrences = tr("%n occurrence(s)", "", replacements);
2410 QString numDocuments = tr("%n documents", "", docList.count());
2411 QString message = tr("Replaced %1 in %2").arg(numOccurrences).arg(numDocuments);
2412 statusBar()->showMessage(message, kStatusMessageDuration);
2413 }
2414 else {
2415 if (!searchSel) {
2416 // If we are not searching within a selection, we implicitly
2417 // search the whole document with ReplaceAll
2418 searchRange.select(QTextCursor::Document);
2419 rangeStart = searchRange.selectionStart();
2420 rangeEnd = searchRange.selectionEnd();
2421 }
2422 int replacements = doReplaceAll(searchText, regex, replacement, flags, rangeStart, rangeEnd);
2423 statusBar()->showMessage(tr("Replaced %n occurrence(s)", "", replacements), kStatusMessageDuration);
2424 }
2425 }
2426
2427 if (regex != NULL)
2428 delete regex;
2429 }
2430
doReplaceAll(const QString & searchText,QRegExp * regex,const QString & replacement,QTextDocument::FindFlags flags,int rangeStart,int rangeEnd)2431 int TeXDocument::doReplaceAll(const QString& searchText, QRegExp* regex, const QString& replacement,
2432 QTextDocument::FindFlags flags, int rangeStart, int rangeEnd)
2433 {
2434 QTextCursor searchRange = textCursor();
2435 searchRange.select(QTextCursor::Document);
2436 if (rangeStart < 0)
2437 rangeStart = searchRange.selectionStart();
2438 if (rangeEnd < 0)
2439 rangeEnd = searchRange.selectionEnd();
2440
2441 int replacements = 0;
2442 bool first = true;
2443 while (1) {
2444 QTextCursor curs = doSearch(textEdit->document(), searchText, regex, flags, rangeStart, rangeEnd);
2445 if (curs.isNull()) {
2446 if (!first)
2447 searchRange.endEditBlock();
2448 break;
2449 }
2450 if (first) {
2451 searchRange.beginEditBlock();
2452 first = false;
2453 }
2454 QString target;
2455 int oldLen = curs.selectionEnd() - curs.selectionStart();
2456 if (regex != NULL)
2457 target = textEdit->document()->toPlainText().mid(curs.selectionStart(), oldLen).replace(*regex, replacement);
2458 else
2459 target = replacement;
2460 int newLen = target.length();
2461 if ((flags & QTextDocument::FindBackward) != 0)
2462 rangeEnd = curs.selectionStart();
2463 else {
2464 rangeStart = curs.selectionEnd() - oldLen + newLen;
2465 rangeEnd += newLen - oldLen;
2466 }
2467 searchRange.setPosition(curs.selectionStart());
2468 searchRange.setPosition(curs.selectionEnd(), QTextCursor::KeepAnchor);
2469 searchRange.insertText(target);
2470 ++replacements;
2471 }
2472 if (!first) {
2473 searchRange.setPosition(rangeStart);
2474 textEdit->setTextCursor(searchRange);
2475 }
2476 return replacements;
2477 }
2478
doSearch(QTextDocument * theDoc,const QString & searchText,const QRegExp * regex,QTextDocument::FindFlags flags,int s,int e)2479 QTextCursor TeXDocument::doSearch(QTextDocument *theDoc, const QString& searchText, const QRegExp *regex, QTextDocument::FindFlags flags, int s, int e)
2480 {
2481 QTextCursor curs;
2482 const QString& docText = theDoc->toPlainText();
2483
2484 if ((flags & QTextDocument::FindBackward) != 0) {
2485 if (regex != NULL) {
2486 // this doesn't seem to match \n or even \x2029 for newline
2487 // curs = theDoc->find(*regex, e, flags);
2488 int offset = regex->lastIndexIn(docText, e, QRegExp::CaretAtZero);
2489 while (offset >= s && offset + regex->matchedLength() > e)
2490 offset = regex->lastIndexIn(docText, offset - 1, QRegExp::CaretAtZero);
2491 if (offset >= s) {
2492 curs = QTextCursor(theDoc);
2493 curs.setPosition(offset);
2494 curs.setPosition(offset + regex->matchedLength(), QTextCursor::KeepAnchor);
2495 }
2496 }
2497 else {
2498 curs = theDoc->find(searchText, e, flags);
2499 if (!curs.isNull()) {
2500 if (curs.selectionEnd() > e)
2501 curs = theDoc->find(searchText, curs, flags);
2502 if (curs.selectionStart() < s)
2503 curs = QTextCursor();
2504 }
2505 }
2506 }
2507 else {
2508 if (regex != NULL) {
2509 // this doesn't seem to match \n or even \x2029 for newline
2510 // curs = theDoc->find(*regex, s, flags);
2511 int offset = regex->indexIn(docText, s, QRegExp::CaretAtZero);
2512 if (offset >= 0) {
2513 curs = QTextCursor(theDoc);
2514 curs.setPosition(offset);
2515 curs.setPosition(offset + regex->matchedLength(), QTextCursor::KeepAnchor);
2516 }
2517 }
2518 else {
2519 curs = theDoc->find(searchText, s, flags);
2520 }
2521 if (curs.selectionEnd() > e)
2522 curs = QTextCursor();
2523 }
2524 return curs;
2525 }
2526
copyToFind()2527 void TeXDocument::copyToFind()
2528 {
2529 if (textEdit->textCursor().hasSelection()) {
2530 QString searchText = textEdit->textCursor().selectedText();
2531 searchText.replace(QString(0x2029), "\n");
2532 QSETTINGS_OBJECT(settings);
2533 // Note: To search for multi-line strings, we currently need regex
2534 // enabled (since we only have a single search line). If it was not
2535 // enabled, we also need to ensure that the replaceText is escaped
2536 // properly
2537 bool isMultiLine = searchText.contains("\n");
2538 if (isMultiLine && !settings.value("searchRegex").toBool()) {
2539 settings.setValue("searchRegex", true);
2540 settings.setValue("replaceText", QRegExp::escape(settings.value("replaceText").toString()));
2541 }
2542 if (settings.value("searchRegex").toBool()) {
2543 if (isMultiLine)
2544 settings.setValue("searchText", QRegExp::escape(searchText).replace("\n", "\\n"));
2545 else
2546 settings.setValue("searchText", QRegExp::escape(searchText));
2547 }
2548 else
2549 settings.setValue("searchText", searchText);
2550 }
2551 }
2552
copyToReplace()2553 void TeXDocument::copyToReplace()
2554 {
2555 if (textEdit->textCursor().hasSelection()) {
2556 QString replaceText = textEdit->textCursor().selectedText();
2557 replaceText.replace(QString(0x2029), "\n");
2558 QSETTINGS_OBJECT(settings);
2559 // Note: To do multi-line replacements, we currently need regex enabled
2560 // (since we only have a single replace line). If it was not enabled, we
2561 // also need to ensure that the searchText is escaped properly
2562 bool isMultiLine = replaceText.contains("\n");
2563 if (isMultiLine && !settings.value("searchRegex").toBool()) {
2564 settings.setValue("searchRegex", true);
2565 settings.setValue("searchText", QRegExp::escape(settings.value("searchText").toString()));
2566 }
2567 if (settings.value("searchRegex").toBool()) {
2568 if (isMultiLine)
2569 settings.setValue("replaceText", QRegExp::escape(replaceText).replace("\n", "\\n"));
2570 else
2571 settings.setValue("replaceText", QRegExp::escape(replaceText));
2572 }
2573 else
2574 settings.setValue("replaceText", replaceText);
2575 }
2576 }
2577
findSelection()2578 void TeXDocument::findSelection()
2579 {
2580 copyToFind();
2581 doFindAgain();
2582 }
2583
showSelection()2584 void TeXDocument::showSelection()
2585 {
2586 int oldScrollValue = -1;
2587 if (textEdit->verticalScrollBar() != NULL)
2588 oldScrollValue = textEdit->verticalScrollBar()->value();
2589 textEdit->ensureCursorVisible();
2590 maybeCenterSelection(oldScrollValue);
2591 }
2592
zoomToLeft(QWidget * otherWindow)2593 void TeXDocument::zoomToLeft(QWidget *otherWindow)
2594 {
2595 QDesktopWidget *desktop = QApplication::desktop();
2596 QRect screenRect = desktop->availableGeometry(otherWindow == NULL ? this : otherWindow);
2597 screenRect.setTop(screenRect.top() + 22);
2598 screenRect.setLeft(screenRect.left() + 1);
2599 screenRect.setBottom(screenRect.bottom() - 1);
2600 screenRect.setRight((screenRect.left() + screenRect.right()) / 2 - 1);
2601 setGeometry(screenRect);
2602 }
2603
typeset()2604 void TeXDocument::typeset()
2605 {
2606 if (process)
2607 return; // this shouldn't happen if we disable the command at the right time
2608
2609 if (isUntitled || textEdit->document()->isModified())
2610 if (!save()) {
2611 statusBar()->showMessage(tr("Cannot process unsaved document"), kStatusMessageDuration);
2612 return;
2613 }
2614
2615 findRootFilePath();
2616 if (!saveFilesHavingRoot(rootFilePath))
2617 return;
2618
2619 QFileInfo fileInfo(rootFilePath);
2620 if (!fileInfo.isReadable()) {
2621 statusBar()->showMessage(tr("Root document %1 is not readable").arg(rootFilePath), kStatusMessageDuration);
2622 return;
2623 }
2624
2625 Engine e = TWApp::instance()->getNamedEngine(engine->currentText());
2626 if (e.program() == "") {
2627 statusBar()->showMessage(tr("%1 is not properly configured").arg(engine->currentText()), kStatusMessageDuration);
2628 return;
2629 }
2630
2631 process = new QProcess(this);
2632 updateTypesettingAction();
2633
2634 QString workingDir = fileInfo.canonicalPath(); // Note that fileInfo refers to the root file
2635 #if defined(Q_OS_WIN)
2636 // files in the root directory of the current drive have to be handled specially
2637 // because QFileInfo::canonicalPath() returns a path without trailing slash
2638 // (i.e., a bare drive letter)
2639 if (workingDir.length() == 2 && workingDir.endsWith(':'))
2640 workingDir.append('/');
2641 #endif
2642 process->setWorkingDirectory(workingDir);
2643
2644 QStringList env = QProcess::systemEnvironment();
2645 QStringList binPaths = TWApp::instance()->getBinaryPaths(env);
2646
2647 QString exeFilePath = TWApp::instance()->findProgram(e.program(), binPaths);
2648
2649 #if !defined(Q_OS_DARWIN) // not supported on OS X yet :(
2650 // Add a (customized) TEXEDIT environment variable
2651 env << QString("TEXEDIT=%1 --position=%d %s").arg(QCoreApplication::applicationFilePath());
2652
2653 #if defined(Q_OS_WIN) // MiKTeX apparently uses it's own variable
2654 env << QString("MIKTEX_EDITOR=%1 --position=%l \"%f\"").arg(QCoreApplication::applicationFilePath());
2655 #endif
2656 #endif
2657
2658 if (!exeFilePath.isEmpty()) {
2659 QStringList args = e.arguments();
2660
2661 // for old MikTeX versions: delete $synctexoption if it causes an error
2662 static bool checkedForSynctex = false;
2663 static bool synctexSupported = true;
2664 if (!checkedForSynctex) {
2665 QString pdftex = TWApp::instance()->findProgram("pdftex", binPaths);
2666 if (!pdftex.isEmpty()) {
2667 int result = QProcess::execute(pdftex, QStringList() << "-synctex=1" << "-version");
2668 synctexSupported = (result == 0);
2669 }
2670 checkedForSynctex = true;
2671 }
2672 if (!synctexSupported)
2673 args.removeAll("$synctexoption");
2674
2675 args.replaceInStrings("$synctexoption", "-synctex=1");
2676 args.replaceInStrings("$fullname", fileInfo.fileName());
2677 args.replaceInStrings("$basename", fileInfo.completeBaseName());
2678 args.replaceInStrings("$suffix", fileInfo.suffix());
2679 args.replaceInStrings("$directory", fileInfo.absoluteDir().absolutePath());
2680
2681 textEdit_console->clear();
2682 if (consoleTabs->isHidden()) {
2683 keepConsoleOpen = false;
2684 showConsole();
2685 }
2686 else {
2687 inputLine->show();
2688 }
2689 // ensure the window is visible - otherwise we can't see the output
2690 // panel (and the typeset process appears to hang in case of an error)
2691 consoleTabs->setCurrentIndex(0);
2692 raise();
2693
2694 inputLine->setFocus(Qt::OtherFocusReason);
2695 showPdfWhenFinished = e.showPdf();
2696 userInterrupt = false;
2697
2698 process->setEnvironment(env);
2699 process->setProcessChannelMode(QProcess::MergedChannels);
2700
2701 connect(process, SIGNAL(readyReadStandardOutput()), this, SLOT(processStandardOutput()));
2702 connect(process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(processError(QProcess::ProcessError)));
2703 connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(processFinished(int, QProcess::ExitStatus)));
2704
2705 QString pdfName;
2706 if (getPreviewFileName(pdfName))
2707 oldPdfTime = QFileInfo(pdfName).lastModified();
2708 else
2709 oldPdfTime = QDateTime();
2710
2711 // Stop watching the pdf document while it is being changed to avoid
2712 // interference
2713 if (pdfDoc && pdfDoc->widget())
2714 pdfDoc->widget()->setWatchForDocumentChangesOnDisk(false);
2715
2716 process->start(exeFilePath, args);
2717 }
2718 else {
2719 process->deleteLater();
2720 process = NULL;
2721 QMessageBox msgBox(QMessageBox::Critical, tr("Unable to execute %1").arg(e.name()),
2722 "<p>" + tr("The program \"%1\" was not found.").arg(e.program()) + "</p>" +
2723 #if defined(Q_OS_WIN)
2724 "<p>" + tr("You need a <b>TeX distribution</b> like <a href=\"http://tug.org/texlive/\">TeX Live</a> or <a href=\"http://miktex.org/\">MiKTeX</a> installed on your system to typeset your document.") + "</p>" +
2725 #elif defined(Q_OS_DARWIN)
2726 "<p>" + tr("You need a <b>TeX distribution</b> like <a href=\"http://www.tug.org/mactex/\">MacTeX</a> installed on your system to typeset your document.") + "</p>" +
2727 #else // defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
2728 "<p>" + tr("You need a <b>TeX distribution</b> like <a href=\"http://tug.org/texlive/\">TeX Live</a> installed on your system to typeset your document. On most systems such a TeX distribution is available as prebuilt package.") + "</p>" +
2729 #endif
2730 "<p>" + tr("When a TeX distribution is installed you may need to tell TeXworks where to find it in Edit -> Preferences -> Typesetting.") + "</p>",
2731 QMessageBox::Cancel, this);
2732 msgBox.setDetailedText(
2733 tr("Searched in directories:") + "\n" +
2734 " * " + binPaths.join("\n * ") + "\n" +
2735 tr("Check the configuration of the %1 tool and the path settings in the Preferences dialog.").arg(e.name()));
2736 msgBox.exec();
2737 updateTypesettingAction();
2738 }
2739 }
2740
interrupt()2741 void TeXDocument::interrupt()
2742 {
2743 if (process != NULL) {
2744 userInterrupt = true;
2745 process->kill();
2746
2747 // Start watching for changes in the pdf (again)
2748 if (pdfDoc && pdfDoc->widget())
2749 pdfDoc->widget()->setWatchForDocumentChangesOnDisk(true);
2750 }
2751 }
2752
updateTypesettingAction()2753 void TeXDocument::updateTypesettingAction()
2754 {
2755 if (process == NULL) {
2756 disconnect(actionTypeset, SIGNAL(triggered()), this, SLOT(interrupt()));
2757 actionTypeset->setIcon(QIcon(":/images/images/runtool.png"));
2758 actionTypeset->setText(tr("Typeset"));
2759 connect(actionTypeset, SIGNAL(triggered()), this, SLOT(typeset()));
2760 if (pdfDoc != NULL)
2761 pdfDoc->updateTypesettingAction(false);
2762 }
2763 else {
2764 disconnect(actionTypeset, SIGNAL(triggered()), this, SLOT(typeset()));
2765 actionTypeset->setIcon(QIcon(":/images/tango/process-stop.png"));
2766 actionTypeset->setText(tr("Abort typesetting"));
2767 connect(actionTypeset, SIGNAL(triggered()), this, SLOT(interrupt()));
2768 if (pdfDoc != NULL)
2769 pdfDoc->updateTypesettingAction(true);
2770 }
2771 }
2772
processStandardOutput()2773 void TeXDocument::processStandardOutput()
2774 {
2775 QByteArray bytes = process->readAllStandardOutput();
2776 QTextCursor cursor(textEdit_console->document());
2777 cursor.select(QTextCursor::Document);
2778 cursor.setPosition(cursor.selectionEnd());
2779 cursor.insertText(QString::fromUtf8(bytes));
2780 textEdit_console->setTextCursor(cursor);
2781 }
2782
processError(QProcess::ProcessError)2783 void TeXDocument::processError(QProcess::ProcessError /*error*/)
2784 {
2785 if (userInterrupt)
2786 textEdit_console->append(tr("Process interrupted by user"));
2787 else
2788 textEdit_console->append(process->errorString());
2789 process->kill();
2790 process->deleteLater();
2791 process = NULL;
2792 inputLine->hide();
2793 updateTypesettingAction();
2794
2795 // Start watching for changes in the pdf (again)
2796 if (pdfDoc && pdfDoc->widget())
2797 pdfDoc->widget()->setWatchForDocumentChangesOnDisk(true);
2798 }
2799
processFinished(int exitCode,QProcess::ExitStatus exitStatus)2800 void TeXDocument::processFinished(int exitCode, QProcess::ExitStatus exitStatus)
2801 {
2802 // Start watching for changes in the pdf (again)
2803 if (pdfDoc && pdfDoc->widget())
2804 pdfDoc->widget()->setWatchForDocumentChangesOnDisk(true);
2805
2806 if (exitStatus != QProcess::CrashExit) {
2807 QString pdfName;
2808 if (getPreviewFileName(pdfName)) {
2809 actionGo_to_Preview->setEnabled(true);
2810 if (QFileInfo(pdfName).lastModified() != oldPdfTime) {
2811 // only open/refresh the PDF if it was changed by the typeset process
2812 if (pdfDoc == NULL || pdfName != pdfDoc->fileName()) {
2813 if (showPdfWhenFinished && openPdfIfAvailable(true))
2814 pdfDoc->selectWindow();
2815 }
2816 else {
2817 pdfDoc->reload(); // always reload if it is loaded, we don't want a stale window
2818 if (showPdfWhenFinished)
2819 pdfDoc->selectWindow();
2820 }
2821 }
2822 }
2823 else
2824 actionGo_to_Preview->setEnabled(true);
2825 }
2826
2827 executeAfterTypesetHooks();
2828
2829 QSETTINGS_OBJECT(settings);
2830
2831 bool shouldHideConsole = false;
2832 QVariant hideConsoleSetting = settings.value("autoHideConsole", kDefault_HideConsole);
2833 // Backwards compatibility to Tw 0.4.0 and before
2834 if (hideConsoleSetting.toString() == "true" || hideConsoleSetting.toString() == "false")
2835 hideConsoleSetting = (hideConsoleSetting.toBool() ? kDefault_HideConsole : 0);
2836
2837 switch(hideConsoleSetting.toInt()) {
2838 case 0: // Never hide console
2839 shouldHideConsole = false;
2840 break;
2841 case 1: // Hide console automatically
2842 shouldHideConsole = (!keepConsoleOpen && exitCode == 0 && exitStatus != QProcess::CrashExit);
2843 break;
2844 case 2: // Always hide console on success
2845 shouldHideConsole = (exitCode == 0 && exitStatus != QProcess::CrashExit);
2846 break;
2847 default: // Should never happen
2848 ;
2849 }
2850
2851 if (shouldHideConsole)
2852 hideConsole();
2853 else
2854 inputLine->hide();
2855
2856 if (process)
2857 process->deleteLater();
2858 process = NULL;
2859 updateTypesettingAction();
2860 }
2861
executeAfterTypesetHooks()2862 void TeXDocument::executeAfterTypesetHooks()
2863 {
2864 TWScriptManager * scriptManager = TWApp::instance()->getScriptManager();
2865
2866 for (int i = consoleTabs->count() - 1; i > 0; --i)
2867 consoleTabs->removeTab(i);
2868
2869 foreach (TWScript *s, scriptManager->getHookScripts("AfterTypeset")) {
2870 QVariant result;
2871 bool success = s->run(this, result);
2872 if (success && !result.isNull()) {
2873 QString res = result.toString();
2874 if (res.startsWith("<html>", Qt::CaseInsensitive)) {
2875 QTextBrowser *browser = new QTextBrowser(this);
2876 browser->setOpenLinks(false);
2877 connect(browser, SIGNAL(anchorClicked(const QUrl&)), this, SLOT(anchorClicked(const QUrl&)));
2878 browser->setHtml(res);
2879 consoleTabs->addTab(browser, s->getTitle());
2880 }
2881 else {
2882 QTextEdit *textEdit = new QTextEdit(this);
2883 textEdit->setPlainText(res);
2884 textEdit->setReadOnly(true);
2885 consoleTabs->addTab(textEdit, s->getTitle());
2886 }
2887 }
2888 }
2889 }
2890
anchorClicked(const QUrl & url)2891 void TeXDocument::anchorClicked(const QUrl& url)
2892 {
2893 if (url.scheme() == "texworks") {
2894 int line = 0;
2895 if (url.hasFragment()) {
2896 line = url.fragment().toLong();
2897 }
2898 TeXDocument * target = openDocument(QFileInfo(getRootFilePath()).absoluteDir().filePath(url.path()), true, true, line);
2899 if (target)
2900 target->textEdit->setFocus(Qt::OtherFocusReason);
2901 }
2902 else {
2903 TWApp::instance()->openUrl(url);
2904 }
2905 }
2906
2907 // showConsole() and hideConsole() are used internally to update the visibility;
2908 // they must NOT change the keepConsoleOpen setting that records user choice
showConsole()2909 void TeXDocument::showConsole()
2910 {
2911 consoleTabs->show();
2912 if (process != NULL)
2913 inputLine->show();
2914 actionShow_Hide_Console->setText(tr("Hide Console Output"));
2915 }
2916
hideConsole()2917 void TeXDocument::hideConsole()
2918 {
2919 consoleTabs->hide();
2920 inputLine->hide();
2921 actionShow_Hide_Console->setText(tr("Show Console Output"));
2922 }
2923
2924 // this is connected to the user command, so remember the choice
2925 // for when typesetting finishes
toggleConsoleVisibility()2926 void TeXDocument::toggleConsoleVisibility()
2927 {
2928 if (consoleTabs->isVisible()) {
2929 hideConsole();
2930 keepConsoleOpen = false;
2931 }
2932 else {
2933 showConsole();
2934 keepConsoleOpen = true;
2935 }
2936 }
2937
acceptInputLine()2938 void TeXDocument::acceptInputLine()
2939 {
2940 if (process != NULL) {
2941 QString str = inputLine->text();
2942 QTextCursor curs(textEdit_console->document());
2943 curs.setPosition(textEdit_console->toPlainText().length());
2944 textEdit_console->setTextCursor(curs);
2945 QTextCharFormat consoleFormat = textEdit_console->currentCharFormat();
2946 QTextCharFormat inputFormat(consoleFormat);
2947 inputFormat.setForeground(inputLine->palette().text());
2948 str.append("\n");
2949 textEdit_console->insertPlainText(str);
2950 curs.movePosition(QTextCursor::PreviousCharacter);
2951 curs.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor, str.length() - 1);
2952 curs.setCharFormat(inputFormat);
2953 process->write(str.toUtf8());
2954 inputLine->clear();
2955 }
2956 }
2957
goToPreview()2958 void TeXDocument::goToPreview()
2959 {
2960 if (pdfDoc != NULL)
2961 pdfDoc->selectWindow();
2962 else {
2963 if (!openPdfIfAvailable(true)) {
2964 // This should only fail if the user has done something sneaky like closing the
2965 // preview window and then renaming the PDF file, since we opened the source
2966 // and checked that it exists (otherwise Go to Preview would have been disabled).
2967 // We could issue a status-bar warning here but it's a pretty obscure case...
2968 // for now just disable the command.
2969 actionGo_to_Preview->setEnabled(false);
2970 actionSide_by_Side->setEnabled(false);
2971 }
2972 }
2973 }
2974
syncClick(int lineNo,int col)2975 void TeXDocument::syncClick(int lineNo, int col)
2976 {
2977 if (!isUntitled) {
2978 // ensure that there is a pdf to receive our signal
2979 goToPreview();
2980 emit syncFromSource(curFile, lineNo, col, true);
2981 }
2982 }
2983
contentsChanged(int position,int,int)2984 void TeXDocument::contentsChanged(int position, int /*charsRemoved*/, int /*charsAdded*/)
2985 {
2986 if (position < PEEK_LENGTH) {
2987 int pos;
2988 QTextCursor curs(textEdit->document());
2989 // (begin|end)EditBlock() is a workaround for QTBUG-24718 that causes
2990 // movePosition() to crash the program under some circumstances.
2991 // Since we don't change any text in the edit block, it should be a noop
2992 // in the context of undo/redo.
2993 curs.beginEditBlock();
2994 curs.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, PEEK_LENGTH);
2995 curs.endEditBlock();
2996
2997 QString peekStr = curs.selectedText();
2998
2999 /* Search for engine specification */
3000 QRegExp re("% *!TEX +(?:TS-)?program *= *([^\\x2029]+)\\x2029", Qt::CaseInsensitive);
3001 pos = re.indexIn(peekStr);
3002 if (pos > -1) {
3003 QString name = re.cap(1).trimmed();
3004 int index = engine->findText(name, Qt::MatchFixedString);
3005 if (index > -1) {
3006 if (index != engine->currentIndex()) {
3007 engine->setCurrentIndex(index);
3008 emit asyncFlashStatusBarMessage(tr("Set engine to \"%1\"").arg(engine->currentText()), kStatusMessageDuration);
3009 }
3010 }
3011 else {
3012 emit asyncFlashStatusBarMessage(tr("Engine \"%1\" not defined").arg(name), kStatusMessageDuration);
3013 }
3014 }
3015
3016 /* Search for encoding specification */
3017 bool hasMetadata;
3018 QString reqName;
3019 QTextCodec *newCodec = scanForEncoding(peekStr, hasMetadata, reqName);
3020 if (newCodec != NULL) {
3021 codec = newCodec;
3022 showEncodingSetting();
3023 }
3024
3025 /* Search for spellcheck specification */
3026 QRegExp reSpell("% *!TEX +spellcheck *= *([^\\x2029]+)\\x2029", Qt::CaseInsensitive);
3027 pos = reSpell.indexIn(peekStr);
3028 if (pos > -1) {
3029 QString lang = reSpell.cap(1).trimmed();
3030 setSpellcheckLanguage(lang);
3031 }
3032 }
3033 }
3034
findRootFilePath()3035 void TeXDocument::findRootFilePath()
3036 {
3037 if (isUntitled) {
3038 rootFilePath = "";
3039 return;
3040 }
3041 QFileInfo fileInfo(curFile);
3042 QString rootName;
3043 QTextCursor curs(textEdit->document());
3044 curs.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, PEEK_LENGTH);
3045 QString peekStr = curs.selectedText();
3046 QRegExp re("% *!TEX +root *= *([^\\x2029]+)\\x2029", Qt::CaseInsensitive);
3047 int pos = re.indexIn(peekStr);
3048 if (pos > -1) {
3049 rootName = re.cap(1).trimmed();
3050 QFileInfo rootFileInfo(fileInfo.canonicalPath() + "/" + rootName);
3051 if (rootFileInfo.exists())
3052 rootFilePath = rootFileInfo.canonicalFilePath();
3053 else
3054 rootFilePath = rootFileInfo.filePath();
3055 }
3056 else
3057 rootFilePath = fileInfo.canonicalFilePath();
3058 }
3059
addTag(const QTextCursor & cursor,int level,const QString & text)3060 void TeXDocument::addTag(const QTextCursor& cursor, int level, const QString& text)
3061 {
3062 int index = 0;
3063 while (index < tags.size()) {
3064 if (tags[index].cursor.selectionStart() > cursor.selectionStart())
3065 break;
3066 ++index;
3067 }
3068 tags.insert(index, Tag(cursor, level, text));
3069 }
3070
removeTags(int offset,int len)3071 int TeXDocument::removeTags(int offset, int len)
3072 {
3073 int removed = 0;
3074 for (int index = tags.count() - 1; index >= 0; --index) {
3075 if (tags[index].cursor.selectionStart() < offset)
3076 break;
3077 if (tags[index].cursor.selectionStart() < offset + len) {
3078 tags.removeAt(index);
3079 ++removed;
3080 }
3081 }
3082 return removed;
3083 }
3084
goToTag(int index)3085 void TeXDocument::goToTag(int index)
3086 {
3087 if (index < tags.count()) {
3088 textEdit->setTextCursor(tags[index].cursor);
3089 textEdit->setFocus(Qt::OtherFocusReason);
3090 }
3091 }
3092
tagsChanged()3093 void TeXDocument::tagsChanged()
3094 {
3095 if (deferTagListChanges)
3096 tagListChanged = true;
3097 else
3098 emit tagListUpdated();
3099 }
3100
removeAuxFiles()3101 void TeXDocument::removeAuxFiles()
3102 {
3103 findRootFilePath();
3104 if (rootFilePath.isEmpty())
3105 return;
3106
3107 QFileInfo fileInfo(rootFilePath);
3108 QString jobname = fileInfo.completeBaseName();
3109 QDir dir(fileInfo.dir());
3110
3111 QStringList filterList = TWUtils::cleanupPatterns().split(QRegExp("\\s+"));
3112 if (filterList.count() == 0)
3113 return;
3114 for (int i = 0; i < filterList.count(); ++i)
3115 filterList[i].replace("$jobname", jobname);
3116
3117 dir.setNameFilters(filterList);
3118 QStringList auxFileList = dir.entryList(QDir::Files | QDir::CaseSensitive, QDir::Name);
3119 if (auxFileList.count() > 0)
3120 ConfirmDelete::doConfirmDelete(dir, auxFileList);
3121 else
3122 (void)QMessageBox::information(this, tr("No files found"),
3123 tr("No auxiliary files associated with this document at the moment."));
3124 }
3125
3126 #if defined(Q_OS_DARWIN)
3127 #define OPEN_FILE_IN_NEW_WINDOW Qt::MoveAction // unmodified drag appears as MoveAction on Mac OS X
3128 #define INSERT_DOCUMENT_TEXT Qt::CopyAction
3129 #define CREATE_INCLUDE_COMMAND Qt::LinkAction
3130 #else
3131 #define OPEN_FILE_IN_NEW_WINDOW Qt::CopyAction // ...but as CopyAction on X11
3132 #define INSERT_DOCUMENT_TEXT Qt::MoveAction
3133 #define CREATE_INCLUDE_COMMAND Qt::LinkAction
3134 #endif
3135
dragEnterEvent(QDragEnterEvent * event)3136 void TeXDocument::dragEnterEvent(QDragEnterEvent *event)
3137 {
3138 event->ignore();
3139 if (event->mimeData()->hasUrls()) {
3140 const QList<QUrl> urls = event->mimeData()->urls();
3141 foreach (const QUrl& url, urls) {
3142 if (url.scheme() == "file") {
3143 event->acceptProposedAction();
3144 break;
3145 }
3146 }
3147 }
3148 }
3149
dragMoveEvent(QDragMoveEvent * event)3150 void TeXDocument::dragMoveEvent(QDragMoveEvent *event)
3151 {
3152 if (event->proposedAction() == INSERT_DOCUMENT_TEXT || event->proposedAction() == CREATE_INCLUDE_COMMAND) {
3153 if (dragSavedCursor.isNull())
3154 dragSavedCursor = textEdit->textCursor();
3155 QTextCursor curs = textEdit->cursorForPosition(textEdit->mapFromGlobal(mapToGlobal(event->pos())));
3156 textEdit->setTextCursor(curs);
3157 }
3158 else {
3159 if (!dragSavedCursor.isNull()) {
3160 textEdit->setTextCursor(dragSavedCursor);
3161 dragSavedCursor = QTextCursor();
3162 }
3163 }
3164 event->acceptProposedAction();
3165 }
3166
dragLeaveEvent(QDragLeaveEvent * event)3167 void TeXDocument::dragLeaveEvent(QDragLeaveEvent *event)
3168 {
3169 if (!dragSavedCursor.isNull()) {
3170 textEdit->setTextCursor(dragSavedCursor);
3171 dragSavedCursor = QTextCursor();
3172 }
3173 event->accept();
3174 }
3175
dropEvent(QDropEvent * event)3176 void TeXDocument::dropEvent(QDropEvent *event)
3177 {
3178 if (event->mimeData()->hasUrls()) {
3179 Qt::DropAction action = event->proposedAction();
3180 const QList<QUrl> urls = event->mimeData()->urls();
3181 bool editBlockStarted = false;
3182 QString text;
3183 QTextCursor curs = textEdit->cursorForPosition(textEdit->mapFromGlobal(mapToGlobal(event->pos())));
3184 foreach (const QUrl& url, urls) {
3185 if (url.scheme() == "file") {
3186 QString fileName = url.toLocalFile();
3187 switch (action) {
3188 case OPEN_FILE_IN_NEW_WINDOW:
3189 if(!TWUtils::isImageFile(fileName)) {
3190 TWApp::instance()->openFile(fileName);
3191 break;
3192 }
3193 // for graphic files, fall through (there's no point in
3194 // trying to open binary files as text
3195
3196 case INSERT_DOCUMENT_TEXT:
3197 if (!TWUtils::isPDFfile(fileName) && !TWUtils::isImageFile(fileName) && !TWUtils::isPostscriptFile(fileName)) {
3198 QTextCodec *codecUsed;
3199 text = readFile(fileName, &codecUsed);
3200 if (!text.isNull()) {
3201 if (!editBlockStarted) {
3202 curs.beginEditBlock();
3203 editBlockStarted = true;
3204 }
3205 textEdit->setTextCursor(curs);
3206 curs.insertText(text);
3207 }
3208 break;
3209 }
3210 // for graphic files, fall through -- behave the same as the "link" action
3211
3212 case CREATE_INCLUDE_COMMAND:
3213 if (!editBlockStarted) {
3214 curs.beginEditBlock();
3215 editBlockStarted = true;
3216 }
3217 textEdit->setTextCursor(curs);
3218 if (TWUtils::isPDFfile(fileName))
3219 text = TWUtils::includePdfCommand();
3220 else if (TWUtils::isImageFile(fileName))
3221 text = TWUtils::includeImageCommand();
3222 else if (TWUtils::isPostscriptFile(fileName))
3223 text = TWUtils::includePostscriptCommand();
3224 else
3225 text = TWUtils::includeTextCommand();
3226 curs.insertText(text.arg(fileName));
3227 break;
3228 default:
3229 // do nothing
3230 break;
3231 }
3232 }
3233 }
3234 if (editBlockStarted)
3235 curs.endEditBlock();
3236 }
3237 dragSavedCursor = QTextCursor();
3238 event->accept();
3239 }
3240
detachPdf()3241 void TeXDocument::detachPdf()
3242 {
3243 if (pdfDoc != NULL) {
3244 disconnect(pdfDoc, SIGNAL(destroyed()), this, SLOT(pdfClosed()));
3245 disconnect(this, SIGNAL(destroyed(QObject*)), pdfDoc, SLOT(texClosed(QObject*)));
3246 pdfDoc = NULL;
3247 }
3248 }
3249