documentmanager.cpp 51.7 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 "coreconstants.h"
con's avatar
con committed
31

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

41
#include <utils/fileutils.h>
42
#include <utils/hostosinfo.h>
Eike Ziller's avatar
Eike Ziller committed
43
#include <utils/mimetypes/mimedatabase.h>
hjk's avatar
hjk committed
44
#include <utils/qtcassert.h>
45
#include <utils/pathchooser.h>
46
#include <utils/reloadpromptutils.h>
hjk's avatar
hjk committed
47

48
#include <QStringList>
49
50
51
52
53
54
55
56
57
58
59
60
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QSettings>
#include <QTimer>
#include <QAction>
#include <QFileDialog>
#include <QMainWindow>
#include <QMenu>
#include <QMessageBox>
con's avatar
con committed
61
62

/*!
63
  \class Core::DocumentManager
con's avatar
con committed
64
  \mainclass
65
  \inheaderfile documentmanager.h
66
  \brief The DocumentManager class manages a set of IDocument objects.
con's avatar
con committed
67

Leena Miettinen's avatar
Leena Miettinen committed
68
69
70
71
  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
72
73
74
  will be adjusted accordingly. Furthermore, on application exit the user will
  be asked to save all modified files.

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

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

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

Leena Miettinen's avatar
Leena Miettinen committed
88
89
90
  The service also manages the list of recent files to be shown to the user.

  \sa addToRecentFiles(), recentFiles()
con's avatar
con committed
91
92
 */

93
94
95
static const char settingsGroupC[] = "RecentFiles";
static const char filesKeyC[] = "Files";
static const char editorsKeyC[] = "EditorIds";
96

97
98
99
static const char directoryGroupC[] = "Directories";
static const char projectDirectoryKeyC[] = "Projects";
static const char useProjectDirectoryKeyC[] = "UseProjectsDirectory";
100
static const char buildDirectoryKeyC[] = "BuildDirectory.Template";
101

Orgad Shaneh's avatar
Orgad Shaneh committed
102
103
using namespace Utils;

104
namespace Core {
105
106
107

static void readSettings();

108
109
110
111
112
static bool saveModifiedFilesHelper(const QList<IDocument *> &documents,
                                    const QString &message,
                                    bool *cancelled, bool silently,
                                    const QString &alwaysSaveMessage,
                                    bool *alwaysSave, QList<IDocument *> *failedToSave);
113

114
115
namespace Internal {

116
struct FileStateItem
117
118
119
120
121
{
    QDateTime modified;
    QFile::Permissions permissions;
};

122
123
struct FileState
{
124
    QMap<IDocument *, FileStateItem> lastUpdatedState;
125
126
127
    FileStateItem expected;
};

dt's avatar
dt committed
128

129
class DocumentManagerPrivate : public QObject
130
{
131
    Q_OBJECT
Orgad Shaneh's avatar
Orgad Shaneh committed
132
public:
133
    DocumentManagerPrivate();
134
135
    QFileSystemWatcher *fileWatcher();
    QFileSystemWatcher *linkWatcher();
136

137
138
139
    void checkOnNextFocusChange();
    void onApplicationFocusChange();

dt's avatar
dt committed
140
    QMap<QString, FileState> m_states;
141
    QSet<QString> m_changedFiles;
142
143
    QList<IDocument *> m_documentsWithoutWatch;
    QMap<IDocument *, QStringList> m_documentsWithWatch;
144
    QSet<QString> m_expectedFileNames;
145

146
    QList<DocumentManager::RecentFile> m_recentFiles;
147
148
    static const int m_maxRecentFiles = 7;

149
    QFileSystemWatcher *m_fileWatcher; // Delayed creation.
150
    QFileSystemWatcher *m_linkWatcher; // Delayed creation (only UNIX/if a link is seen).
151
    bool m_blockActivated;
152
    bool m_checkOnFocusChange = false;
153
    QString m_lastVisitedDirectory;
154
    QString m_defaultLocationForNewFiles;
155
156
    QString m_projectsDirectory;
    bool m_useProjectsDirectory;
157
    QString m_buildDirectory;
Leena Miettinen's avatar
Leena Miettinen committed
158
    // When we are calling into an IDocument
dt's avatar
dt committed
159
160
161
    // we don't want to receive a changed()
    // signal
    // That makes the code easier
162
    IDocument *m_blockedIDocument;
163
164
};

165
static DocumentManager *m_instance;
166
static DocumentManagerPrivate *d;
167

168
QFileSystemWatcher *DocumentManagerPrivate::fileWatcher()
169
170
{
    if (!m_fileWatcher) {
171
        m_fileWatcher= new QFileSystemWatcher(m_instance);
Orgad Shaneh's avatar
Orgad Shaneh committed
172
173
        QObject::connect(m_fileWatcher, &QFileSystemWatcher::fileChanged,
                         m_instance, &DocumentManager::changedFile);
174
    }
175
    return m_fileWatcher;
176
177
}

178
QFileSystemWatcher *DocumentManagerPrivate::linkWatcher()
179
{
Orgad Shaneh's avatar
Orgad Shaneh committed
180
    if (HostOsInfo::isAnyUnixHost()) {
181
182
183
        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
184
185
            QObject::connect(m_linkWatcher, &QFileSystemWatcher::fileChanged,
                             m_instance, &DocumentManager::changedFile);
186
187
        }
        return m_linkWatcher;
188
    }
189

190
    return fileWatcher();
191
192
}

193
194
195
196
197
198
199
200
201
202
203
204
205
void DocumentManagerPrivate::checkOnNextFocusChange()
{
    m_checkOnFocusChange = true;
}

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

206
DocumentManagerPrivate::DocumentManagerPrivate() :
207
208
    m_fileWatcher(0),
    m_linkWatcher(0),
209
210
    m_blockActivated(false),
    m_lastVisitedDirectory(QDir::currentPath()),
Daniel Teske's avatar
Daniel Teske committed
211
    m_useProjectsDirectory(true),
212
    m_blockedIDocument(0)
con's avatar
con committed
213
{
214
    connect(qApp, &QApplication::focusChanged, this, &DocumentManagerPrivate::onApplicationFocusChange);
215
216
217
}

} // namespace Internal
218
219
220
} // namespace Core

