1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "documentmanager.h"
27
28 #include "icore.h"
29 #include "idocument.h"
30 #include "idocumentfactory.h"
31 #include "coreconstants.h"
32
33 #include <coreplugin/actionmanager/actioncontainer.h>
34 #include <coreplugin/actionmanager/actionmanager.h>
35 #include <coreplugin/actionmanager/command.h>
36 #include <coreplugin/diffservice.h>
37 #include <coreplugin/dialogs/filepropertiesdialog.h>
38 #include <coreplugin/dialogs/readonlyfilesdialog.h>
39 #include <coreplugin/dialogs/saveitemsdialog.h>
40 #include <coreplugin/editormanager/editormanager.h>
41 #include <coreplugin/editormanager/editormanager_p.h>
42 #include <coreplugin/editormanager/editorview.h>
43 #include <coreplugin/editormanager/ieditor.h>
44 #include <coreplugin/editormanager/ieditorfactory.h>
45 #include <coreplugin/editormanager/iexternaleditor.h>
46
47
48 #include <extensionsystem/pluginmanager.h>
49
50 #include <utils/algorithm.h>
51 #include <utils/fileutils.h>
52 #include <utils/globalfilechangeblocker.h>
53 #include <utils/hostosinfo.h>
54 #include <utils/mimetypes/mimedatabase.h>
55 #include <utils/optional.h>
56 #include <utils/pathchooser.h>
57 #include <utils/qtcassert.h>
58 #include <utils/reloadpromptutils.h>
59
60 #include <QStringList>
61 #include <QDateTime>
62 #include <QDir>
63 #include <QFile>
64 #include <QFileInfo>
65 #include <QFileSystemWatcher>
66 #include <QLoggingCategory>
67 #include <QSettings>
68 #include <QTimer>
69 #include <QAction>
70 #include <QFileDialog>
71 #include <QMainWindow>
72 #include <QMenu>
73 #include <QMessageBox>
74
75 static const bool kUseProjectsDirectoryDefault = true;
76 static Q_LOGGING_CATEGORY(log, "qtc.core.documentmanager", QtWarningMsg)
77
78 /*!
79 \class Core::DocumentManager
80 \inheaderfile coreplugin/documentmanager.h
81 \ingroup mainclasses
82 \inmodule QtCreator
83
84 \brief The DocumentManager class manages a set of documents.
85
86 The DocumentManager service monitors a set of IDocument objects.
87
88 This section uses the following terminology:
89
90 \list
91 \li A \e file means a collection of data stored on a disk under a name
92 (that is, the usual meaning of the term \e file in computing).
93 \li A \e document holds content open in Qt Creator. If it corresponds to a
94 file, it might differ from it, because it was modified. But a document
95 might not correspond to a file at all. For example, diff viewer
96 documents or Git blame or log records are created and displayed by
97 Qt Creator upon request.
98 \li An \a editor provides a view into a document that is actually visible
99 to the user and potentially allows editing the document. Multiple
100 editors can open views into the same document.
101 \endlist
102
103 Plugins should register documents they work with at the document management
104 service. The files the IDocument objects point to will be monitored at
105 file system level. If a file changes on disk, the status of the IDocument
106 object will be adjusted accordingly. On application exit the user will be
107 asked to save all modified documents.
108
109 Different IDocument objects in the set can point to the same file in the
110 file system. The monitoring for an IDocument can be blocked by
111 using the \l Core::FileChangeBlocker class.
112
113 The functions \c expectFileChange() and \c unexpectFileChange() mark a file change
114 as expected. On expected file changes all IDocument objects are notified to reload
115 themselves.
116
117 The DocumentManager service also provides convenience functions
118 for saving documents, such as \l saveModifiedDocuments() and
119 \l saveModifiedDocumentsSilently(). They present users with a
120 dialog that lists all modified documents and asks them which
121 documents should be saved.
122
123 The service also manages the list of recent files to be shown to the user.
124
125 \sa addToRecentFiles(), recentFiles()
126 */
127
128 static const char settingsGroupC[] = "RecentFiles";
129 static const char filesKeyC[] = "Files";
130 static const char editorsKeyC[] = "EditorIds";
131
132 static const char directoryGroupC[] = "Directories";
133 static const char projectDirectoryKeyC[] = "Projects";
134 static const char useProjectDirectoryKeyC[] = "UseProjectsDirectory";
135
136 using namespace Utils;
137
138 namespace Core {
139
140 static void readSettings();
141
142 static bool saveModifiedFilesHelper(const QList<IDocument *> &documents,
143 const QString &message,
144 bool *cancelled, bool silently,
145 const QString &alwaysSaveMessage,
146 bool *alwaysSave, QList<IDocument *> *failedToSave);
147
148 namespace Internal {
149
150 struct FileStateItem
151 {
152 QDateTime modified;
153 QFile::Permissions permissions;
154 };
155
156 struct FileState
157 {
158 FilePath watchedFilePath;
159 QMap<IDocument *, FileStateItem> lastUpdatedState;
160 FileStateItem expected;
161 };
162
163
164 class DocumentManagerPrivate : public QObject
165 {
166 Q_OBJECT
167 public:
168 DocumentManagerPrivate();
169 QFileSystemWatcher *fileWatcher();
170 QFileSystemWatcher *linkWatcher();
171
172 void checkOnNextFocusChange();
173 void onApplicationFocusChange();
174
175 void registerSaveAllAction();
176
177 QMap<FilePath, FileState> m_states; // filePathKey -> FileState
178 QSet<FilePath> m_changedFiles; // watched file paths collected from file watcher notifications
179 QList<IDocument *> m_documentsWithoutWatch;
180 QMap<IDocument *, FilePaths> m_documentsWithWatch; // document -> list of filePathKeys
181 QSet<FilePath> m_expectedFileNames; // set of file paths without normalization
182
183 QList<DocumentManager::RecentFile> m_recentFiles;
184
185 bool m_postponeAutoReload = false;
186 bool m_blockActivated = false;
187 bool m_checkOnFocusChange = false;
188 bool m_useProjectsDirectory = kUseProjectsDirectoryDefault;
189
190 QFileSystemWatcher *m_fileWatcher = nullptr; // Delayed creation.
191 QFileSystemWatcher *m_linkWatcher = nullptr; // Delayed creation (only UNIX/if a link is seen).
192 QString m_lastVisitedDirectory = QDir::currentPath();
193 QString m_defaultLocationForNewFiles;
194 FilePath m_projectsDirectory;
195 // When we are calling into an IDocument
196 // we don't want to receive a changed()
197 // signal
198 // That makes the code easier
199 IDocument *m_blockedIDocument = nullptr;
200
201 QAction *m_saveAllAction;
202 };
203
204 static DocumentManager *m_instance;
205 static DocumentManagerPrivate *d;
206
fileWatcher()207 QFileSystemWatcher *DocumentManagerPrivate::fileWatcher()
208 {
209 if (!m_fileWatcher) {
210 m_fileWatcher= new QFileSystemWatcher(m_instance);
211 QObject::connect(m_fileWatcher, &QFileSystemWatcher::fileChanged,
212 m_instance, &DocumentManager::changedFile);
213 }
214 return m_fileWatcher;
215 }
216
linkWatcher()217 QFileSystemWatcher *DocumentManagerPrivate::linkWatcher()
218 {
219 if (HostOsInfo::isAnyUnixHost()) {
220 if (!m_linkWatcher) {
221 m_linkWatcher = new QFileSystemWatcher(m_instance);
222 m_linkWatcher->setObjectName(QLatin1String("_qt_autotest_force_engine_poller"));
223 QObject::connect(m_linkWatcher, &QFileSystemWatcher::fileChanged,
224 m_instance, &DocumentManager::changedFile);
225 }
226 return m_linkWatcher;
227 }
228
229 return fileWatcher();
230 }
231
checkOnNextFocusChange()232 void DocumentManagerPrivate::checkOnNextFocusChange()
233 {
234 m_checkOnFocusChange = true;
235 }
236
onApplicationFocusChange()237 void DocumentManagerPrivate::onApplicationFocusChange()
238 {
239 if (!m_checkOnFocusChange)
240 return;
241 m_checkOnFocusChange = false;
242 m_instance->checkForReload();
243 }
244
registerSaveAllAction()245 void DocumentManagerPrivate::registerSaveAllAction()
246 {
247 ActionContainer *mfile = ActionManager::actionContainer(Constants::M_FILE);
248 Command *cmd = ActionManager::registerAction(m_saveAllAction, Constants::SAVEALL);
249 cmd->setDefaultKeySequence(QKeySequence(useMacShortcuts ? QString() : tr("Ctrl+Shift+S")));
250 mfile->addAction(cmd, Constants::G_FILE_SAVE);
251 m_saveAllAction->setEnabled(false);
252 connect(m_saveAllAction, &QAction::triggered, []() {
253 DocumentManager::saveAllModifiedDocumentsSilently();
254 });
255 }
256
DocumentManagerPrivate()257 DocumentManagerPrivate::DocumentManagerPrivate() :
258 m_saveAllAction(new QAction(tr("Save A&ll"), this))
259 {
260 // we do not want to do too much directly in the focus change event, so queue the connection
261 connect(qApp,
262 &QApplication::focusChanged,
263 this,
264 &DocumentManagerPrivate::onApplicationFocusChange,
265 Qt::QueuedConnection);
266 }
267
268 } // namespace Internal
269 } // namespace Core
270
271 namespace Core {
272
273 using namespace Internal;
274
DocumentManager(QObject * parent)275 DocumentManager::DocumentManager(QObject *parent)
276 : QObject(parent)
277 {
278 d = new DocumentManagerPrivate;
279 m_instance = this;
280
281 connect(Utils::GlobalFileChangeBlocker::instance(), &Utils::GlobalFileChangeBlocker::stateChanged,
282 this, [](bool blocked) {
283 d->m_postponeAutoReload = blocked;
284 if (!blocked)
285 QTimer::singleShot(500, m_instance, &DocumentManager::checkForReload);
286 });
287
288 readSettings();
289
290 if (d->m_useProjectsDirectory)
291 setFileDialogLastVisitedDirectory(d->m_projectsDirectory.toString());
292 }
293
~DocumentManager()294 DocumentManager::~DocumentManager()
295 {
296 delete d;
297 }
298
instance()299 DocumentManager *DocumentManager::instance()
300 {
301 return m_instance;
302 }
303
304 /* Only called from addFileInfo(IDocument *). Adds the document & state to various caches/lists,
305 but does not actually add a watcher. */
addFileInfo(IDocument * document,const FilePath & filePath,const FilePath & filePathKey)306 static void addFileInfo(IDocument *document, const FilePath &filePath, const FilePath &filePathKey)
307 {
308 FileStateItem state;
309 if (!filePath.isEmpty()) {
310 qCDebug(log) << "adding document for" << filePath << "(" << filePathKey << ")";
311 state.modified = filePath.lastModified();
312 state.permissions = filePath.permissions();
313 // Add state if we don't have already
314 if (!d->m_states.contains(filePathKey)) {
315 FileState state;
316 state.watchedFilePath = filePath;
317 d->m_states.insert(filePathKey, state);
318 }
319 d->m_states[filePathKey].lastUpdatedState.insert(document, state);
320 }
321 d->m_documentsWithWatch[document].append(filePathKey); // inserts a new QStringList if not already there
322 }
323
324 /* Adds the IDocuments' file and possibly it's final link target to both m_states
325 (if it's file name is not empty), and the m_filesWithWatch list,
326 and adds a file watcher for each if not already done.
327 (The added file names are guaranteed to be absolute and cleaned.) */
addFileInfos(const QList<IDocument * > & documents)328 static void addFileInfos(const QList<IDocument *> &documents)
329 {
330 FilePaths pathsToWatch;
331 FilePaths linkPathsToWatch;
332 for (IDocument *document : documents) {
333 const FilePath filePath = DocumentManager::filePathKey(document->filePath(),
334 DocumentManager::KeepLinks);
335 const FilePath resolvedFilePath = filePath.canonicalPath();
336 const bool isLink = filePath != resolvedFilePath;
337 addFileInfo(document, filePath, filePath);
338 if (isLink) {
339 addFileInfo(document, resolvedFilePath, resolvedFilePath);
340 if (!filePath.needsDevice()) {
341 linkPathsToWatch.append(d->m_states.value(filePath).watchedFilePath);
342 pathsToWatch.append(d->m_states.value(resolvedFilePath).watchedFilePath);
343 }
344 } else if (!filePath.needsDevice()) {
345 pathsToWatch.append(d->m_states.value(filePath).watchedFilePath);
346 }
347 }
348 // Add or update watcher on file path
349 // This is also used to update the watcher in case of saved (==replaced) files or
350 // update link targets, even if there are multiple documents registered for it
351 if (!pathsToWatch.isEmpty()) {
352 qCDebug(log) << "adding full watch for" << pathsToWatch;
353 d->fileWatcher()->addPaths(Utils::transform(pathsToWatch, &FilePath::toString));
354 }
355 if (!linkPathsToWatch.isEmpty()) {
356 qCDebug(log) << "adding link watch for" << linkPathsToWatch;
357 d->linkWatcher()->addPaths(Utils::transform(linkPathsToWatch, &FilePath::toString));
358 }
359 }
360
361 /*!
362 Adds a list of \a documents to the collection. If \a addWatcher is \c true
363 (the default), the documents' files are added to a file system watcher that
364 notifies the document manager about file changes.
365 */
addDocuments(const QList<IDocument * > & documents,bool addWatcher)366 void DocumentManager::addDocuments(const QList<IDocument *> &documents, bool addWatcher)
367 {
368 if (!addWatcher) {
369 // We keep those in a separate list
370
371 foreach (IDocument *document, documents) {
372 if (document && !d->m_documentsWithoutWatch.contains(document)) {
373 connect(document, &QObject::destroyed,
374 m_instance, &DocumentManager::documentDestroyed);
375 connect(document, &IDocument::filePathChanged,
376 m_instance, &DocumentManager::filePathChanged);
377 connect(document, &IDocument::changed, m_instance, &DocumentManager::updateSaveAll);
378 d->m_documentsWithoutWatch.append(document);
379 }
380 }
381 return;
382 }
383
384 const QList<IDocument *> documentsToWatch = Utils::filtered(documents, [](IDocument *document) {
385 return document && !d->m_documentsWithWatch.contains(document);
386 });
387 for (IDocument *document : documentsToWatch) {
388 connect(document, &IDocument::changed, m_instance, &DocumentManager::checkForNewFileName);
389 connect(document, &QObject::destroyed, m_instance, &DocumentManager::documentDestroyed);
390 connect(document, &IDocument::filePathChanged,
391 m_instance, &DocumentManager::filePathChanged);
392 connect(document, &IDocument::changed, m_instance, &DocumentManager::updateSaveAll);
393 }
394 addFileInfos(documentsToWatch);
395 }
396
397
398 /* Removes all occurrences of the IDocument from m_filesWithWatch and m_states.
399 If that results in a file no longer being referenced by any IDocument, this
400 also removes the file watcher.
401 */
removeFileInfo(IDocument * document)402 static void removeFileInfo(IDocument *document)
403 {
404 if (!d->m_documentsWithWatch.contains(document))
405 return;
406 foreach (const FilePath &filePath, d->m_documentsWithWatch.value(document)) {
407 if (!d->m_states.contains(filePath))
408 continue;
409 qCDebug(log) << "removing document (" << filePath << ")";
410 d->m_states[filePath].lastUpdatedState.remove(document);
411 if (d->m_states.value(filePath).lastUpdatedState.isEmpty()) {
412 const FilePath &watchedFilePath = d->m_states.value(filePath).watchedFilePath;
413 if (!watchedFilePath.needsDevice()) {
414 const QString &localFilePath = watchedFilePath.path();
415 if (d->m_fileWatcher
416 && d->m_fileWatcher->files().contains(localFilePath)) {
417 qCDebug(log) << "removing watch for" << localFilePath;
418 d->m_fileWatcher->removePath(localFilePath);
419 }
420 if (d->m_linkWatcher
421 && d->m_linkWatcher->files().contains(localFilePath)) {
422 qCDebug(log) << "removing watch for" << localFilePath;
423 d->m_linkWatcher->removePath(localFilePath);
424 }
425 }
426 d->m_states.remove(filePath);
427 }
428 }
429 d->m_documentsWithWatch.remove(document);
430 }
431
432 /// Dumps the state of the file manager's map
433 /// For debugging purposes
434 /*
435 static void dump()
436 {
437 qDebug() << "======== dumping state map";
438 QMap<QString, FileState>::const_iterator it, end;
439 it = d->m_states.constBegin();
440 end = d->m_states.constEnd();
441 for (; it != end; ++it) {
442 qDebug() << it.key();
443 qDebug() << " expected:" << it.value().expected.modified;
444
445 QMap<IDocument *, FileStateItem>::const_iterator jt, jend;
446 jt = it.value().lastUpdatedState.constBegin();
447 jend = it.value().lastUpdatedState.constEnd();
448 for (; jt != jend; ++jt) {
449 qDebug() << " " << jt.key()->fileName() << jt.value().modified;
450 }
451 }
452 qDebug() << "------- dumping files with watch list";
453 foreach (IDocument *key, d->m_filesWithWatch.keys()) {
454 qDebug() << key->fileName() << d->m_filesWithWatch.value(key);
455 }
456 qDebug() << "------- dumping watch list";
457 if (d->m_fileWatcher)
458 qDebug() << d->m_fileWatcher->files();
459 qDebug() << "------- dumping link watch list";
460 if (d->m_linkWatcher)
461 qDebug() << d->m_linkWatcher->files();
462 }
463 */
464
465 /*!
466 Tells the document manager that a file has been renamed from \a from to
467 \a to on disk from within \QC.
468
469 Needs to be called right after the actual renaming on disk (that is, before
470 the file system watcher can report the event during the next event loop run).
471
472 \a from needs to be an absolute file path.
473 This will notify all IDocument objects pointing to that file of the rename
474 by calling \l IDocument::setFilePath(), and update the cached time and
475 permission information to avoid annoying the user with \e {the file has
476 been removed} popups.
477 */
renamedFile(const Utils::FilePath & from,const Utils::FilePath & to)478 void DocumentManager::renamedFile(const Utils::FilePath &from, const Utils::FilePath &to)
479 {
480 const Utils::FilePath &fromKey = filePathKey(from, KeepLinks);
481
482 // gather the list of IDocuments
483 QList<IDocument *> documentsToRename;
484 for (auto it = d->m_documentsWithWatch.cbegin(), end = d->m_documentsWithWatch.cend();
485 it != end; ++it) {
486 if (it.value().contains(fromKey))
487 documentsToRename.append(it.key());
488 }
489
490 // rename the IDocuments
491 foreach (IDocument *document, documentsToRename) {
492 d->m_blockedIDocument = document;
493 removeFileInfo(document);
494 document->setFilePath(to);
495 addFileInfos({document});
496 d->m_blockedIDocument = nullptr;
497 }
498 emit m_instance->allDocumentsRenamed(from, to);
499 }
500
filePathChanged(const FilePath & oldName,const FilePath & newName)501 void DocumentManager::filePathChanged(const FilePath &oldName, const FilePath &newName)
502 {
503 auto doc = qobject_cast<IDocument *>(sender());
504 QTC_ASSERT(doc, return);
505 if (doc == d->m_blockedIDocument)
506 return;
507 emit m_instance->documentRenamed(doc, oldName, newName);
508 }
509
updateSaveAll()510 void DocumentManager::updateSaveAll()
511 {
512 d->m_saveAllAction->setEnabled(!modifiedDocuments().empty());
513 }
514
515 /*!
516 Adds \a document to the collection. If \a addWatcher is \c true
517 (the default), the document's file is added to a file system watcher
518 that notifies the document manager about file changes.
519 */
addDocument(IDocument * document,bool addWatcher)520 void DocumentManager::addDocument(IDocument *document, bool addWatcher)
521 {
522 addDocuments({document}, addWatcher);
523 }
524
documentDestroyed(QObject * obj)525 void DocumentManager::documentDestroyed(QObject *obj)
526 {
527 auto document = static_cast<IDocument*>(obj);
528 // Check the special unwatched first:
529 if (!d->m_documentsWithoutWatch.removeOne(document))
530 removeFileInfo(document);
531 }
532
533 /*!
534 Removes \a document from the collection.
535
536 Returns \c true if the document had the \c addWatcher argument to
537 addDocument() set.
538 */
removeDocument(IDocument * document)539 bool DocumentManager::removeDocument(IDocument *document)
540 {
541 QTC_ASSERT(document, return false);
542
543 bool addWatcher = false;
544 // Special casing unwatched files
545 if (!d->m_documentsWithoutWatch.removeOne(document)) {
546 addWatcher = true;
547 removeFileInfo(document);
548 disconnect(document, &IDocument::changed, m_instance, &DocumentManager::checkForNewFileName);
549 }
550 disconnect(document, &QObject::destroyed, m_instance, &DocumentManager::documentDestroyed);
551 disconnect(document, &IDocument::changed, m_instance, &DocumentManager::updateSaveAll);
552 return addWatcher;
553 }
554
555 /* Slot reacting on IDocument::changed. We need to check if the signal was sent
556 because the document was saved under different name. */
checkForNewFileName()557 void DocumentManager::checkForNewFileName()
558 {
559 auto document = qobject_cast<IDocument *>(sender());
560 // We modified the IDocument
561 // Trust the other code to also update the m_states map
562 if (document == d->m_blockedIDocument)
563 return;
564 QTC_ASSERT(document, return);
565 QTC_ASSERT(d->m_documentsWithWatch.contains(document), return);
566
567 // Maybe the name has changed or file has been deleted and created again ...
568 // This also updates the state to the on disk state
569 removeFileInfo(document);
570 addFileInfos({document});
571 }
572
573 /*!
574 Returns a guaranteed cleaned absolute file path for \a filePath.
575 Resolves symlinks if \a resolveMode is ResolveLinks.
576 */
filePathKey(const Utils::FilePath & filePath,ResolveMode resolveMode)577 FilePath DocumentManager::filePathKey(const Utils::FilePath &filePath, ResolveMode resolveMode)
578 {
579 const FilePath &result = filePath.absoluteFilePath().cleanPath();
580 if (resolveMode == ResolveLinks)
581 return result.canonicalPath();
582 return result;
583 }
584
585 /*!
586 Returns the list of IDocuments that have been modified.
587 */
modifiedDocuments()588 QList<IDocument *> DocumentManager::modifiedDocuments()
589 {
590 QList<IDocument *> modified;
591
592 const auto docEnd = d->m_documentsWithWatch.keyEnd();
593 for (auto docIt = d->m_documentsWithWatch.keyBegin(); docIt != docEnd; ++docIt) {
594 IDocument *document = *docIt;
595 if (document->isModified())
596 modified << document;
597 }
598
599 foreach (IDocument *document, d->m_documentsWithoutWatch) {
600 if (document->isModified())
601 modified << document;
602 }
603
604 return modified;
605 }
606
607 /*!
608 Treats any subsequent change to \a fileName as an expected file change.
609
610 \sa unexpectFileChange()
611 */
expectFileChange(const Utils::FilePath & filePath)612 void DocumentManager::expectFileChange(const Utils::FilePath &filePath)
613 {
614 if (filePath.isEmpty())
615 return;
616 d->m_expectedFileNames.insert(filePath);
617 }
618
619 /* only called from unblock and unexpect file change functions */
updateExpectedState(const FilePath & filePathKey)620 static void updateExpectedState(const FilePath &filePathKey)
621 {
622 if (filePathKey.isEmpty())
623 return;
624 if (d->m_states.contains(filePathKey)) {
625 const FilePath watched = d->m_states.value(filePathKey).watchedFilePath;
626 d->m_states[filePathKey].expected.modified = watched.lastModified();
627 d->m_states[filePathKey].expected.permissions = watched.permissions();
628 }
629 }
630
631 /*!
632 Considers all changes to \a fileName unexpected again.
633
634 \sa expectFileChange()
635 */
unexpectFileChange(const FilePath & filePath)636 void DocumentManager::unexpectFileChange(const FilePath &filePath)
637 {
638 // We are updating the expected time of the file
639 // And in changedFile we'll check if the modification time
640 // is the same as the saved one here
641 // If so then it's a expected change
642
643 if (filePath.isEmpty())
644 return;
645 d->m_expectedFileNames.remove(filePath);
646 const FilePath cleanAbsFilePath = filePathKey(filePath, KeepLinks);
647 updateExpectedState(filePathKey(filePath, KeepLinks));
648 const FilePath resolvedCleanAbsFilePath = cleanAbsFilePath.canonicalPath();
649 if (cleanAbsFilePath != resolvedCleanAbsFilePath)
650 updateExpectedState(filePathKey(filePath, ResolveLinks));
651 }
652
saveModifiedFilesHelper(const QList<IDocument * > & documents,const QString & message,bool * cancelled,bool silently,const QString & alwaysSaveMessage,bool * alwaysSave,QList<IDocument * > * failedToSave)653 static bool saveModifiedFilesHelper(const QList<IDocument *> &documents,
654 const QString &message, bool *cancelled, bool silently,
655 const QString &alwaysSaveMessage, bool *alwaysSave,
656 QList<IDocument *> *failedToSave)
657 {
658 if (cancelled)
659 (*cancelled) = false;
660
661 QList<IDocument *> notSaved;
662 QHash<IDocument *, QString> modifiedDocumentsMap;
663 QList<IDocument *> modifiedDocuments;
664
665 foreach (IDocument *document, documents) {
666 if (document && document->isModified() && !document->isTemporary()) {
667 QString name = document->filePath().toString();
668 if (name.isEmpty())
669 name = document->fallbackSaveAsFileName();
670
671 // There can be several IDocuments pointing to the same file
672 // Prefer one that is not readonly
673 // (even though it *should* not happen that the IDocuments are inconsistent with readonly)
674 if (!modifiedDocumentsMap.key(name, nullptr) || !document->isFileReadOnly())
675 modifiedDocumentsMap.insert(document, name);
676 }
677 }
678 modifiedDocuments = modifiedDocumentsMap.keys();
679 if (!modifiedDocuments.isEmpty()) {
680 QList<IDocument *> documentsToSave;
681 if (silently) {
682 documentsToSave = modifiedDocuments;
683 } else {
684 SaveItemsDialog dia(ICore::dialogParent(), modifiedDocuments);
685 if (!message.isEmpty())
686 dia.setMessage(message);
687 if (!alwaysSaveMessage.isNull())
688 dia.setAlwaysSaveMessage(alwaysSaveMessage);
689 if (dia.exec() != QDialog::Accepted) {
690 if (cancelled)
691 (*cancelled) = true;
692 if (alwaysSave)
693 (*alwaysSave) = dia.alwaysSaveChecked();
694 if (failedToSave)
695 (*failedToSave) = modifiedDocuments;
696 const QStringList filesToDiff = dia.filesToDiff();
697 if (!filesToDiff.isEmpty()) {
698 if (auto diffService = DiffService::instance())
699 diffService->diffModifiedFiles(filesToDiff);
700 }
701 return false;
702 }
703 if (alwaysSave)
704 *alwaysSave = dia.alwaysSaveChecked();
705 documentsToSave = dia.itemsToSave();
706 }
707 // Check for files without write permissions.
708 QList<IDocument *> roDocuments;
709 foreach (IDocument *document, documentsToSave) {
710 if (document->isFileReadOnly())
711 roDocuments << document;
712 }
713 if (!roDocuments.isEmpty()) {
714 ReadOnlyFilesDialog roDialog(roDocuments, ICore::dialogParent());
715 roDialog.setShowFailWarning(true, DocumentManager::tr(
716 "Could not save the files.",
717 "error message"));
718 if (roDialog.exec() == ReadOnlyFilesDialog::RO_Cancel) {
719 if (cancelled)
720 (*cancelled) = true;
721 if (failedToSave)
722 (*failedToSave) = modifiedDocuments;
723 return false;
724 }
725 }
726 foreach (IDocument *document, documentsToSave) {
727 if (!EditorManagerPrivate::saveDocument(document)) {
728 if (cancelled)
729 *cancelled = true;
730 notSaved.append(document);
731 }
732 }
733 }
734 if (failedToSave)
735 (*failedToSave) = notSaved;
736 return notSaved.isEmpty();
737 }
738
saveDocument(IDocument * document,const Utils::FilePath & filePath,bool * isReadOnly)739 bool DocumentManager::saveDocument(IDocument *document,
740 const Utils::FilePath &filePath,
741 bool *isReadOnly)
742 {
743 bool ret = true;
744 const Utils::FilePath &savePath = filePath.isEmpty() ? document->filePath() : filePath;
745 expectFileChange(savePath); // This only matters to other IDocuments which refer to this file
746 bool addWatcher = removeDocument(document); // So that our own IDocument gets no notification at all
747
748 QString errorString;
749 if (!document->save(&errorString, filePath, false)) {
750 if (isReadOnly) {
751 QFile ofi(savePath.toString());
752 // Check whether the existing file is writable
753 if (!ofi.open(QIODevice::ReadWrite) && ofi.open(QIODevice::ReadOnly)) {
754 *isReadOnly = true;
755 goto out;
756 }
757 *isReadOnly = false;
758 }
759 QMessageBox::critical(ICore::dialogParent(), tr("File Error"),
760 tr("Error while saving file: %1").arg(errorString));
761 out:
762 ret = false;
763 }
764
765 addDocument(document, addWatcher);
766 unexpectFileChange(savePath);
767 m_instance->updateSaveAll();
768 return ret;
769 }
770
allDocumentFactoryFiltersString(QString * allFilesFilter=nullptr)771 QString DocumentManager::allDocumentFactoryFiltersString(QString *allFilesFilter = nullptr)
772 {
773 QSet<QString> uniqueFilters;
774
775 for (IEditorFactory *factory : IEditorFactory::allEditorFactories()) {
776 for (const QString &mt : factory->mimeTypes()) {
777 const QString filter = mimeTypeForName(mt).filterString();
778 if (!filter.isEmpty())
779 uniqueFilters.insert(filter);
780 }
781 }
782
783 for (IDocumentFactory *factory : IDocumentFactory::allDocumentFactories()) {
784 for (const QString &mt : factory->mimeTypes()) {
785 const QString filter = mimeTypeForName(mt).filterString();
786 if (!filter.isEmpty())
787 uniqueFilters.insert(filter);
788 }
789 }
790
791 QStringList filters = Utils::toList(uniqueFilters);
792 filters.sort();
793 const QString allFiles = Utils::allFilesFilterString();
794 if (allFilesFilter)
795 *allFilesFilter = allFiles;
796 filters.prepend(allFiles);
797 return filters.join(QLatin1String(";;"));
798 }
799
getSaveFileName(const QString & title,const QString & pathIn,const QString & filter,QString * selectedFilter)800 QString DocumentManager::getSaveFileName(const QString &title, const QString &pathIn,
801 const QString &filter, QString *selectedFilter)
802 {
803 const QString &path = pathIn.isEmpty() ? fileDialogInitialDirectory() : pathIn;
804 QString fileName;
805 bool repeat;
806 do {
807 repeat = false;
808 fileName = QFileDialog::getSaveFileName(
809 ICore::dialogParent(), title, path, filter, selectedFilter, QFileDialog::DontConfirmOverwrite);
810 if (!fileName.isEmpty()) {
811 // If the selected filter is All Files (*) we leave the name exactly as the user
812 // specified. Otherwise the suffix must be one available in the selected filter. If
813 // the name already ends with such suffix nothing needs to be done. But if not, the
814 // first one from the filter is appended.
815 if (selectedFilter && *selectedFilter != Utils::allFilesFilterString()) {
816 // Mime database creates filter strings like this: Anything here (*.foo *.bar)
817 const QRegularExpression regExp(QLatin1String(".*\\s+\\((.*)\\)$"));
818 QRegularExpressionMatchIterator matchIt = regExp.globalMatch(*selectedFilter);
819 if (matchIt.hasNext()) {
820 bool suffixOk = false;
821 const QRegularExpressionMatch match = matchIt.next();
822 QString caption = match.captured(1);
823 caption.remove(QLatin1Char('*'));
824 const QStringList suffixes = caption.split(QLatin1Char(' '));
825 for (const QString &suffix : suffixes)
826 if (fileName.endsWith(suffix)) {
827 suffixOk = true;
828 break;
829 }
830 if (!suffixOk && !suffixes.isEmpty())
831 fileName.append(suffixes.at(0));
832 }
833 }
834 if (QFile::exists(fileName)) {
835 if (QMessageBox::warning(ICore::dialogParent(), tr("Overwrite?"),
836 tr("An item named \"%1\" already exists at this location. "
837 "Do you want to overwrite it?").arg(QDir::toNativeSeparators(fileName)),
838 QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
839 repeat = true;
840 }
841 }
842 }
843 } while (repeat);
844 if (!fileName.isEmpty())
845 setFileDialogLastVisitedDirectory(QFileInfo(fileName).absolutePath());
846 return fileName;
847 }
848
getSaveFileNameWithExtension(const QString & title,const QString & pathIn,const QString & filter)849 QString DocumentManager::getSaveFileNameWithExtension(const QString &title, const QString &pathIn,
850 const QString &filter)
851 {
852 QString selected = filter;
853 return getSaveFileName(title, pathIn, filter, &selected);
854 }
855
856 /*!
857 Asks the user for a new file name (\uicontrol {Save File As}) for \a document.
858 */
getSaveAsFileName(const IDocument * document)859 QString DocumentManager::getSaveAsFileName(const IDocument *document)
860 {
861 QTC_ASSERT(document, return QString());
862 const QString filter = allDocumentFactoryFiltersString();
863 const QString filePath = document->filePath().toString();
864 QString selectedFilter;
865 QString fileDialogPath = filePath;
866 if (!filePath.isEmpty()) {
867 selectedFilter = Utils::mimeTypeForFile(filePath).filterString();
868 } else {
869 const QString suggestedName = document->fallbackSaveAsFileName();
870 if (!suggestedName.isEmpty()) {
871 const QList<MimeType> types = Utils::mimeTypesForFileName(suggestedName);
872 if (!types.isEmpty())
873 selectedFilter = types.first().filterString();
874 }
875 const QString defaultPath = document->fallbackSaveAsPath();
876 if (!defaultPath.isEmpty())
877 fileDialogPath = defaultPath + (suggestedName.isEmpty()
878 ? QString()
879 : '/' + suggestedName);
880 }
881 if (selectedFilter.isEmpty())
882 selectedFilter = Utils::mimeTypeForName(document->mimeType()).filterString();
883
884 return getSaveFileName(tr("Save File As"),
885 fileDialogPath,
886 filter,
887 &selectedFilter);
888 }
889
890 /*!
891 Silently saves all documents and returns \c true if all modified documents
892 are saved successfully.
893
894 This method tries to avoid showing dialogs to the user, but can do so anyway
895 (e.g. if a file is not writeable).
896
897 If users canceled any of the dialogs they interacted with, \a canceled
898 is set. If passed to the method, \a failedToClose returns a list of
899 documents that could not be saved.
900 */
saveAllModifiedDocumentsSilently(bool * canceled,QList<IDocument * > * failedToClose)901 bool DocumentManager::saveAllModifiedDocumentsSilently(bool *canceled,
902 QList<IDocument *> *failedToClose)
903 {
904 return saveModifiedDocumentsSilently(modifiedDocuments(), canceled, failedToClose);
905 }
906
907 /*!
908 Silently saves \a documents and returns \c true if all of them were saved
909 successfully.
910
911 This method tries to avoid showing dialogs to the user, but can do so anyway
912 (e.g. if a file is not writeable).
913
914 If users canceled any of the dialogs they interacted with, \a canceled
915 is set. If passed to the method, \a failedToClose returns a list of
916 documents that could not be saved.
917 */
saveModifiedDocumentsSilently(const QList<IDocument * > & documents,bool * canceled,QList<IDocument * > * failedToClose)918 bool DocumentManager::saveModifiedDocumentsSilently(const QList<IDocument *> &documents,
919 bool *canceled,
920 QList<IDocument *> *failedToClose)
921 {
922 return saveModifiedFilesHelper(documents,
923 QString(),
924 canceled,
925 true,
926 QString(),
927 nullptr,
928 failedToClose);
929 }
930
931 /*!
932 Silently saves \a document and returns \c true if it was saved successfully.
933
934 This method tries to avoid showing dialogs to the user, but can do so anyway
935 (e.g. if a file is not writeable).
936
937 If users canceled any of the dialogs they interacted with, \a canceled
938 is set. If passed to the method, \a failedToClose returns a list of
939 documents that could not be saved.
940
941 */
saveModifiedDocumentSilently(IDocument * document,bool * canceled,QList<IDocument * > * failedToClose)942 bool DocumentManager::saveModifiedDocumentSilently(IDocument *document, bool *canceled,
943 QList<IDocument *> *failedToClose)
944 {
945 return saveModifiedDocumentsSilently({document}, canceled, failedToClose);
946 }
947
948 /*!
949 Presents a dialog with all modified documents to users and asks them which
950 of these should be saved.
951
952 This method may show additional dialogs to the user, e.g. if a file is
953 not writeable.
954
955 The dialog text can be set using \a message. If users canceled any
956 of the dialogs they interacted with, \a canceled is set and the
957 method returns \c false.
958
959 The \a alwaysSaveMessage shows an additional checkbox in the dialog.
960 The state of this checkbox is written into \a alwaysSave if set.
961
962 If passed to the method, \a failedToClose returns a list of
963 documents that could not be saved.
964 */
saveAllModifiedDocuments(const QString & message,bool * canceled,const QString & alwaysSaveMessage,bool * alwaysSave,QList<IDocument * > * failedToClose)965 bool DocumentManager::saveAllModifiedDocuments(const QString &message, bool *canceled,
966 const QString &alwaysSaveMessage, bool *alwaysSave,
967 QList<IDocument *> *failedToClose)
968 {
969 return saveModifiedDocuments(modifiedDocuments(), message, canceled,
970 alwaysSaveMessage, alwaysSave, failedToClose);
971 }
972
973 /*!
974 Presents a dialog with \a documents to users and asks them which
975 of these should be saved.
976
977 This method may show additional dialogs to the user, e.g. if a file is
978 not writeable.
979
980 The dialog text can be set using \a message. If users canceled any
981 of the dialogs they interacted with, \a canceled is set and the
982 method returns \c false.
983
984 The \a alwaysSaveMessage shows an additional checkbox in the dialog.
985 The state of this checkbox is written into \a alwaysSave if set.
986
987 If passed to the method, \a failedToClose returns a list of
988 documents that could not be saved.
989 */
saveModifiedDocuments(const QList<IDocument * > & documents,const QString & message,bool * canceled,const QString & alwaysSaveMessage,bool * alwaysSave,QList<IDocument * > * failedToClose)990 bool DocumentManager::saveModifiedDocuments(const QList<IDocument *> &documents,
991 const QString &message, bool *canceled,
992 const QString &alwaysSaveMessage, bool *alwaysSave,
993 QList<IDocument *> *failedToClose)
994 {
995 return saveModifiedFilesHelper(documents, message, canceled, false,
996 alwaysSaveMessage, alwaysSave, failedToClose);
997 }
998
999 /*!
1000 Presents a dialog with the \a document to users and asks them whether
1001 it should be saved.
1002
1003 This method may show additional dialogs to the user, e.g. if a file is
1004 not writeable.
1005
1006 The dialog text can be set using \a message. If users canceled any
1007 of the dialogs they interacted with, \a canceled is set and the
1008 method returns \c false.
1009
1010 The \a alwaysSaveMessage shows an additional checkbox in the dialog.
1011 The state of this checkbox is written into \a alwaysSave if set.
1012
1013 If passed to the method, \a failedToClose returns a list of
1014 documents that could not be saved.
1015 */
saveModifiedDocument(IDocument * document,const QString & message,bool * canceled,const QString & alwaysSaveMessage,bool * alwaysSave,QList<IDocument * > * failedToClose)1016 bool DocumentManager::saveModifiedDocument(IDocument *document, const QString &message, bool *canceled,
1017 const QString &alwaysSaveMessage, bool *alwaysSave,
1018 QList<IDocument *> *failedToClose)
1019 {
1020 return saveModifiedDocuments({document}, message, canceled,
1021 alwaysSaveMessage, alwaysSave, failedToClose);
1022 }
1023
showFilePropertiesDialog(const FilePath & filePath)1024 void DocumentManager::showFilePropertiesDialog(const FilePath &filePath)
1025 {
1026 FilePropertiesDialog properties(filePath);
1027 properties.exec();
1028 }
1029
1030 /*!
1031 Asks the user for a set of file names to be opened. The \a filters
1032 and \a selectedFilter arguments are interpreted like in
1033 QFileDialog::getOpenFileNames(). \a pathIn specifies a path to open the
1034 dialog in if that is not overridden by the user's policy.
1035 */
1036
getOpenFileNames(const QString & filters,const QString & pathIn,QString * selectedFilter)1037 QStringList DocumentManager::getOpenFileNames(const QString &filters,
1038 const QString &pathIn,
1039 QString *selectedFilter)
1040 {
1041 const QString &path = pathIn.isEmpty() ? fileDialogInitialDirectory() : pathIn;
1042 const QStringList files = QFileDialog::getOpenFileNames(ICore::dialogParent(),
1043 tr("Open File"),
1044 path, filters,
1045 selectedFilter);
1046 if (!files.isEmpty())
1047 setFileDialogLastVisitedDirectory(QFileInfo(files.front()).absolutePath());
1048 return files;
1049 }
1050
changedFile(const QString & fileName)1051 void DocumentManager::changedFile(const QString &fileName)
1052 {
1053 const FilePath filePath = FilePath::fromString(fileName);
1054 const bool wasempty = d->m_changedFiles.isEmpty();
1055
1056 if (d->m_states.contains(filePathKey(filePath, KeepLinks)))
1057 d->m_changedFiles.insert(filePath);
1058 qCDebug(log) << "file change notification for" << filePath;
1059
1060 if (wasempty && !d->m_changedFiles.isEmpty())
1061 QTimer::singleShot(200, this, &DocumentManager::checkForReload);
1062 }
1063
checkForReload()1064 void DocumentManager::checkForReload()
1065 {
1066 if (d->m_postponeAutoReload || d->m_changedFiles.isEmpty())
1067 return;
1068 if (QApplication::applicationState() != Qt::ApplicationActive)
1069 return;
1070 // If d->m_blockActivated is true, then it means that the event processing of either the
1071 // file modified dialog, or of loading large files, has delivered a file change event from
1072 // a watcher *and* the timer triggered. We may never end up here in a nested way, so
1073 // recheck later at the end of the checkForReload function.
1074 if (d->m_blockActivated)
1075 return;
1076 if (QApplication::activeModalWidget()) {
1077 // We do not want to prompt for modified file if we currently have some modal dialog open.
1078 // There is no really sensible way to get notified globally if a window closed,
1079 // so just check on every focus change.
1080 d->checkOnNextFocusChange();
1081 return;
1082 }
1083
1084 d->m_blockActivated = true;
1085
1086 IDocument::ReloadSetting defaultBehavior = EditorManager::reloadSetting();
1087 ReloadPromptAnswer previousReloadAnswer = ReloadCurrent;
1088 FileDeletedPromptAnswer previousDeletedAnswer = FileDeletedSave;
1089
1090 QList<IDocument *> documentsToClose;
1091 QHash<IDocument*, QString> documentsToSave;
1092
1093 // collect file information
1094 QMap<FilePath, FileStateItem> currentStates;
1095 QMap<FilePath, IDocument::ChangeType> changeTypes;
1096 QSet<IDocument *> changedIDocuments;
1097 foreach (const FilePath &filePath, d->m_changedFiles) {
1098 const FilePath fileKey = filePathKey(filePath, KeepLinks);
1099 qCDebug(log) << "handling file change for" << filePath << "(" << fileKey << ")";
1100 IDocument::ChangeType type = IDocument::TypeContents;
1101 FileStateItem state;
1102 if (!filePath.exists()) {
1103 qCDebug(log) << "file was removed";
1104 type = IDocument::TypeRemoved;
1105 } else {
1106 state.modified = filePath.lastModified();
1107 state.permissions = filePath.permissions();
1108 qCDebug(log) << "file was modified, time:" << state.modified
1109 << "permissions: " << state.permissions;
1110 }
1111 currentStates.insert(fileKey, state);
1112 changeTypes.insert(fileKey, type);
1113 foreach (IDocument *document, d->m_states.value(fileKey).lastUpdatedState.keys())
1114 changedIDocuments.insert(document);
1115 }
1116
1117 // clean up. do this before we may enter the main loop, otherwise we would
1118 // lose consecutive notifications.
1119 d->m_changedFiles.clear();
1120
1121 // collect information about "expected" file names
1122 // we can't do the "resolving" already in expectFileChange, because
1123 // if the resolved names are different when unexpectFileChange is called
1124 // we would end up with never-unexpected file names
1125 QSet<FilePath> expectedFileKeys;
1126 foreach (const FilePath &filePath, d->m_expectedFileNames) {
1127 const FilePath cleanAbsFilePath = filePathKey(filePath, KeepLinks);
1128 expectedFileKeys.insert(filePathKey(filePath, KeepLinks));
1129 const FilePath resolvedCleanAbsFilePath = cleanAbsFilePath.canonicalPath();
1130 if (cleanAbsFilePath != resolvedCleanAbsFilePath)
1131 expectedFileKeys.insert(filePathKey(filePath, ResolveLinks));
1132 }
1133
1134 // handle the IDocuments
1135 QStringList errorStrings;
1136 QStringList filesToDiff;
1137 foreach (IDocument *document, changedIDocuments) {
1138 IDocument::ChangeTrigger trigger = IDocument::TriggerInternal;
1139 optional<IDocument::ChangeType> type;
1140 bool changed = false;
1141 // find out the type & behavior from the two possible files
1142 // behavior is internal if all changes are expected (and none removed)
1143 // type is "max" of both types (remove > contents > permissions)
1144 foreach (const FilePath &fileKey, d->m_documentsWithWatch.value(document)) {
1145 // was the file reported?
1146 if (!currentStates.contains(fileKey))
1147 continue;
1148
1149 FileStateItem currentState = currentStates.value(fileKey);
1150 FileStateItem expectedState = d->m_states.value(fileKey).expected;
1151 FileStateItem lastState = d->m_states.value(fileKey).lastUpdatedState.value(document);
1152
1153 // did the file actually change?
1154 if (lastState.modified == currentState.modified && lastState.permissions == currentState.permissions)
1155 continue;
1156 changed = true;
1157
1158 // was it only a permission change?
1159 if (lastState.modified == currentState.modified)
1160 continue;
1161
1162 // was the change unexpected?
1163 if ((currentState.modified != expectedState.modified || currentState.permissions != expectedState.permissions)
1164 && !expectedFileKeys.contains(fileKey)) {
1165 trigger = IDocument::TriggerExternal;
1166 }
1167
1168 // find out the type
1169 IDocument::ChangeType fileChange = changeTypes.value(fileKey);
1170 if (fileChange == IDocument::TypeRemoved)
1171 type = IDocument::TypeRemoved;
1172 else if (fileChange == IDocument::TypeContents && !type)
1173 type = IDocument::TypeContents;
1174 }
1175
1176 if (!changed) // probably because the change was blocked with (un)blockFileChange
1177 continue;
1178
1179 // handle it!
1180 d->m_blockedIDocument = document;
1181
1182 // Update file info, also handling if e.g. link target has changed.
1183 // We need to do that before the file is reloaded, because removing the watcher will
1184 // loose any pending change events. Loosing change events *before* the file is reloaded
1185 // doesn't matter, because in that case we then reload the new version of the file already
1186 // anyhow.
1187 removeFileInfo(document);
1188 addFileInfos({document});
1189
1190 bool success = true;
1191 QString errorString;
1192 // we've got some modification
1193 // check if it's contents or permissions:
1194 if (!type) {
1195 // Only permission change
1196 document->checkPermissions();
1197 success = true;
1198 // now we know it's a content change or file was removed
1199 } else if (defaultBehavior == IDocument::ReloadUnmodified && type == IDocument::TypeContents
1200 && !document->isModified()) {
1201 // content change, but unmodified (and settings say to reload in this case)
1202 success = document->reload(&errorString, IDocument::FlagReload, *type);
1203 // file was removed or it's a content change and the default behavior for
1204 // unmodified files didn't kick in
1205 } else if (defaultBehavior == IDocument::ReloadUnmodified && type == IDocument::TypeRemoved
1206 && !document->isModified()) {
1207 // file removed, but unmodified files should be reloaded
1208 // so we close the file
1209 documentsToClose << document;
1210 } else if (defaultBehavior == IDocument::IgnoreAll) {
1211 // content change or removed, but settings say ignore
1212 success = document->reload(&errorString, IDocument::FlagIgnore, *type);
1213 // either the default behavior is to always ask,
1214 // or the ReloadUnmodified default behavior didn't kick in,
1215 // so do whatever the IDocument wants us to do
1216 } else {
1217 // check if IDocument wants us to ask
1218 if (document->reloadBehavior(trigger, *type) == IDocument::BehaviorSilent) {
1219 // content change or removed, IDocument wants silent handling
1220 if (type == IDocument::TypeRemoved)
1221 documentsToClose << document;
1222 else
1223 success = document->reload(&errorString, IDocument::FlagReload, *type);
1224 // IDocument wants us to ask
1225 } else if (type == IDocument::TypeContents) {
1226 // content change, IDocument wants to ask user
1227 if (previousReloadAnswer == ReloadNone || previousReloadAnswer == ReloadNoneAndDiff) {
1228 // answer already given, ignore
1229 success = document->reload(&errorString, IDocument::FlagIgnore, IDocument::TypeContents);
1230 } else if (previousReloadAnswer == ReloadAll) {
1231 // answer already given, reload
1232 success = document->reload(&errorString, IDocument::FlagReload, IDocument::TypeContents);
1233 } else {
1234 // Ask about content change
1235 previousReloadAnswer = reloadPrompt(document->filePath(), document->isModified(),
1236 DiffService::instance(),
1237 ICore::dialogParent());
1238 switch (previousReloadAnswer) {
1239 case ReloadAll:
1240 case ReloadCurrent:
1241 success = document->reload(&errorString, IDocument::FlagReload, IDocument::TypeContents);
1242 break;
1243 case ReloadSkipCurrent:
1244 case ReloadNone:
1245 case ReloadNoneAndDiff:
1246 success = document->reload(&errorString, IDocument::FlagIgnore, IDocument::TypeContents);
1247 break;
1248 case CloseCurrent:
1249 documentsToClose << document;
1250 break;
1251 }
1252 }
1253 if (previousReloadAnswer == ReloadNoneAndDiff)
1254 filesToDiff.append(document->filePath().toString());
1255
1256 // IDocument wants us to ask, and it's the TypeRemoved case
1257 } else {
1258 // Ask about removed file
1259 bool unhandled = true;
1260 while (unhandled) {
1261 if (previousDeletedAnswer != FileDeletedCloseAll) {
1262 previousDeletedAnswer =
1263 fileDeletedPrompt(document->filePath().toString(),
1264 ICore::dialogParent());
1265 }
1266 switch (previousDeletedAnswer) {
1267 case FileDeletedSave:
1268 documentsToSave.insert(document, document->filePath().toString());
1269 unhandled = false;
1270 break;
1271 case FileDeletedSaveAs:
1272 {
1273 const QString &saveFileName = getSaveAsFileName(document);
1274 if (!saveFileName.isEmpty()) {
1275 documentsToSave.insert(document, saveFileName);
1276 unhandled = false;
1277 }
1278 break;
1279 }
1280 case FileDeletedClose:
1281 case FileDeletedCloseAll:
1282 documentsToClose << document;
1283 unhandled = false;
1284 break;
1285 }
1286 }
1287 }
1288 }
1289 if (!success) {
1290 if (errorString.isEmpty())
1291 errorStrings << tr("Cannot reload %1").arg(document->filePath().toUserOutput());
1292 else
1293 errorStrings << errorString;
1294 }
1295
1296 d->m_blockedIDocument = nullptr;
1297 }
1298
1299 if (!filesToDiff.isEmpty()) {
1300 if (auto diffService = DiffService::instance())
1301 diffService->diffModifiedFiles(filesToDiff);
1302 }
1303
1304 if (!errorStrings.isEmpty())
1305 QMessageBox::critical(ICore::dialogParent(), tr("File Error"),
1306 errorStrings.join(QLatin1Char('\n')));
1307
1308 // handle deleted files
1309 EditorManager::closeDocuments(documentsToClose, false);
1310 for (auto it = documentsToSave.cbegin(), end = documentsToSave.cend(); it != end; ++it) {
1311 saveDocument(it.key(), Utils::FilePath::fromString(it.value()));
1312 it.key()->checkPermissions();
1313 }
1314
1315 d->m_blockActivated = false;
1316 // re-check in case files where modified while the dialog was open
1317 QMetaObject::invokeMethod(this, &DocumentManager::checkForReload, Qt::QueuedConnection);
1318 // dump();
1319 }
1320
1321 /*!
1322 Adds the \a filePath to the list of recent files. Associates the file to
1323 be reopened with the editor that has the specified \a editorId, if possible.
1324 \a editorId defaults to the empty ID, which lets \QC figure out
1325 the best editor itself.
1326 */
addToRecentFiles(const Utils::FilePath & filePath,Id editorId)1327 void DocumentManager::addToRecentFiles(const Utils::FilePath &filePath, Id editorId)
1328 {
1329 if (filePath.isEmpty())
1330 return;
1331 const FilePath fileKey = filePathKey(filePath, KeepLinks);
1332 Utils::erase(d->m_recentFiles, [fileKey](const RecentFile &file) {
1333 return fileKey == filePathKey(file.first, DocumentManager::KeepLinks);
1334 });
1335 while (d->m_recentFiles.count() >= EditorManagerPrivate::maxRecentFiles())
1336 d->m_recentFiles.removeLast();
1337 d->m_recentFiles.prepend(RecentFile(filePath, editorId));
1338 }
1339
1340 /*!
1341 Clears the list of recent files. Should only be called by
1342 the core plugin when the user chooses to clear the list.
1343 */
clearRecentFiles()1344 void DocumentManager::clearRecentFiles()
1345 {
1346 d->m_recentFiles.clear();
1347 }
1348
1349 /*!
1350 Returns the list of recent files.
1351 */
recentFiles()1352 QList<DocumentManager::RecentFile> DocumentManager::recentFiles()
1353 {
1354 return d->m_recentFiles;
1355 }
1356
saveSettings()1357 void DocumentManager::saveSettings()
1358 {
1359 QVariantList recentFiles;
1360 QStringList recentEditorIds;
1361 foreach (const RecentFile &file, d->m_recentFiles) {
1362 recentFiles.append(file.first.toVariant());
1363 recentEditorIds.append(file.second.toString());
1364 }
1365
1366 QtcSettings *s = ICore::settings();
1367 s->beginGroup(settingsGroupC);
1368 s->setValueWithDefault(filesKeyC, recentFiles);
1369 s->setValueWithDefault(editorsKeyC, recentEditorIds);
1370 s->endGroup();
1371 s->beginGroup(directoryGroupC);
1372 s->setValueWithDefault(projectDirectoryKeyC,
1373 d->m_projectsDirectory.toString(),
1374 PathChooser::homePath());
1375 s->setValueWithDefault(useProjectDirectoryKeyC,
1376 d->m_useProjectsDirectory,
1377 kUseProjectsDirectoryDefault);
1378 s->endGroup();
1379 }
1380
readSettings()1381 void readSettings()
1382 {
1383 QSettings *s = ICore::settings();
1384 d->m_recentFiles.clear();
1385 s->beginGroup(QLatin1String(settingsGroupC));
1386 const QVariantList recentFiles = s->value(QLatin1String(filesKeyC)).toList();
1387 const QStringList recentEditorIds = s->value(QLatin1String(editorsKeyC)).toStringList();
1388 s->endGroup();
1389 // clean non-existing files
1390 for (int i = 0, n = recentFiles.size(); i < n; ++i) {
1391 QString editorId;
1392 if (i < recentEditorIds.size()) // guard against old or weird settings
1393 editorId = recentEditorIds.at(i);
1394 const Utils::FilePath &filePath = FilePath::fromVariant(recentFiles.at(i));
1395 if (filePath.exists() && !filePath.isDir())
1396 d->m_recentFiles.append({filePath, Id::fromString(editorId)});
1397 }
1398
1399 s->beginGroup(QLatin1String(directoryGroupC));
1400 const FilePath settingsProjectDir = FilePath::fromString(s->value(QLatin1String(projectDirectoryKeyC),
1401 QString()).toString());
1402 if (!settingsProjectDir.isEmpty() && settingsProjectDir.isDir())
1403 d->m_projectsDirectory = settingsProjectDir;
1404 else
1405 d->m_projectsDirectory = FilePath::fromString(PathChooser::homePath());
1406 d->m_useProjectsDirectory
1407 = s->value(QLatin1String(useProjectDirectoryKeyC), kUseProjectsDirectoryDefault).toBool();
1408
1409 s->endGroup();
1410 }
1411
1412 /*!
1413
1414 Returns the initial directory for a new file dialog. If there is a current
1415 document associated with a file, uses that. Or if there is a default location
1416 for new files, uses that. Otherwise, uses the last visited directory.
1417
1418 \sa setFileDialogLastVisitedDirectory(), setDefaultLocationForNewFiles()
1419 */
1420
fileDialogInitialDirectory()1421 QString DocumentManager::fileDialogInitialDirectory()
1422 {
1423 IDocument *doc = EditorManager::currentDocument();
1424 if (doc && !doc->isTemporary() && !doc->filePath().isEmpty())
1425 return doc->filePath().absolutePath().path();
1426 if (!d->m_defaultLocationForNewFiles.isEmpty())
1427 return d->m_defaultLocationForNewFiles;
1428 return d->m_lastVisitedDirectory;
1429 }
1430
1431 /*!
1432
1433 Returns the default location for new files.
1434
1435 \sa fileDialogInitialDirectory()
1436 */
defaultLocationForNewFiles()1437 QString DocumentManager::defaultLocationForNewFiles()
1438 {
1439 return d->m_defaultLocationForNewFiles;
1440 }
1441
1442 /*!
1443 Sets the default \a location for new files.
1444 */
setDefaultLocationForNewFiles(const QString & location)1445 void DocumentManager::setDefaultLocationForNewFiles(const QString &location)
1446 {
1447 d->m_defaultLocationForNewFiles = location;
1448 }
1449
1450 /*!
1451
1452 Returns the directory for projects. Defaults to HOME.
1453
1454 \sa setProjectsDirectory(), setUseProjectsDirectory()
1455 */
1456
projectsDirectory()1457 FilePath DocumentManager::projectsDirectory()
1458 {
1459 return d->m_projectsDirectory;
1460 }
1461
1462 /*!
1463
1464 Sets the \a directory for projects.
1465
1466 \sa projectsDirectory(), useProjectsDirectory()
1467 */
1468
setProjectsDirectory(const FilePath & directory)1469 void DocumentManager::setProjectsDirectory(const FilePath &directory)
1470 {
1471 if (d->m_projectsDirectory != directory) {
1472 d->m_projectsDirectory = directory;
1473 emit m_instance->projectsDirectoryChanged(d->m_projectsDirectory);
1474 }
1475 }
1476
1477 /*!
1478
1479 Returns whether the directory for projects is to be used or whether the user
1480 chose to use the current directory.
1481
1482 \sa setProjectsDirectory(), setUseProjectsDirectory()
1483 */
1484
useProjectsDirectory()1485 bool DocumentManager::useProjectsDirectory()
1486 {
1487 return d->m_useProjectsDirectory;
1488 }
1489
1490 /*!
1491
1492 Sets whether the directory for projects is to be used to
1493 \a useProjectsDirectory.
1494
1495 \sa projectsDirectory(), useProjectsDirectory()
1496 */
1497
setUseProjectsDirectory(bool useProjectsDirectory)1498 void DocumentManager::setUseProjectsDirectory(bool useProjectsDirectory)
1499 {
1500 d->m_useProjectsDirectory = useProjectsDirectory;
1501 }
1502
1503 /*!
1504
1505 Returns the last visited directory of a file dialog.
1506
1507 \sa setFileDialogLastVisitedDirectory(), fileDialogInitialDirectory()
1508
1509 */
1510
fileDialogLastVisitedDirectory()1511 QString DocumentManager::fileDialogLastVisitedDirectory()
1512 {
1513 return d->m_lastVisitedDirectory;
1514 }
1515
1516 /*!
1517
1518 Sets the last visited \a directory of a file dialog that will be remembered
1519 for the next one.
1520
1521 \sa fileDialogLastVisitedDirectory(), fileDialogInitialDirectory()
1522
1523 */
1524
setFileDialogLastVisitedDirectory(const QString & directory)1525 void DocumentManager::setFileDialogLastVisitedDirectory(const QString &directory)
1526 {
1527 d->m_lastVisitedDirectory = directory;
1528 }
1529
notifyFilesChangedInternally(const Utils::FilePaths & filePaths)1530 void DocumentManager::notifyFilesChangedInternally(const Utils::FilePaths &filePaths)
1531 {
1532 emit m_instance->filesChangedInternally(filePaths);
1533 }
1534
registerSaveAllAction()1535 void DocumentManager::registerSaveAllAction()
1536 {
1537 d->registerSaveAllAction();
1538 }
1539
1540 // -------------- FileChangeBlocker
1541
1542 /*!
1543 \class Core::FileChangeBlocker
1544 \inheaderfile coreplugin/documentmanager.h
1545 \inmodule QtCreator
1546
1547 \brief The FileChangeBlocker class blocks all change notifications to all
1548 IDocument objects that match the given filename.
1549
1550 Additionally, the class unblocks in the destructor. To also reload the
1551 IDocument object in the destructor, set modifiedReload() to \c true.
1552 */
1553
FileChangeBlocker(const FilePath & filePath)1554 FileChangeBlocker::FileChangeBlocker(const FilePath &filePath)
1555 : m_filePath(filePath)
1556 {
1557 DocumentManager::expectFileChange(filePath);
1558 }
1559
~FileChangeBlocker()1560 FileChangeBlocker::~FileChangeBlocker()
1561 {
1562 DocumentManager::unexpectFileChange(m_filePath);
1563 }
1564
1565 } // namespace Core
1566
1567 #include "documentmanager.moc"
1568