documentmanager.cpp 57 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3 4
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
con's avatar
con committed
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
con's avatar
con committed
7
**
hjk's avatar
hjk committed
8 9 10 11
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
12 13 14
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
15
**
16 17 18 19 20 21 22
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
con's avatar
con committed
23
**
hjk's avatar
hjk committed
24
****************************************************************************/
hjk's avatar
hjk committed
25

26
#include "documentmanager.h"
hjk's avatar
hjk committed
27

28
#include "icore.h"
29
#include "idocument.h"
30
#include "idocumentfactory.h"
31
#include "coreconstants.h"
con's avatar
con committed
32

33
#include <coreplugin/diffservice.h>
34 35 36
#include <coreplugin/dialogs/readonlyfilesdialog.h>
#include <coreplugin/dialogs/saveitemsdialog.h>
#include <coreplugin/editormanager/editormanager.h>
37
#include <coreplugin/editormanager/editormanager_p.h>
38
#include <coreplugin/editormanager/editorview.h>
39 40 41
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/editormanager/ieditorfactory.h>
#include <coreplugin/editormanager/iexternaleditor.h>
42

43 44
#include <extensionsystem/pluginmanager.h>

45
#include <utils/fileutils.h>
46
#include <utils/hostosinfo.h>
Eike Ziller's avatar
Eike Ziller committed
47
#include <utils/mimetypes/mimedatabase.h>
hjk's avatar
hjk committed
48
#include <utils/qtcassert.h>
49
#include <utils/pathchooser.h>
50
#include <utils/reloadpromptutils.h>
hjk's avatar
hjk committed
51

52
#include <QStringList>
53 54 55 56 57
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFileSystemWatcher>
58
#include <QLoggingCategory>
59 60 61 62 63 64 65
#include <QSettings>
#include <QTimer>
#include <QAction>
#include <QFileDialog>
#include <QMainWindow>
#include <QMenu>
#include <QMessageBox>
con's avatar
con committed
66

67 68
Q_LOGGING_CATEGORY(log, "qtc.core.documentmanager")

con's avatar
con committed
69
/*!
70
  \class Core::DocumentManager
con's avatar
con committed
71
  \mainclass
72
  \inheaderfile documentmanager.h
73
  \brief The DocumentManager class manages a set of IDocument objects.
con's avatar
con committed
74

Leena Miettinen's avatar
Leena Miettinen committed
75 76 77 78
  The DocumentManager service monitors a set of IDocument objects. Plugins
  should register files they work with at the service. The files the IDocument
  objects point to will be monitored at filesystem level. If a file changes,
  the status of the IDocument object
con's avatar
con committed
79 80 81
  will be adjusted accordingly. Furthermore, on application exit the user will
  be asked to save all modified files.

82
  Different IDocument objects in the set can point to the same file in the
Leena Miettinen's avatar
Leena Miettinen committed
83 84
  filesystem. The monitoring for an IDocument can be blocked by
  \c blockFileChange(), and enabled again by \c unblockFileChange().
con's avatar
con committed
85

Leena Miettinen's avatar
Leena Miettinen committed
86
  The functions \c expectFileChange() and \c unexpectFileChange() mark a file change
87
  as expected. On expected file changes all IDocument objects are notified to reload
88 89
  themselves.

90
  The DocumentManager service also provides two convenience functions for saving
Leena Miettinen's avatar
Leena Miettinen committed
91
  files: \c saveModifiedFiles() and \c saveModifiedFilesSilently(). Both take a list
con's avatar
con committed
92 93 94
  of FileInterfaces as an argument, and return the list of files which were
  _not_ saved.

Leena Miettinen's avatar
Leena Miettinen committed
95 96 97
  The service also manages the list of recent files to be shown to the user.

  \sa addToRecentFiles(), recentFiles()
con's avatar
con committed
98 99
 */

100 101 102
static const char settingsGroupC[] = "RecentFiles";
static const char filesKeyC[] = "Files";
static const char editorsKeyC[] = "EditorIds";
103

104 105 106
static const char directoryGroupC[] = "Directories";
static const char projectDirectoryKeyC[] = "Projects";
static const char useProjectDirectoryKeyC[] = "UseProjectsDirectory";
107
static const char buildDirectoryKeyC[] = "BuildDirectory.Template";
108

109 110
using namespace Utils;