namespace Core {
221

222
223
using namespace Internal;

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

231
    readSettings();
232
233
234

    if (d->m_useProjectsDirectory)
        setFileDialogLastVisitedDirectory(d->m_projectsDirectory);
con's avatar
con committed
235
236
}

237
DocumentManager::~DocumentManager()
238
239
240
241
{
    delete d;
}

242
DocumentManager *DocumentManager::instance()
243
{
244
245
246
    return m_instance;
}

247
248
/* only called from addFileInfo(IDocument *) */
static void addFileInfo(const QString &fileName, IDocument *document, bool isLink)
249
250
251
252
253
254
255
{
    FileStateItem state;
    if (!fileName.isEmpty()) {
        const QFileInfo fi(fileName);
        state.modified = fi.lastModified();
        state.permissions = fi.permissions();
        // Add watcher if we don't have that already
256
        if (!d->m_states.contains(fileName))
257
258
259
260
261
262
263
            d->m_states.insert(fileName, FileState());

        QFileSystemWatcher *watcher = 0;
        if (isLink)
            watcher = d->linkWatcher();
        else
            watcher = d->fileWatcher();
264
265
        if (!watcher->files().contains(fileName))
            watcher->addPath(fileName);
266

267
        d->m_states[fileName].lastUpdatedState.insert(document, state);
268
    }
269
    d->m_documentsWithWatch[document].append(fileName); // inserts a new QStringList if not already there
270
271
}

272
/* Adds the IDocument's file and possibly it's final link target to both m_states
273
274
275
   (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.) */
276
static void addFileInfo(IDocument *document)
277
{
278
279
    const QString fixedName = DocumentManager::fixFileName(document->filePath().toString(), DocumentManager::KeepLinks);
    const QString fixedResolvedName = DocumentManager::fixFileName(document->filePath().toString(), DocumentManager::ResolveLinks);
280
    addFileInfo(fixedResolvedName, document, false);
281
    if (fixedName != fixedResolvedName)
282
        addFileInfo(fixedName, document, true);
283
284
}

con's avatar
con committed
285
/*!
286
    Adds a list of IDocument's to the collection. If \a addWatcher is true (the default),
con's avatar
con committed
287
288
    the files are added to a file system watcher that notifies the file manager
    about file changes.
con's avatar
con committed
289
*/
290
void DocumentManager::addDocuments(const QList<IDocument *> &documents, bool addWatcher)
con's avatar
con committed
291
{
dt's avatar
dt committed
292
293
294
    if (!addWatcher) {
        // We keep those in a separate list

295
296
        foreach (IDocument *document, documents) {
            if (document && !d->m_documentsWithoutWatch.contains(document)) {
Orgad Shaneh's avatar
Orgad Shaneh committed
297
298
                connect(document, &QObject::destroyed,
                        m_instance, &DocumentManager::documentDestroyed);
299
300
                connect(document, &IDocument::filePathChanged,
                        m_instance, &DocumentManager::filePathChanged);
301
                d->m_documentsWithoutWatch.append(document);
302
303
            }
        }
con's avatar
con committed
304
        return;
dt's avatar
dt committed
305
306
    }

307
308
    foreach (IDocument *document, documents) {
        if (document && !d->m_documentsWithWatch.contains(document)) {
Orgad Shaneh's avatar
Orgad Shaneh committed
309
310
311
312
            connect(document, &IDocument::changed, m_instance, &DocumentManager::checkForNewFileName);
            connect(document, &QObject::destroyed, m_instance, &DocumentManager::documentDestroyed);
            connect(document, &IDocument::filePathChanged,
                    m_instance, &DocumentManager::filePathChanged);
313
            addFileInfo(document);
314
        }
con's avatar
con committed
315
316
317
    }
}

318

319
320
/* 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
321
322
   also removes the file watcher.
*/
323
static void removeFileInfo(IDocument *document)
324
{
325
    if (!d->m_documentsWithWatch.contains(document))
326
        return;
327
    foreach (const QString &fileName, d->m_documentsWithWatch.value(document)) {
328
329
        if (!d->m_states.contains(fileName))
            continue;
330
        d->m_states[fileName].lastUpdatedState.remove(document);
331
332
333
334
335
336
        if (d->m_states.value(fileName).lastUpdatedState.isEmpty()) {
            if (d->m_fileWatcher && d->m_fileWatcher->files().contains(fileName))
                d->m_fileWatcher->removePath(fileName);
            if (d->m_linkWatcher && d->m_linkWatcher->files().contains(fileName))
                d->m_linkWatcher->removePath(fileName);
            d->m_states.remove(fileName);
337
        }
338
    }
339
    d->m_documentsWithWatch.remove(document);
340
341
}

dt's avatar
dt committed
342
343
/// Dumps the state of the file manager's map
/// For debugging purposes
344
345
/*
static void dump()
dt's avatar
dt committed
346
{
347
    qDebug() << "======== dumping state map";
348
    QMap<QString, FileState>::const_iterator it, end;
dt's avatar
dt committed
349
350
351
352
    it = d->m_states.constBegin();
    end = d->m_states.constEnd();
    for (; it != end; ++it) {
        qDebug() << it.key();
353
        qDebug() << "   expected:" << it.value().expected.modified;
dt's avatar
dt committed
354

355
        QMap<IDocument *, FileStateItem>::const_iterator jt, jend;
dt's avatar
dt committed
356
357
358
        jt = it.value().lastUpdatedState.constBegin();
        jend = it.value().lastUpdatedState.constEnd();
        for (; jt != jend; ++jt) {
359
            qDebug() << "  " << jt.key()->fileName() << jt.value().modified;
dt's avatar
dt committed
360
361
        }
    }
362
    qDebug() << "------- dumping files with watch list";
363
    foreach (IDocument *key, d->m_filesWithWatch.keys()) {
364
365
366
        qDebug() << key->fileName() << d->m_filesWithWatch.value(key);
    }
    qDebug() << "------- dumping watch list";
367
368
    if (d->m_fileWatcher)
        qDebug() << d->m_fileWatcher->files();
369
    qDebug() << "------- dumping link watch list";
370
371
    if (d->m_linkWatcher)
        qDebug() << d->m_linkWatcher->files();
dt's avatar
dt committed
372
}
373
*/
dt's avatar
dt committed
374

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

Leena Miettinen's avatar
Leena Miettinen committed
378
379
    Needs to be called right after the actual renaming on disk (that is, before
    the file system
380
    watcher can report the event during the next event loop run). \a from needs to be an absolute file path.