111
namespace Core {
112 113 114

static void readSettings();

115 116 117 118 119
static bool saveModifiedFilesHelper(const QList<IDocument *> &documents,
                                    const QString &message,
                                    bool *cancelled, bool silently,
                                    const QString &alwaysSaveMessage,
                                    bool *alwaysSave, QList<IDocument *> *failedToSave);
120

121 122
namespace Internal {

123
struct FileStateItem
124 125 126 127 128
{
    QDateTime modified;
    QFile::Permissions permissions;
};

129 130
struct FileState
{
131
    QString watchedFilePath;
132
    QMap<IDocument *, FileStateItem> lastUpdatedState;
133 134 135
    FileStateItem expected;
};

dt's avatar
dt committed
136

137
class DocumentManagerPrivate : public QObject
138
{
139
    Q_OBJECT
Orgad Shaneh's avatar
Orgad Shaneh committed
140
public:
141
    DocumentManagerPrivate();
142 143
    QFileSystemWatcher *fileWatcher();
    QFileSystemWatcher *linkWatcher();
144

145 146 147
    void checkOnNextFocusChange();
    void onApplicationFocusChange();

148 149
    QMap<QString, FileState> m_states; // filePathKey -> FileState
    QSet<QString> m_changedFiles; // watched file paths collected from file watcher notifications
150
    QList<IDocument *> m_documentsWithoutWatch;
151 152
    QMap<IDocument *, QStringList> m_documentsWithWatch; // document -> list of filePathKeys
    QSet<QString> m_expectedFileNames; // set of file names without normalization
153

154
    QList<DocumentManager::RecentFile> m_recentFiles;
155 156
    static const int m_maxRecentFiles = 7;

157 158 159
    QFileSystemWatcher *m_fileWatcher = nullptr; // Delayed creation.
    QFileSystemWatcher *m_linkWatcher = nullptr; // Delayed creation (only UNIX/if a link is seen).
    bool m_blockActivated = false;
160
    bool m_checkOnFocusChange = false;
161
    QString m_lastVisitedDirectory = QDir::currentPath();
162
    QString m_defaultLocationForNewFiles;
163
    FileName m_projectsDirectory;
164
    bool m_useProjectsDirectory = true;
165
    QString m_buildDirectory;
Leena Miettinen's avatar
Leena Miettinen committed
166
    // When we are calling into an IDocument
dt's avatar
dt committed
167 168 169
    // we don't want to receive a changed()
    // signal
    // That makes the code easier
170
    IDocument *m_blockedIDocument = nullptr;
171 172
};

173
static DocumentManager *m_instance;
174
static DocumentManagerPrivate *d;
175

176
QFileSystemWatcher *DocumentManagerPrivate::fileWatcher()
177 178
{
    if (!m_fileWatcher) {
179
        m_fileWatcher= new QFileSystemWatcher(m_instance);
Orgad Shaneh's avatar
Orgad Shaneh committed
180 181
        QObject::connect(m_fileWatcher, &QFileSystemWatcher::fileChanged,
                         m_instance, &DocumentManager::changedFile);
182
    }
183
    return m_fileWatcher;
184 185
}

186
QFileSystemWatcher *DocumentManagerPrivate::linkWatcher()
187
{
188
    if (HostOsInfo::isAnyUnixHost()) {
189 190 191
        if (!m_linkWatcher) {
            m_linkWatcher = new QFileSystemWatcher(m_instance);
            m_linkWatcher->setObjectName(QLatin1String("_qt_autotest_force_engine_poller"));
Orgad Shaneh's avatar
Orgad Shaneh committed
192 193
            QObject::connect(m_linkWatcher, &QFileSystemWatcher::fileChanged,
                             m_instance, &DocumentManager::changedFile);
194 195
        }
        return m_linkWatcher;
196
    }
197

198
    return fileWatcher();
199 200
}

201 202 203 204 205 206 207 208 209 210 211 212 213
void DocumentManagerPrivate::checkOnNextFocusChange()
{
    m_checkOnFocusChange = true;
}

void DocumentManagerPrivate::onApplicationFocusChange()
{
    if (!m_checkOnFocusChange)
        return;
    m_checkOnFocusChange = false;
    m_instance->checkForReload();
}

214
DocumentManagerPrivate::DocumentManagerPrivate()
con's avatar
con committed
215
{
216
    connect(qApp, &QApplication::focusChanged, this, &DocumentManagerPrivate::onApplicationFocusChange);
217 218 219
}

} // namespace Internal
220 221 222
} // namespace Core