381
    This will notify all IDocument objects pointing to that file of the rename
Leena Miettinen's avatar
Leena Miettinen committed
382
    by calling \c IDocument::rename(), and update the cached time and permission
383
384
385
    information to avoid annoying the user with "file has been removed"
    popups.
*/
386
void DocumentManager::renamedFile(const QString &from, const QString &to)
dt's avatar
dt committed
387
{
388
    const QString &fixedFrom = fixFileName(from, KeepLinks);
dt's avatar
dt committed
389

390
391
392
    // gather the list of IDocuments
    QList<IDocument *> documentsToRename;
    QMapIterator<IDocument *, QStringList> it(d->m_documentsWithWatch);
393
394
395
    while (it.hasNext()) {
        it.next();
        if (it.value().contains(fixedFrom))
396
            documentsToRename.append(it.key());
397
398
    }

399
400
401
402
    // rename the IDocuments
    foreach (IDocument *document, documentsToRename) {
        d->m_blockedIDocument = document;
        removeFileInfo(document);
Orgad Shaneh's avatar
Orgad Shaneh committed
403
        document->setFilePath(FileName::fromString(to));
404
405
        addFileInfo(document);
        d->m_blockedIDocument = 0;
406
    }
407
    emit m_instance->allDocumentsRenamed(from, to);
408
}
409

Orgad Shaneh's avatar
Orgad Shaneh committed
410
void DocumentManager::filePathChanged(const FileName &oldName, const FileName &newName)
411
412
413
414
415
{
    IDocument *doc = qobject_cast<IDocument *>(sender());
    QTC_ASSERT(doc, return);
    if (doc == d->m_blockedIDocument)
        return;
416
    emit m_instance->documentRenamed(doc, oldName.toString(), newName.toString());
417
418
}

con's avatar
con committed
419
/*!
Leena Miettinen's avatar
Leena Miettinen committed
420
421
    Adds an IDocument object to the collection. If \a addWatcher is \c true
    (the default),
con's avatar
con committed
422
423
    the file is added to a file system watcher that notifies the file manager
    about file changes.
con's avatar
con committed
424
*/
425
void DocumentManager::addDocument(IDocument *document, bool addWatcher)
con's avatar
con committed
426
{
427
    addDocuments(QList<IDocument *>() << document, addWatcher);
con's avatar
con committed
428
429
}

430
void DocumentManager::documentDestroyed(QObject *obj)
con's avatar
con committed
431
{
432
    IDocument *document = static_cast<IDocument*>(obj);
dt's avatar
dt committed
433
    // Check the special unwatched first:
434
435
    if (!d->m_documentsWithoutWatch.removeOne(document))
        removeFileInfo(document);
con's avatar
con committed
436
437
438
}

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

Leena Miettinen's avatar
Leena Miettinen committed
441
442
    Returns \c true if the file specified by \a document had the \a addWatcher
    argument to \a addDocument() set.