namespace Core {
223

224 225
using namespace Internal;

226 227
DocumentManager::DocumentManager(QObject *parent)
  : QObject(parent)
228
{
229
    d = new DocumentManagerPrivate;
230
    m_instance = this;
231
    qApp->installEventFilter(this);
con's avatar
con committed
232

233
    readSettings();
234 235

    if (d->m_useProjectsDirectory)
236
        setFileDialogLastVisitedDirectory(d->m_projectsDirectory.toString());
con's avatar
con committed
237 238
}

239
DocumentManager::~DocumentManager()
240 241 242 243
{
    delete d;
}

244
DocumentManager *DocumentManager::instance()
245
{
246 247 248
    return m_instance;
}

249
/* only called from addFileInfo(IDocument *) */
250 251
static void addFileInfo(IDocument *document, const QString &filePath,
                        const QString &filePathKey, bool isLink)
252 253
{
    FileStateItem state;
254 255 256
    if (!filePath.isEmpty()) {
        qCDebug(log) << "adding document for" << filePath << "(" << filePathKey << ")";
        const QFileInfo fi(filePath);
257 258
        state.modified = fi.lastModified();
        state.permissions = fi.permissions();
259
        // Add state if we don't have already
260 261 262 263 264
        if (!d->m_states.contains(filePathKey)) {
            FileState state;
            state.watchedFilePath = filePath;
            d->m_states.insert(filePathKey, state);
        }
265 266 267 268 269 270
        // Add or update watcher on file path
        // This is also used to update the watcher in case of saved (==replaced) files or
        // update link targets, even if there are multiple documents registered for it
        const QString watchedFilePath = d->m_states.value(filePathKey).watchedFilePath;
        qCDebug(log) << "adding (" << (isLink ? "link" : "full") << ") watch for"
                     << watchedFilePath;
271
        QFileSystemWatcher *watcher = nullptr;
272 273 274 275 276
        if (isLink)
            watcher = d->linkWatcher();
        else
            watcher = d->fileWatcher();
        watcher->addPath(watchedFilePath);
277

278
        d->m_states[filePathKey].lastUpdatedState.insert(document, state);
279
    }
280
    d->m_documentsWithWatch[document].append(filePathKey); // inserts a new QStringList if not already there
281 282
}

283
/* Adds the IDocument's file and possibly it's final link target to both m_states
284 285 286
   (if it's file name is not empty), and the m_filesWithWatch list,
   and adds a file watcher for each if not already done.
   (The added file names are guaranteed to be absolute and cleaned.) */
287
static void addFileInfo(IDocument *document)
288
{
289 290 291 292 293 294 295 296 297 298 299 300 301
    const QString documentFilePath = document->filePath().toString();
    const QString filePath = DocumentManager::cleanAbsoluteFilePath(
                documentFilePath, DocumentManager::KeepLinks);
    const QString filePathKey = DocumentManager::filePathKey(
                documentFilePath, DocumentManager::KeepLinks);
    const QString resolvedFilePath = DocumentManager::cleanAbsoluteFilePath(
                documentFilePath, DocumentManager::ResolveLinks);
    const QString resolvedFilePathKey = DocumentManager::filePathKey(
                documentFilePath, DocumentManager::ResolveLinks);
    const bool isLink = filePath != resolvedFilePath;
    addFileInfo(document, filePath, filePathKey, isLink);
    if (isLink)
        addFileInfo(document, resolvedFilePath, resolvedFilePathKey, false);
302 303
}

con's avatar
con committed
304
/*!
305
    Adds a list of IDocument's to the collection. If \a addWatcher is true (the default),
con's avatar
con committed
306 307
    the files are added to a file system watcher that notifies the file manager
    about file changes.
con's avatar
con committed
308
*/
309
void DocumentManager::addDocuments(const QList<IDocument *> &documents, bool addWatcher)
con's avatar
con committed
310
{
dt's avatar
dt committed
311 312 313
    if (!addWatcher) {
        // We keep those in a separate list

314 315
        foreach (IDocument *document, documents) {
            if (document && !d->m_documentsWithoutWatch.contains(document)) {
Orgad Shaneh's avatar
Orgad Shaneh committed
316 317
                connect(document, &QObject::destroyed,
                        m_instance, &DocumentManager::documentDestroyed);
318 319
                connect(document, &IDocument::filePathChanged,
                        m_instance, &DocumentManager::filePathChanged);
320
                d->m_documentsWithoutWatch.append(document);
321 322
            }
        }
con's avatar
con committed
323
        return;
dt's avatar
dt committed
324 325
    }

326 327
    foreach (IDocument *document, documents) {
        if (document && !d->m_documentsWithWatch.contains(document)) {
Orgad Shaneh's avatar
Orgad Shaneh committed
328 329 330 331
            connect(document, &IDocument::changed, m_instance, &DocumentManager::checkForNewFileName);
            connect(document, &QObject::destroyed, m_instance, &DocumentManager::documentDestroyed);
            connect(document, &IDocument::filePathChanged,
                    m_instance, &DocumentManager::filePathChanged);
332
            addFileInfo(document);
333
        }
con's avatar
con committed
334 335 336
    }
}

337

338 339
/* Removes all occurrences of the IDocument from m_filesWithWatch and m_states.
   If that results in a file no longer being referenced by any IDocument, this
340 341
   also removes the file watcher.
*/
342
static void removeFileInfo(IDocument *document)
343
{
344
    if (!d->m_documentsWithWatch.contains(document))
345
        return;
346
    foreach (const QString &fileName, d->m_documentsWithWatch.value(document)) {
347 348
        if (!d->m_states.contains(fileName))
            continue;
349
        qCDebug(log) << "removing document (" << fileName << ")";
350
        d->m_states[fileName].lastUpdatedState.remove(document);
351
        if (d->m_states.value(fileName).lastUpdatedState.isEmpty()) {
352 353 354 355 356 357 358 359 360
            const QString &watchedFilePath = d->m_states.value(fileName).watchedFilePath;
            if (d->m_fileWatcher && d->m_fileWatcher->files().contains(watchedFilePath)) {
                qCDebug(log) << "removing watch for" << watchedFilePath;
                d->m_fileWatcher->removePath(watchedFilePath);
            }
            if (d->m_linkWatcher && d->m_linkWatcher->files().contains(watchedFilePath)) {
                qCDebug(log) << "removing watch for" << watchedFilePath;
                d->m_linkWatcher->removePath(watchedFilePath);
            }
361
            d->m_states.remove(fileName);
362
        }
363
    }
364
    d->m_documentsWithWatch.remove(document);
365 366
}

dt's avatar
dt committed
367 368
/// Dumps the state of the file manager's map
/// For debugging purposes
369 370
/*
static void dump()
dt's avatar
dt committed
371
{
372
    qDebug() << "======== dumping state map";
373
    QMap<QString, FileState>::const_iterator it, end;
dt's avatar
dt committed
374 375 376 377
    it = d->m_states.constBegin();
    end = d->m_states.constEnd();
    for (; it != end; ++it) {
        qDebug() << it.key();
378
        qDebug() << "   expected:" << it.value().expected.modified;
dt's avatar
dt committed
379

380
        QMap<IDocument *, FileStateItem>::const_iterator jt, jend;
dt's avatar
dt committed
381 382 383
        jt = it.value().lastUpdatedState.constBegin();
        jend = it.value().lastUpdatedState.constEnd();
        for (; jt != jend; ++jt) {
384
            qDebug() << "  " << jt.key()->fileName() << jt.value().modified;
dt's avatar
dt committed
385 386
        }
    }
387
    qDebug() << "------- dumping files with watch list";
388
    foreach (IDocument *key, d->m_filesWithWatch.keys()) {
389 390 391
        qDebug() << key->fileName() << d->m_filesWithWatch.value(key);
    }
    qDebug() << "------- dumping watch list";
392 393
    if (d->m_fileWatcher)
        qDebug() << d->m_fileWatcher->files();
394
    qDebug() << "------- dumping link watch list";
395 396
    if (d->m_linkWatcher)
        qDebug() << d->m_linkWatcher->files();
dt's avatar
dt committed
397
}
398
*/
dt's avatar
dt committed
399

400
/*!
Leena Miettinen's avatar
Leena Miettinen committed
401
    Tells the file manager that a file has been renamed on disk from within \QC.
402

Leena Miettinen's avatar
Leena Miettinen committed
403 404
    Needs to be called right after the actual renaming on disk (that is, before
    the file system
405
    watcher can report the event during the next event loop run). \a from needs to be an absolute file path.
406
    This will notify all IDocument objects pointing to that file of the rename
Leena Miettinen's avatar
Leena Miettinen committed
407
    by calling \c IDocument::rename(), and update the cached time and permission
408 409 410
    information to avoid annoying the user with "file has been removed"
    popups.
*/
411
void DocumentManager::renamedFile(const QString &from, const QString &to)
dt's avatar
dt committed
412
{
413
    const QString &fromKey = filePathKey(from, KeepLinks);
dt's avatar
dt committed
414

415 416 417
    // gather the list of IDocuments
    QList<IDocument *> documentsToRename;
    QMapIterator<IDocument *, QStringList> it(d->m_documentsWithWatch);
418 419
    while (it.hasNext()) {
        it.next();
420
        if (it.value().contains(fromKey))
421
            documentsToRename.append(it.key());
422 423
    }

424 425 426 427
    // rename the IDocuments
    foreach (IDocument *document, documentsToRename) {
        d->m_blockedIDocument = document;
        removeFileInfo(document);
428
        document->setFilePath(FileName::fromString(to));
429
        addFileInfo(document);
430
        d->m_blockedIDocument = nullptr;
431
    }
432
    emit m_instance->allDocumentsRenamed(from, to);
433
}
434

435
void DocumentManager::filePathChanged(const FileName &oldName, const FileName &newName)
436 437 438 439 440
{
    IDocument *doc = qobject_cast<IDocument *>(sender());
    QTC_ASSERT(doc, return);
    if (doc == d->m_blockedIDocument)
        return;
441
    emit m_instance->documentRenamed(doc, oldName.toString(), newName.toString());
442 443
}

con's avatar
con committed
444
/*!
Leena Miettinen's avatar
Leena Miettinen committed
445 446
    Adds an IDocument object to the collection. If \a addWatcher is \c true
    (the default),
con's avatar
con committed
447 448
    the file is added to a file system watcher that notifies the file manager
    about file changes.
con's avatar
con committed
449
*/
450
void DocumentManager::addDocument(IDocument *document, bool addWatcher)
con's avatar
con committed
451
{
452
    addDocuments(QList<IDocument *>() << document, addWatcher);
con's avatar
con committed
453 454
}

455
void DocumentManager::documentDestroyed(QObject *obj)
con's avatar
con committed
456
{
457
    IDocument *document = static_cast<IDocument*>(obj);
dt's avatar
dt committed
458
    // Check the special unwatched first:
459 460
    if (!d->m_documentsWithoutWatch.removeOne(document))
        removeFileInfo(document);
con's avatar
con committed
461 462 463
}

/*!
Leena Miettinen's avatar
Leena Miettinen committed
464
    Removes an IDocument object from the collection.
con's avatar
con committed
465

Leena Miettinen's avatar
Leena Miettinen committed
466 467
    Returns \c true if the file specified by \a document had the \a addWatcher
    argument to \a addDocument() set.
con's avatar
con committed
468
*/
469
bool DocumentManager::removeDocument(IDocument *document)
con's avatar
con committed
470
{
471
    QTC_ASSERT(document, return false);
con's avatar
con committed
472

473
    bool addWatcher = false;
dt's avatar
dt committed
474
    // Special casing unwatched files
475
    if (!d->m_documentsWithoutWatch.removeOne(document)) {
476
        addWatcher = true;
477
        removeFileInfo(document);
Orgad Shaneh's avatar
Orgad Shaneh committed
478
        disconnect(document, &IDocument::changed, m_instance, &DocumentManager::checkForNewFileName);
dt's avatar
dt committed
479
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
480
    disconnect(document, &QObject::destroyed, m_instance, &DocumentManager::documentDestroyed);
481
    return addWatcher;
con's avatar
con committed
482 483
}

484
/* Slot reacting on IDocument::changed. We need to check if the signal was sent
485
   because the file was saved under different name. */
486
void DocumentManager::checkForNewFileName()
con's avatar
con committed
487
{
488 489
    IDocument *document = qobject_cast<IDocument *>(sender());
    // We modified the IDocument
dt's avatar
dt committed
490
    // Trust the other code to also update the m_states map
491
    if (document == d->m_blockedIDocument)
dt's avatar
dt committed
492
        return;
493 494
    QTC_ASSERT(document, return);
    QTC_ASSERT(d->m_documentsWithWatch.contains(document), return);
495

496
    // Maybe the name has changed or file has been deleted and created again ...
497
    // This also updates the state to the on disk state
498 499
    removeFileInfo(document);
    addFileInfo(document);
con's avatar
con committed
500 501
}

502
/*!
503 504
    Returns a guaranteed cleaned absolute file path for \a filePath in portable form.
    Resolves symlinks if \a resolveMode is ResolveLinks.
505
*/
506
QString DocumentManager::cleanAbsoluteFilePath(const QString &filePath, ResolveMode resolveMode)
con's avatar
con committed
507
{
508 509 510 511 512
    QFileInfo fi(QDir::fromNativeSeparators(filePath));
    if (fi.exists() && resolveMode == ResolveLinks) {
        // if the filePath is no link, we want this method to return the same for both ResolveModes
        // so wrap with absoluteFilePath because that forces drive letters upper case
        return QFileInfo(fi.canonicalFilePath()).absoluteFilePath();
513
    }
514 515 516 517 518 519 520 521 522 523 524 525
    return QDir::cleanPath(fi.absoluteFilePath());
}

/*!
    Returns a representation of \a filePath that can be used as a key for maps.
    (A cleaned absolute file path in portable form, that is all lowercase
    if the file system is case insensitive (in the host OS settings).)
    Resolves symlinks if \a resolveMode is ResolveLinks.
*/
QString DocumentManager::filePathKey(const QString &filePath, ResolveMode resolveMode)
{
    QString s = cleanAbsoluteFilePath(filePath, resolveMode);
526
    if (HostOsInfo::fileNameCaseSensitivity() == Qt::CaseInsensitive)
527
        s = s.toLower();
528
    return s;
con's avatar
con committed
529 530 531
}

/*!
Leena Miettinen's avatar
Leena Miettinen committed
532
    Returns the list of IDocuments that have been modified.
con's avatar
con committed
533
*/
534
QList<IDocument *> DocumentManager::modifiedDocuments()
con's avatar
con committed
535
{
536
    QList<IDocument *> modified;
con's avatar
con committed
537

538 539 540
    const auto docEnd = d->m_documentsWithWatch.keyEnd();
    for (auto docIt = d->m_documentsWithWatch.keyBegin(); docIt != docEnd; ++docIt) {
        IDocument *document = *docIt;
541 542
        if (document->isModified())
            modified << document;
con's avatar
con committed
543
    }
544

545 546 547
    foreach (IDocument *document, d->m_documentsWithoutWatch) {
        if (document->isModified())
            modified << document;
dt's avatar
dt committed
548 549
    }

550
    return modified;
con's avatar
con committed
551 552
}

553
/*!
Leena Miettinen's avatar
Leena Miettinen committed
554
    Any subsequent change to \a fileName is treated as an expected file change.
555

556
    \see DocumentManager::unexpectFileChange(const QString &fileName)
557
*/
558
void DocumentManager::expectFileChange(const QString &fileName)
con's avatar
con committed
559
{
560 561 562
    if (fileName.isEmpty())
        return;
    d->m_expectedFileNames.insert(fileName);
563 564
}

565
/* only called from unblock and unexpect file change functions */
566
static void updateExpectedState(const QString &filePathKey)
567
{
568
    if (filePathKey.isEmpty())
569
        return;
570 571 572 573
    if (d->m_states.contains(filePathKey)) {
        QFileInfo fi(d->m_states.value(filePathKey).watchedFilePath);
        d->m_states[filePathKey].expected.modified = fi.lastModified();
        d->m_states[filePathKey].expected.permissions = fi.permissions();
574 575 576
    }
}

577
/*!
Leena Miettinen's avatar
Leena Miettinen committed
578
    Any changes to \a fileName are unexpected again.
579

580
    \see DocumentManager::expectFileChange(const QString &fileName)
581
*/
582
void DocumentManager::unexpectFileChange(const QString &fileName)
583 584 585 586 587 588
{
    // We are updating the expected time of the file
    // And in changedFile we'll check if the modification time
    // is the same as the saved one here
    // If so then it's a expected change

589 590
    if (fileName.isEmpty())
        return;
591
    d->m_expectedFileNames.remove(fileName);
592 593 594 595 596
    const QString cleanAbsFilePath = cleanAbsoluteFilePath(fileName, KeepLinks);
    updateExpectedState(filePathKey(fileName, KeepLinks));
    const QString resolvedCleanAbsFilePath = cleanAbsoluteFilePath(fileName, ResolveLinks);
    if (cleanAbsFilePath != resolvedCleanAbsFilePath)
        updateExpectedState(filePathKey(fileName, ResolveLinks));
597 598
}

599 600 601 602
static bool saveModifiedFilesHelper(const QList<IDocument *> &documents,
                                    const QString &message, bool *cancelled, bool silently,
                                    const QString &alwaysSaveMessage, bool *alwaysSave,
                                    QList<IDocument *> *failedToSave)
con's avatar
con committed
603 604 605 606
{
    if (cancelled)
        (*cancelled) = false;

607 608 609
    QList<IDocument *> notSaved;
    QMap<IDocument *, QString> modifiedDocumentsMap;
    QList<IDocument *> modifiedDocuments;
con's avatar
con committed
610

611
    foreach (IDocument *document, documents) {
612
        if (document && document->isModified()) {
613
            QString name = document->filePath().toString();
con's avatar
con committed
614
            if (name.isEmpty())
615
                name = document->fallbackSaveAsFileName();
con's avatar
con committed
616

617
            // There can be several IDocuments pointing to the same file
618
            // Prefer one that is not readonly
619
            // (even though it *should* not happen that the IDocuments are inconsistent with readonly)
620
            if (!modifiedDocumentsMap.key(name, nullptr) || !document->isFileReadOnly())
621
                modifiedDocumentsMap.insert(document, name);
con's avatar
con committed
622 623
        }
    }
624 625 626
    modifiedDocuments = modifiedDocumentsMap.keys();
    if (!modifiedDocuments.isEmpty()) {
        QList<IDocument *> documentsToSave;
con's avatar
con committed
627
        if (silently) {
628
            documentsToSave = modifiedDocuments;
con's avatar
con committed
629
        } else {
630
            SaveItemsDialog dia(ICore::dialogParent(), modifiedDocuments);
con's avatar
con committed
631 632
            if (!message.isEmpty())
                dia.setMessage(message);
633 634
            if (!alwaysSaveMessage.isNull())
                dia.setAlwaysSaveMessage(alwaysSaveMessage);
con's avatar
con committed
635 636 637
            if (dia.exec() != QDialog::Accepted) {
                if (cancelled)
                    (*cancelled) = true;
638
                if (alwaysSave)
639 640 641
                    (*alwaysSave) = dia.alwaysSaveChecked();
                if (failedToSave)
                    (*failedToSave) = modifiedDocuments;
642 643 644 645 646
                const QStringList filesToDiff = dia.filesToDiff();
                if (!filesToDiff.isEmpty()) {
                    if (auto diffService = ExtensionSystem::PluginManager::getObject<DiffService>())
                        diffService->diffModifiedFiles(filesToDiff);
                }
647
                return false;
con's avatar
con committed
648
            }
649 650
            if (alwaysSave)
                *alwaysSave = dia.alwaysSaveChecked();
651
            documentsToSave = dia.itemsToSave();
con's avatar
con committed
652
        }
653 654 655 656 657 658 659
        // Check for files without write permissions.
        QList<IDocument *> roDocuments;
        foreach (IDocument *document, documentsToSave) {
            if (document->isFileReadOnly())
                roDocuments << document;
        }
        if (!roDocuments.isEmpty()) {
660
            ReadOnlyFilesDialog roDialog(roDocuments, ICore::dialogParent());
661
            roDialog.setShowFailWarning(true, DocumentManager::tr(
662 663
                                            "Could not save the files.",
                                            "error message"));
hjk's avatar
hjk committed
664
            if (roDialog.exec() == ReadOnlyFilesDialog::RO_Cancel) {
665 666
                if (cancelled)
                    (*cancelled) = true;
667 668 669
                if (failedToSave)
                    (*failedToSave) = modifiedDocuments;
                return false;
670 671
            }
        }
672
        foreach (IDocument *document, documentsToSave) {
673
            if (!EditorManagerPrivate::saveDocument(document)) {
674 675
                if (cancelled)
                    *cancelled = true;
676
                notSaved.append(document);
con's avatar
con committed
677 678 679
            }
        }
    }
680 681 682
    if (failedToSave)
        (*failedToSave) = notSaved;
    return notSaved.isEmpty();
con's avatar
con committed
683 684
}

685
bool DocumentManager::saveDocument(IDocument *document, const QString &fileName, bool *isReadOnly)
686
{
687
    bool ret = true;
688
    QString effName = fileName.isEmpty() ? document->filePath().toString() : fileName;
689 690
    expectFileChange(effName); // This only matters to other IDocuments which refer to this file
    bool addWatcher = removeDocument(document); // So that our own IDocument gets no notification at all
691

692
    QString errorString;
693
    if (!document->save(&errorString, fileName, false)) {
694 695 696
        if (isReadOnly) {
            QFile ofi(effName);
            // Check whether the existing file is writable
697
            if (!ofi.open(QIODevice::ReadWrite) && ofi.open(QIODevice::ReadOnly)) {
698
                *isReadOnly = true;
699
                goto out;
700 701 702
            }
            *isReadOnly = false;
        }
703
        QMessageBox::critical(ICore::dialogParent(), tr("File Error"),
704
                              tr("Error while saving file: %1").arg(errorString));
705 706
      out:
        ret = false;
707
    }
708

709
    addDocument(document, addWatcher);
710 711
    unexpectFileChange(effName);
    return ret;
712 713
}

714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740
template<typename FactoryType>
QSet<QString> filterStrings()
{
    QSet<QString> filters;
    for (FactoryType *factory : ExtensionSystem::PluginManager::getObjects<FactoryType>()) {
        for (const QString &mt : factory->mimeTypes()) {
            const QString filter = mimeTypeForName(mt).filterString();
            if (!filter.isEmpty())
                filters.insert(filter);
        }
    }
    return filters;
}

QString DocumentManager::allDocumentFactoryFiltersString(QString *allFilesFilter = 0)
{
    const QSet<QString> uniqueFilters = filterStrings<IDocumentFactory>()
                                        + filterStrings<IEditorFactory>();
    QStringList filters = uniqueFilters.toList();
    filters.sort();
    const QString allFiles = Utils::allFilesFilterString();
    if (allFilesFilter)
        *allFilesFilter = allFiles;
    filters.prepend(allFiles);
    return filters.join(QLatin1String(";;"));
}

741
QString DocumentManager::getSaveFileName(const QString &title, const QString &pathIn,
742
                                     const QString &filter, QString *selectedFilter)
con's avatar
con committed
743
{
744
    const QString &path = pathIn.isEmpty() ? fileDialogInitialDirectory() : pathIn;
con's avatar
con committed
745 746 747 748
    QString fileName;
    bool repeat;
    do {
        repeat = false;
749
        fileName = QFileDialog::getSaveFileName(
750
            ICore::dialogParent(), title, path, filter, selectedFilter, QFileDialog::DontConfirmOverwrite);
751 752 753 754 755
        if (!fileName.isEmpty()) {
            // If the selected filter is All Files (*) we leave the name exactly as the user
            // specified. Otherwise the suffix must be one available in the selected filter. If
            // the name already ends with such suffix nothing needs to be done. But if not, the
            // first one from the filter is appended.
756
            if (selectedFilter && *selectedFilter != Utils::allFilesFilterString()) {
757
                // Mime database creates filter strings like this: Anything here (*.foo *.bar)
758
                QRegExp regExp(QLatin1String(".*\\s+\\((.*)\\)$"));
759 760
                const int index = regExp.lastIndexIn(*selectedFilter);
                if (index != -1) {
761
                    bool suffixOk = false;
762 763 764
                    QString caption = regExp.cap(1);
                    caption.remove(QLatin1Char('*'));
                    const QVector<QStringRef> suffixes = caption.splitRef(QLatin1Char(' '));
765
                    foreach (const QStringRef &suffix, suffixes)
766 767 768 769 770
                        if (fileName.endsWith(suffix)) {
                            suffixOk = true;
                            break;
                        }
                    if (!suffixOk && !suffixes.isEmpty())
771
                        fileName.append(suffixes.at(0).toString());
772 773
                }
            }
con's avatar
con committed
774
            if (QFile::exists(fileName)) {
775
                if (QMessageBox::warning(ICore::dialogParent(), tr("Overwrite?"),
776
                    tr("An item named \"%1\" already exists at this location. "
777
                       "Do you want to overwrite it?").arg(QDir::toNativeSeparators(fileName)),
778
                    QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
con's avatar
con committed
779
                    repeat = true;
780
                }
con's avatar
con committed
781 782 783
            }
        }
    } while (repeat);
784 785
    if (!fileName.isEmpty())
        setFileDialogLastVisitedDirectory(QFileInfo(fileName).absolutePath());
con's avatar
con committed
786 787 788
    return fileName;
}

789
QString DocumentManager::getSaveFileNameWithExtension(const QString &title, const QString &pathIn,
790 791 792 793 794 795
                                                  const QString &filter)
{
    QString selected = filter;
    return getSaveFileName(title, pathIn, filter, &selected);
}

con's avatar
con committed
796
/*!
Leena Miettinen's avatar
Leena Miettinen committed
797
    Asks the user for a new file name (\gui {Save File As}) for \a document.
con's avatar
con committed
798
*/
799 800 801
QString DocumentManager::getSaveAsFileName(const IDocument *document)
{
    QTC_ASSERT(document, return QString());
802
    const QString filter = allDocumentFactoryFiltersString();
803 804 805 806
    const QString filePath = document->filePath().toString();
    QString selectedFilter;
    QString fileDialogPath = filePath;
    if (!filePath.isEmpty()) {
807
        selectedFilter = Utils::mimeTypeForFile(filePath).filterString();
808 809 810
    } else {
        const QString suggestedName = document->fallbackSaveAsFileName();
        if (!suggestedName.isEmpty()) {
811
            const QList<MimeType> types = Utils::mimeTypesForFileName(suggestedName);
812 813 814
            if (!types.isEmpty())
                selectedFilter = types.first().filterString();
        }
815
        const QString defaultPath = document->fallbackSaveAsPath();
con's avatar
con committed
816
        if (!defaultPath.isEmpty())
817 818 819
            fileDialogPath = defaultPath + (suggestedName.isEmpty()
                    ? QString()
                    : '/' + suggestedName);
con's avatar
con committed
820
    }
821
    if (selectedFilter.isEmpty())
822
        selectedFilter = Utils::mimeTypeForName(document->mimeType()).filterString();
con's avatar
con committed
823

824 825 826 827
    return getSaveFileName(tr("Save File As"),
                           fileDialogPath,
                           filter,
                           &selectedFilter);
con's avatar
con committed
828 829
}

830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856
/*!
    Silently saves all documents and will return true if all modified documents were saved
    successfully.

    This method will try to avoid showing dialogs to the user, but can do so anyway (e.g. if
    a file is not writeable).

    \a Canceled will be set if the user canceled any of the dialogs that he interacted with.
    \a FailedToClose will contain a list of documents that could not be saved if passed into the
    method.
*/
bool DocumentManager::saveAllModifiedDocumentsSilently(bool *canceled,
                                                       QList<IDocument *> *failedToClose)
{
    return saveModifiedDocumentsSilently(modifiedDocuments(), canceled, failedToClose);
}

/*!
    Silently saves \a documents and will return true if all of them were saved successfully.

    This method will try to avoid showing dialogs to the user, but can do so anyway (e.g. if
    a file is not writeable).

    \a Canceled will be set if the user canceled any of the dialogs that he interacted with.
    \a FailedToClose will contain a list of documents that could not be saved if passed into the
    method.
*/
857 858
bool DocumentManager::saveModifiedDocumentsSilently(const QList<IDocument *> &documents,
                                                    bool *canceled,
859 860
                                                    QList<IDocument *> *failedToClose)
{
861 862 863 864 865 866 867
    return saveModifiedFilesHelper(documents,
                                   QString(),
                                   canceled,
                                   true,
                                   QString(),
                                   nullptr,
                                   failedToClose);
868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948
}

/*!
    Silently saves a \a document and will return true if it was saved successfully.

    This method will try to avoid showing dialogs to the user, but can do so anyway (e.g. if
    a file is not writeable).

    \a Canceled will be set if the user canceled any of the dialogs that he interacted with.
    \a FailedToClose will contain a list of documents that could not be saved if passed into the
    method.
*/
bool DocumentManager::saveModifiedDocumentSilently(IDocument *document, bool *canceled,
                                                   QList<IDocument *> *failedToClose)
{
    return saveModifiedDocumentsSilently(QList<IDocument *>() << document, canceled, failedToClose);
}

/*!
    Presents a dialog with all modified documents to the user and will ask him which of these
    should be saved.

    This method may show additional dialogs to the user, e.g. if a file is not writeable).

    The dialog text can be set using \a message. \a Canceled will be set if the user canceled any
    of the dialogs that he interacted with (the method will also return false in this case).
    The \a alwaysSaveMessage will show an additional checkbox asking in the dialog. The state of
    this checkbox will be written into \a alwaysSave if set.
    \a FailedToClose will contain a list of documents that could not be saved if passed into the
    method.
*/
bool DocumentManager::saveAllModifiedDocuments(const QString &message, bool *canceled,
                                               const QString &alwaysSaveMessage, bool *alwaysSave,
                                               QList<IDocument *> *failedToClose)
{
    return saveModifiedDocuments(modifiedDocuments(), message, canceled,
                                 alwaysSaveMessage, alwaysSave, failedToClose);
}

/*!
    Presents a dialog with \a documents to the user and will ask him which of these should be saved.

    This method may show additional dialogs to the user, e.g. if a file is not writeable).

    The dialog text can be set using \a message. \a Canceled will be set if the user canceled any
    of the dialogs that he interacted with (the method will also return false in this case).
    The \a alwaysSaveMessage will show an additional checkbox asking in the dialog. The state of
    this checkbox will be written into \a alwaysSave if set.
    \a FailedToClose will contain a list of documents that could not be saved if passed into the
    method.
*/
bool DocumentManager::saveModifiedDocuments(const QList<IDocument *> &documents,
                                            const QString &message, bool *canceled,
                                            const QString &alwaysSaveMessage, bool *alwaysSave,
                                            QList<IDocument *> *failedToClose)
{
    return saveModifiedFilesHelper(documents, message, canceled, false,
                                   alwaysSaveMessage, alwaysSave, failedToClose);
}

/*!
    Presents a dialog with the one \a document to the user and will ask him whether he wants it
    saved.

    This method may show additional dialogs to the user, e.g. if the file is not writeable).

    The dialog text can be set using \a message. \a Canceled will be set if the user canceled any
    of the dialogs that he interacted with (the method will also return false in this case).
    The \a alwaysSaveMessage will show an additional checkbox asking in the dialog. The state of
    this checkbox will be written into \a alwaysSave if set.
    \a FailedToClose will contain a list of documents that could not be saved if passed into the
    method.
*/
bool DocumentManager::saveModifiedDocument(IDocument *document, const QString &message, bool *canceled,
                                           const QString &alwaysSaveMessage, bool *alwaysSave,
                                           QList<IDocument *> *failedToClose)
{
    return saveModifiedDocuments(QList<IDocument *>() << document, message, canceled,
                                 alwaysSaveMessage, alwaysSave, failedToClose);
}

949
/*!
con's avatar
con committed
950
    Asks the user for a set of file names to be opened. The \a filters
Leena Miettinen's avatar
Leena Miettinen committed
951 952 953
    and \a selectedFilter arguments are interpreted like in
    \c QFileDialog::getOpenFileNames(). \a pathIn specifies a path to open the
    dialog in if that is not overridden by the user's policy.
954 955
*/

956
QStringList DocumentManager::getOpenFileNames(const QString &filters,
957 958
                                              const QString &pathIn,
                                              QString *selectedFilter)
959
{
960
    const QString &path = pathIn.isEmpty() ? fileDialogInitialDirectory() : pathIn;
961
    const QStringList files = QFileDialog::getOpenFileNames(ICore::dialogParent(),
962 963 964 965 966 967 968 969
                                                      tr("Open File"),
                                                      path, filters,
                                                      selectedFilter);
    if (!files.isEmpty())
        setFileDialogLastVisitedDirectory(QFileInfo(files.front()).absolutePath());
    return files;
}

Eike Ziller's avatar