con's avatar
con committed
443
*/
444
bool DocumentManager::removeDocument(IDocument *document)
con's avatar
con committed
445
{
446
    QTC_ASSERT(document, return false);
con's avatar
con committed
447

448
    bool addWatcher = false;
dt's avatar
dt committed
449
    // Special casing unwatched files
450
    if (!d->m_documentsWithoutWatch.removeOne(document)) {
451
        addWatcher = true;
452
        removeFileInfo(document);
Orgad Shaneh's avatar
Orgad Shaneh committed
453
        disconnect(document, &IDocument::changed, m_instance, &DocumentManager::checkForNewFileName);
dt's avatar
dt committed
454
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
455
    disconnect(document, &QObject::destroyed, m_instance, &DocumentManager::documentDestroyed);
456
    return addWatcher;
con's avatar
con committed
457
458
}

459
/* Slot reacting on IDocument::changed. We need to check if the signal was sent
460
   because the file was saved under different name. */
461
void DocumentManager::checkForNewFileName()
con's avatar
con committed
462
{
463
464
    IDocument *document = qobject_cast<IDocument *>(sender());
    // We modified the IDocument
dt's avatar
dt committed
465
    // Trust the other code to also update the m_states map
466
    if (document == d->m_blockedIDocument)
dt's avatar
dt committed
467
        return;
468
469
    QTC_ASSERT(document, return);
    QTC_ASSERT(d->m_documentsWithWatch.contains(document), return);
470

471
    // Maybe the name has changed or file has been deleted and created again ...
472
    // This also updates the state to the on disk state
473
474
    removeFileInfo(document);
    addFileInfo(document);
con's avatar
con committed
475
476
}

477
478
479
480
481
/*!
    Returns a guaranteed cleaned path in native form. If the file exists,
    it will either be a cleaned absolute file path (fixmode == KeepLinks), or
    a cleaned canonical file path (fixmode == ResolveLinks).
*/
482
QString DocumentManager::fixFileName(const QString &fileName, FixMode fixmode)
con's avatar
con committed
483
484
{
    QString s = fileName;
485
    QFileInfo fi(s);
486
487
488
489
490
491
492
493
494
    if (fi.exists()) {
        if (fixmode == ResolveLinks)
            s = fi.canonicalFilePath();
        else
            s = QDir::cleanPath(fi.absoluteFilePath());
    } else {
        s = QDir::cleanPath(s);
    }
    s = QDir::toNativeSeparators(s);
Orgad Shaneh's avatar
Orgad Shaneh committed
495
    if (HostOsInfo::fileNameCaseSensitivity() == Qt::CaseInsensitive)
496
        s = s.toLower();
497
    return s;
con's avatar
con committed
498
499
500
}

/*!
Leena Miettinen's avatar
Leena Miettinen committed
501
    Returns the list of IDocuments that have been modified.
con's avatar
con committed
502
*/
503
QList<IDocument *> DocumentManager::modifiedDocuments()
con's avatar
con committed
504
{
505
    QList<IDocument *> modified;
con's avatar
con committed
506

507
508
509
    foreach (IDocument *document, d->m_documentsWithWatch.keys()) {
        if (document->isModified())
            modified << document;
con's avatar
con committed
510
    }
511

512
513
514
    foreach (IDocument *document, d->m_documentsWithoutWatch) {
        if (document->isModified())
            modified << document;
dt's avatar
dt committed
515
516
    }

517
    return modified;
con's avatar
con committed
518
519
}

520
/*!
Leena Miettinen's avatar
Leena Miettinen committed
521
    Any subsequent change to \a fileName is treated as an expected file change.
522

523
    \see DocumentManager::unexpectFileChange(const QString &fileName)
524
*/
525
void DocumentManager::expectFileChange(const QString &fileName)
con's avatar
con committed
526
{
527
528
529
    if (fileName.isEmpty())
        return;
    d->m_expectedFileNames.insert(fileName);
530
531
}

532
/* only called from unblock and unexpect file change functions */
533
534
535
536
537
538
539
540
541
542
543
static void updateExpectedState(const QString &fileName)
{
    if (fileName.isEmpty())
        return;
    if (d->m_states.contains(fileName)) {
        QFileInfo fi(fileName);
        d->m_states[fileName].expected.modified = fi.lastModified();
        d->m_states[fileName].expected.permissions = fi.permissions();
    }
}

544
/*!
Leena Miettinen's avatar
Leena Miettinen committed
545
    Any changes to \a fileName are unexpected again.
546

547
    \see DocumentManager::expectFileChange(const QString &fileName)
548
*/
549
void DocumentManager::unexpectFileChange(const QString &fileName)
550
551
552
553
554
555
{
    // 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

556
557
    if (fileName.isEmpty())
        return;
558
    d->m_expectedFileNames.remove(fileName);
559
560
561
562
563
    const QString fixedName = fixFileName(fileName, KeepLinks);
    updateExpectedState(fixedName);
    const QString fixedResolvedName = fixFileName(fileName, ResolveLinks);
    if (fixedName != fixedResolvedName)
        updateExpectedState(fixedResolvedName);
564
565
}

566
567
568
569
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
570
571
572
573
{
    if (cancelled)
        (*cancelled) = false;

574
575
576
    QList<IDocument *> notSaved;
    QMap<IDocument *, QString> modifiedDocumentsMap;
    QList<IDocument *> modifiedDocuments;
con's avatar
con committed
577

578
    foreach (IDocument *document, documents) {
579
        if (document && document->isModified()) {
580
            QString name = document->filePath().toString();
con's avatar
con committed
581
            if (name.isEmpty())
582
                name = document->fallbackSaveAsFileName();
con's avatar
con committed
583

584
            // There can be several IDocuments pointing to the same file
585
            // Prefer one that is not readonly
586
587
588
            // (even though it *should* not happen that the IDocuments are inconsistent with readonly)
            if (!modifiedDocumentsMap.key(name, 0) || !document->isFileReadOnly())
                modifiedDocumentsMap.insert(document, name);
con's avatar
con committed
589
590
        }
    }
591
592
593
    modifiedDocuments = modifiedDocumentsMap.keys();
    if (!modifiedDocuments.isEmpty()) {
        QList<IDocument *> documentsToSave;
con's avatar
con committed
594
        if (silently) {
595
            documentsToSave = modifiedDocuments;
con's avatar
con committed
596
        } else {
597
            SaveItemsDialog dia(ICore::dialogParent(), modifiedDocuments);
con's avatar
con committed
598
599
            if (!message.isEmpty())
                dia.setMessage(message);
600
601
            if (!alwaysSaveMessage.isNull())
                dia.setAlwaysSaveMessage(alwaysSaveMessage);
con's avatar
con committed
602
603
604
            if (dia.exec() != QDialog::Accepted) {
                if (cancelled)
                    (*cancelled) = true;
605
                if (alwaysSave)
606
607
608
609
                    (*alwaysSave) = dia.alwaysSaveChecked();
                if (failedToSave)
                    (*failedToSave) = modifiedDocuments;
                return false;
con's avatar
con committed
610
            }
611
612
            if (alwaysSave)
                *alwaysSave = dia.alwaysSaveChecked();
613
            documentsToSave = dia.itemsToSave();
con's avatar
con committed
614
        }
615
616
617
618
619
620
621
        // Check for files without write permissions.
        QList<IDocument *> roDocuments;
        foreach (IDocument *document, documentsToSave) {
            if (document->isFileReadOnly())
                roDocuments << document;
        }
        if (!roDocuments.isEmpty()) {
622
            ReadOnlyFilesDialog roDialog(roDocuments, ICore::dialogParent());
623
            roDialog.setShowFailWarning(true, DocumentManager::tr(
624
625
                                            "Could not save the files.",
                                            "error message"));
hjk's avatar
hjk committed
626
            if (roDialog.exec() == ReadOnlyFilesDialog::RO_Cancel) {
627
628
                if (cancelled)
                    (*cancelled) = true;
629
630
631
                if (failedToSave)
                    (*failedToSave) = modifiedDocuments;
                return false;
632
633
            }
        }
634
        foreach (IDocument *document, documentsToSave) {
635
            if (!EditorManagerPrivate::saveDocument(document)) {
636
637
                if (cancelled)
                    *cancelled = true;
638
                notSaved.append(document);
con's avatar
con committed
639
640
641
            }
        }
    }
642
643
644
    if (failedToSave)
        (*failedToSave) = notSaved;
    return notSaved.isEmpty();
con's avatar
con committed
645
646
}

647
bool DocumentManager::saveDocument(IDocument *document, const QString &fileName, bool *isReadOnly)
648
{
649
    bool ret = true;
650
    QString effName = fileName.isEmpty() ? document->filePath().toString() : fileName;
651
652
    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
653

654
    QString errorString;
655
    if (!document->save(&errorString, fileName, false)) {
656
657
658
        if (isReadOnly) {
            QFile ofi(effName);
            // Check whether the existing file is writable
659
            if (!ofi.open(QIODevice::ReadWrite) && ofi.open(QIODevice::ReadOnly)) {
660
                *isReadOnly = true;
661
                goto out;
662
663
664
            }
            *isReadOnly = false;
        }
665
        QMessageBox::critical(ICore::dialogParent(), tr("File Error"),
666
                              tr("Error while saving file: %1").arg(errorString));
667
668
      out:
        ret = false;
669
    }
670

671
    addDocument(document, addWatcher);
672
673
    unexpectFileChange(effName);
    return ret;
674
675
}

676
QString DocumentManager::getSaveFileName(const QString &title, const QString &pathIn,
677
                                     const QString &filter, QString *selectedFilter)
con's avatar
con committed
678
{
679
    const QString &path = pathIn.isEmpty() ? fileDialogInitialDirectory() : pathIn;
con's avatar
con committed
680
681
682
683
    QString fileName;
    bool repeat;
    do {
        repeat = false;
684
        fileName = QFileDialog::getSaveFileName(
685
            ICore::dialogParent(), title, path, filter, selectedFilter, QFileDialog::DontConfirmOverwrite);
686
687
688
689
690
691
692
693
        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.
            if (selectedFilter && *selectedFilter != QCoreApplication::translate(
                    "Core", Constants::ALL_FILES_FILTER)) {
                // Mime database creates filter strings like this: Anything here (*.foo *.bar)
694
                QRegExp regExp(QLatin1String(".*\\s+\\((.*)\\)$"));
695
696
                const int index = regExp.lastIndexIn(*selectedFilter);
                if (index != -1) {
697
                    bool suffixOk = false;
698
                    const QStringList &suffixes = regExp.cap(1).remove(QLatin1Char('*')).split(QLatin1Char(' '));
699
700
701
702
703
704
705
706
707
                    foreach (const QString &suffix, suffixes)
                        if (fileName.endsWith(suffix)) {
                            suffixOk = true;
                            break;
                        }
                    if (!suffixOk && !suffixes.isEmpty())
                        fileName.append(suffixes.at(0));
                }
            }
con's avatar
con committed
708
            if (QFile::exists(fileName)) {
709
                if (QMessageBox::warning(ICore::dialogParent(), tr("Overwrite?"),
710
                    tr("An item named \"%1\" already exists at this location. "
711
                       "Do you want to overwrite it?").arg(QDir::toNativeSeparators(fileName)),
712
                    QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
con's avatar
con committed
713
                    repeat = true;
714
                }
con's avatar
con committed
715
716
717
            }
        }
    } while (repeat);
718
719
    if (!fileName.isEmpty())
        setFileDialogLastVisitedDirectory(QFileInfo(fileName).absolutePath());
con's avatar
con committed
720
721
722
    return fileName;
}

723
QString DocumentManager::getSaveFileNameWithExtension(const QString &title, const QString &pathIn,
724
725
726
727
728
729
                                                  const QString &filter)
{
    QString selected = filter;
    return getSaveFileName(title, pathIn, filter, &selected);
}

con's avatar
con committed
730
/*!
Leena Miettinen's avatar
Leena Miettinen committed
731
    Asks the user for a new file name (\gui {Save File As}) for \a document.
con's avatar
con committed
732
*/
733
QString DocumentManager::getSaveAsFileName(const IDocument *document, const QString &filter, QString *selectedFilter)
con's avatar
con committed
734
{
735
    if (!document)
con's avatar
con committed
736
        return QLatin1String("");
737
    QString absoluteFilePath = document->filePath().toString();
con's avatar
con committed
738
    const QFileInfo fi(absoluteFilePath);
739
740
    QString path;
    QString fileName;
con's avatar
con committed
741
    if (absoluteFilePath.isEmpty()) {
742
743
        fileName = document->fallbackSaveAsFileName();
        const QString defaultPath = document->fallbackSaveAsPath();
con's avatar
con committed
744
745
        if (!defaultPath.isEmpty())
            path = defaultPath;
746
747
748
    } else {
        path = fi.absolutePath();
        fileName = fi.fileName();
con's avatar
con committed
749
    }
750

con's avatar
con committed
751
    QString filterString;
752
    if (filter.isEmpty()) {
Eike Ziller's avatar
Eike Ziller committed
753
754
755
        Utils::MimeDatabase mdb;
        const Utils::MimeType &mt = mdb.mimeTypeForFile(fi);
        if (mt.isValid())
756
757
758
759
            filterString = mt.filterString();
        selectedFilter = &filterString;
    } else {
        filterString = filter;
con's avatar
con committed
760
761
    }

762
    absoluteFilePath = getSaveFileName(tr("Save File As"),
763
        path + QLatin1Char('/') + fileName,
con's avatar
con committed
764
        filterString,
765
        selectedFilter);
con's avatar
con committed
766
767
768
    return absoluteFilePath;
}

769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
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
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
/*!
    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.
*/
bool DocumentManager::saveModifiedDocumentsSilently(const QList<IDocument *> &documents, bool *canceled,
                                                    QList<IDocument *> *failedToClose)
{
    return saveModifiedFilesHelper(documents, QString(), canceled, true, QString(), 0, failedToClose);
}

/*!
    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)