documentmanager.cpp 49.4 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
3
** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
hjk's avatar
hjk committed
4
** Contact: http://www.qt-project.org/legal
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
12
13
14
** 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
** a written agreement between you and Digia.  For licensing terms and
** conditions see http://qt.digia.com/licensing.  For further information
** use the contact form at http://qt.digia.com/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
18
19
20
21
22
23
24
25
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights.  These rights are described in the Digia Qt LGPL Exception
con's avatar
con committed
26
27
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
28
****************************************************************************/
hjk's avatar
hjk committed
29

30
#include "documentmanager.h"
hjk's avatar
hjk committed
31

32
33
#include "editormanager.h"
#include "icore.h"
34
35
36
#include "ieditor.h"
#include "ieditorfactory.h"
#include "iexternaleditor.h"
37
#include "idocument.h"
38
#include "mimedatabase.h"
con's avatar
con committed
39
#include "saveitemsdialog.h"
40
#include "coreconstants.h"
con's avatar
con committed
41

42
43
#include "dialogs/readonlyfilesdialog.h"

44
#include <utils/hostosinfo.h>
hjk's avatar
hjk committed
45
#include <utils/qtcassert.h>
46
#include <utils/pathchooser.h>
47
#include <utils/reloadpromptutils.h>
hjk's avatar
hjk committed
48

49
#include <QStringList>
50
51
52
53
54
55
56
57
58
59
60
61
#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
62
63

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

69
70
71
  The DocumentManager service monitors a set of IDocument's. Plugins should register
  files they work with at the service. The files the IDocument's point to will be
  monitored at filesystem level. If a file changes, the status of the IDocument's
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
76
  Different IDocument objects in the set can point to the same file in the
  filesystem. The monitoring for a IDocument can be blocked by blockFileChange(), and
con's avatar
con committed
77
78
  enabled again by unblockFileChange().

79
  The functions expectFileChange() and 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 methods for saving
con's avatar
con committed
84
85
86
87
88
89
90
91
  files: saveModifiedFiles() and saveModifiedFilesSilently(). Both take a list
  of FileInterfaces as an argument, and return the list of files which were
  _not_ saved.

  The service also manages the list of recent files to be shown to the user
  (see addToRecentFiles() and recentFiles()).
 */

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

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

101
namespace Core {
102
103
104

static void readSettings();

105
static QList<IDocument *> saveModifiedFilesHelper(const QList<IDocument *> &documents,
106
107
108
109
110
                               bool *cancelled, bool silently,
                               const QString &message,
                               const QString &alwaysSaveMessage = QString(),
                               bool *alwaysSave = 0);

111
112
namespace Internal {

113
114
115
116
117
118
119
120
struct OpenWithEntry
{
    OpenWithEntry() : editorFactory(0), externalEditor(0) {}
    IEditorFactory *editorFactory;
    IExternalEditor *externalEditor;
    QString fileName;
};

121
struct FileStateItem
122
123
124
125
126
{
    QDateTime modified;
    QFile::Permissions permissions;
};

127
128
struct FileState
{
129
    QMap<IDocument *, FileStateItem> lastUpdatedState;
130
131
132
    FileStateItem expected;
};

dt's avatar
dt committed
133

134
struct DocumentManagerPrivate
135
{
136
    explicit DocumentManagerPrivate(QMainWindow *mw);
137
138
    QFileSystemWatcher *fileWatcher();
    QFileSystemWatcher *linkWatcher();
139

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
149
150
151
    static const int m_maxRecentFiles = 7;

    QString m_currentFile;

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

166
167
static DocumentManager *m_instance;
static Internal::DocumentManagerPrivate *d;
168

169
QFileSystemWatcher *DocumentManagerPrivate::fileWatcher()
170
171
{
    if (!m_fileWatcher) {
172
        m_fileWatcher= new QFileSystemWatcher(m_instance);
173
174
175
        QObject::connect(m_fileWatcher, SIGNAL(fileChanged(QString)),
                         m_instance, SLOT(changedFile(QString)));
    }
176
    return m_fileWatcher;
177
178
}

179
QFileSystemWatcher *DocumentManagerPrivate::linkWatcher()
180
{
181
182
183
184
185
186
187
188
    if (Utils::HostOsInfo::isAnyUnixHost()) {
        if (!m_linkWatcher) {
            m_linkWatcher = new QFileSystemWatcher(m_instance);
            m_linkWatcher->setObjectName(QLatin1String("_qt_autotest_force_engine_poller"));
            QObject::connect(m_linkWatcher, SIGNAL(fileChanged(QString)),
                             m_instance, SLOT(changedFile(QString)));
        }
        return m_linkWatcher;
189
    }
190

191
    return fileWatcher();
192
193
}

194
DocumentManagerPrivate::DocumentManagerPrivate(QMainWindow *mw) :
con's avatar
con committed
195
    m_mainWindow(mw),
196
197
    m_fileWatcher(0),
    m_linkWatcher(0),
198
199
    m_blockActivated(false),
    m_lastVisitedDirectory(QDir::currentPath()),
200
    m_useProjectsDirectory(Utils::HostOsInfo::isMacHost()), // Creator is in bizarre places when launched via finder.
201
    m_blockedIDocument(0)
con's avatar
con committed
202
{
203
204
205
}

} // namespace Internal
206
207
208
209
210
} // namespace Core

Q_DECLARE_METATYPE(Core::Internal::OpenWithEntry)

namespace Core {
211

212
213
using namespace Internal;

214
DocumentManager::DocumentManager(QMainWindow *mw)
215
  : QObject(mw)
216
{
217
    d = new DocumentManagerPrivate(mw);
218
    m_instance = this;
219
220
    connect(ICore::instance(), SIGNAL(contextChanged(QList<Core::IContext*>,Core::Context)),
        this, SLOT(syncWithEditor(QList<Core::IContext*>)));
221
    qApp->installEventFilter(this);
con's avatar
con committed
222

223
    readSettings();
con's avatar
con committed
224
225
}

226
DocumentManager::~DocumentManager()
227
228
229
230
{
    delete d;
}

231
DocumentManager *DocumentManager::instance()
232
{
233
234
235
    return m_instance;
}

236
237
/* only called from addFileInfo(IDocument *) */
static void addFileInfo(const QString &fileName, IDocument *document, bool isLink)
238
239
240
241
242
243
244
{
    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
245
        if (!d->m_states.contains(fileName))
246
247
248
249
250
251
252
253
254
255
            d->m_states.insert(fileName, FileState());

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

256
        d->m_states[fileName].lastUpdatedState.insert(document, state);
257
    }
258
    d->m_documentsWithWatch[document].append(fileName); // inserts a new QStringList if not already there
259
260
}

261
/* Adds the IDocument's file and possibly it's final link target to both m_states
262
263
264
   (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.) */
265
static void addFileInfo(IDocument *document)
266
{
267
268
269
    const QString fixedName = DocumentManager::fixFileName(document->fileName(), DocumentManager::KeepLinks);
    const QString fixedResolvedName = DocumentManager::fixFileName(document->fileName(), DocumentManager::ResolveLinks);
    addFileInfo(fixedResolvedName, document, false);
270
    if (fixedName != fixedResolvedName)
271
        addFileInfo(fixedName, document, true);
272
273
}

con's avatar
con committed
274
/*!
275
    Adds a list of IDocument's to the collection. If \a addWatcher is true (the default),
con's avatar
con committed
276
277
    the files are added to a file system watcher that notifies the file manager
    about file changes.
con's avatar
con committed
278
*/
279
void DocumentManager::addDocuments(const QList<IDocument *> &documents, bool addWatcher)
con's avatar
con committed
280
{
dt's avatar
dt committed
281
282
283
    if (!addWatcher) {
        // We keep those in a separate list

284
285
        foreach (IDocument *document, documents) {
            if (document && !d->m_documentsWithoutWatch.contains(document)) {
Robert Loehning's avatar
Robert Loehning committed
286
                connect(document, SIGNAL(destroyed(QObject*)), m_instance, SLOT(documentDestroyed(QObject*)));
Robert Loehning's avatar
Robert Loehning committed
287
                connect(document, SIGNAL(fileNameChanged(QString,QString)), m_instance, SLOT(fileNameChanged(QString,QString)));
288
                d->m_documentsWithoutWatch.append(document);
289
290
            }
        }
con's avatar
con committed
291
        return;
dt's avatar
dt committed
292
293
    }

294
295
296
    foreach (IDocument *document, documents) {
        if (document && !d->m_documentsWithWatch.contains(document)) {
            connect(document, SIGNAL(changed()), m_instance, SLOT(checkForNewFileName()));
Robert Loehning's avatar
Robert Loehning committed
297
            connect(document, SIGNAL(destroyed(QObject*)), m_instance, SLOT(documentDestroyed(QObject*)));
Robert Loehning's avatar
Robert Loehning committed
298
            connect(document, SIGNAL(fileNameChanged(QString,QString)), m_instance, SLOT(fileNameChanged(QString,QString)));
299
            addFileInfo(document);
300
        }
con's avatar
con committed
301
302
303
    }
}

304

305
306
/* 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
307
308
   also removes the file watcher.
*/
309
static void removeFileInfo(IDocument *document)
310
{
311
    if (!d->m_documentsWithWatch.contains(document))
312
        return;
313
    foreach (const QString &fileName, d->m_documentsWithWatch.value(document)) {
314
315
        if (!d->m_states.contains(fileName))
            continue;
316
        d->m_states[fileName].lastUpdatedState.remove(document);
317
318
319
320
321
322
        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);
323
        }
324
    }
325
    d->m_documentsWithWatch.remove(document);
326
327
}

dt's avatar
dt committed
328
329
/// Dumps the state of the file manager's map
/// For debugging purposes
330
331
/*
static void dump()
dt's avatar
dt committed
332
{
333
    qDebug() << "======== dumping state map";
334
    QMap<QString, FileState>::const_iterator it, end;
dt's avatar
dt committed
335
336
337
338
    it = d->m_states.constBegin();
    end = d->m_states.constEnd();
    for (; it != end; ++it) {
        qDebug() << it.key();
339
        qDebug() << "   expected:" << it.value().expected.modified;
dt's avatar
dt committed
340

341
        QMap<IDocument *, FileStateItem>::const_iterator jt, jend;
dt's avatar
dt committed
342
343
344
        jt = it.value().lastUpdatedState.constBegin();
        jend = it.value().lastUpdatedState.constEnd();
        for (; jt != jend; ++jt) {
345
            qDebug() << "  " << jt.key()->fileName() << jt.value().modified;
dt's avatar
dt committed
346
347
        }
    }
348
    qDebug() << "------- dumping files with watch list";
349
    foreach (IDocument *key, d->m_filesWithWatch.keys()) {
350
351
352
        qDebug() << key->fileName() << d->m_filesWithWatch.value(key);
    }
    qDebug() << "------- dumping watch list";
353
354
    if (d->m_fileWatcher)
        qDebug() << d->m_fileWatcher->files();
355
    qDebug() << "------- dumping link watch list";
356
357
    if (d->m_linkWatcher)
        qDebug() << d->m_linkWatcher->files();
dt's avatar
dt committed
358
}
359
*/
dt's avatar
dt committed
360

361
362
363
364
365
/*!
    \brief Tells the file manager that a file has been renamed on disk from within Qt Creator.

    Needs to be called right after the actual renaming on disk (i.e. before the file system
    watcher can report the event during the next event loop run). \a from needs to be an absolute file path.
366
367
    This will notify all IDocument objects pointing to that file of the rename
    by calling IDocument::rename, and update the cached time and permission
368
369
370
    information to avoid annoying the user with "file has been removed"
    popups.
*/
371
void DocumentManager::renamedFile(const QString &from, const QString &to)
dt's avatar
dt committed
372
{
373
    const QString &fixedFrom = fixFileName(from, KeepLinks);
dt's avatar
dt committed
374

375
376
377
    // gather the list of IDocuments
    QList<IDocument *> documentsToRename;
    QMapIterator<IDocument *, QStringList> it(d->m_documentsWithWatch);
378
379
380
    while (it.hasNext()) {
        it.next();
        if (it.value().contains(fixedFrom))
381
            documentsToRename.append(it.key());
382
383
    }

384
385
386
387
388
389
390
    // rename the IDocuments
    foreach (IDocument *document, documentsToRename) {
        d->m_blockedIDocument = document;
        removeFileInfo(document);
        document->rename(to);
        addFileInfo(document);
        d->m_blockedIDocument = 0;
391
    }
392
    emit m_instance->allDocumentsRenamed(from, to);
393
}
394
395
396
397
398
399
400
401
402
403

void DocumentManager::fileNameChanged(const QString &oldName, const QString &newName)
{
    IDocument *doc = qobject_cast<IDocument *>(sender());
    QTC_ASSERT(doc, return);
    if (doc == d->m_blockedIDocument)
        return;
    emit m_instance->documentRenamed(doc, oldName, newName);
}

con's avatar
con committed
404
/*!
405
    Adds a IDocument object to the collection. If \a addWatcher is true (the default),
con's avatar
con committed
406
407
    the file is added to a file system watcher that notifies the file manager
    about file changes.
con's avatar
con committed
408
*/
409
void DocumentManager::addDocument(IDocument *document, bool addWatcher)
con's avatar
con committed
410
{
411
    addDocuments(QList<IDocument *>() << document, addWatcher);
con's avatar
con committed
412
413
}

414
void DocumentManager::documentDestroyed(QObject *obj)
con's avatar
con committed
415
{
416
    IDocument *document = static_cast<IDocument*>(obj);
dt's avatar
dt committed
417
    // Check the special unwatched first:
418
419
    if (!d->m_documentsWithoutWatch.removeOne(document))
        removeFileInfo(document);
con's avatar
con committed
420
421
422
}

/*!
423
    Removes a IDocument object from the collection.
con's avatar
con committed
424

425
    Returns true if the file specified by \a document had the addWatcher argument to addDocument() set.
con's avatar
con committed
426
*/
427
bool DocumentManager::removeDocument(IDocument *document)
con's avatar
con committed
428
{
429
    QTC_ASSERT(document, return false);
con's avatar
con committed
430

431
    bool addWatcher = false;
dt's avatar
dt committed
432
    // Special casing unwatched files
433
    if (!d->m_documentsWithoutWatch.removeOne(document)) {
434
        addWatcher = true;
435
436
        removeFileInfo(document);
        disconnect(document, SIGNAL(changed()), m_instance, SLOT(checkForNewFileName()));
dt's avatar
dt committed
437
    }
Robert Loehning's avatar
Robert Loehning committed
438
    disconnect(document, SIGNAL(destroyed(QObject*)), m_instance, SLOT(documentDestroyed(QObject*)));
439
    return addWatcher;
con's avatar
con committed
440
441
}

442
/* Slot reacting on IDocument::changed. We need to check if the signal was sent
443
   because the file was saved under different name. */
444
void DocumentManager::checkForNewFileName()
con's avatar
con committed
445
{
446
447
    IDocument *document = qobject_cast<IDocument *>(sender());
    // We modified the IDocument
dt's avatar
dt committed
448
    // Trust the other code to also update the m_states map
449
    if (document == d->m_blockedIDocument)
dt's avatar
dt committed
450
        return;
451
452
    QTC_ASSERT(document, return);
    QTC_ASSERT(d->m_documentsWithWatch.contains(document), return);
453

454
    // Maybe the name has changed or file has been deleted and created again ...
455
    // This also updates the state to the on disk state
456
457
    removeFileInfo(document);
    addFileInfo(document);
con's avatar
con committed
458
459
}

460
461
462
463
464
/*!
    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).
*/
465
QString DocumentManager::fixFileName(const QString &fileName, FixMode fixmode)
con's avatar
con committed
466
467
{
    QString s = fileName;
468
    QFileInfo fi(s);
469
470
471
472
473
474
475
476
477
    if (fi.exists()) {
        if (fixmode == ResolveLinks)
            s = fi.canonicalFilePath();
        else
            s = QDir::cleanPath(fi.absoluteFilePath());
    } else {
        s = QDir::cleanPath(s);
    }
    s = QDir::toNativeSeparators(s);
478
479
    if (Utils::HostOsInfo::isWindowsHost())
        s = s.toLower();
480
    return s;
con's avatar
con committed
481
482
483
}

/*!
484
    Returns the list of IDocument's that have been modified.
con's avatar
con committed
485
*/
486
QList<IDocument *> DocumentManager::modifiedDocuments()
con's avatar
con committed
487
{
488
    QList<IDocument *> modified;
con's avatar
con committed
489

490
491
492
    foreach (IDocument *document, d->m_documentsWithWatch.keys()) {
        if (document->isModified())
            modified << document;
con's avatar
con committed
493
    }
494

495
496
497
    foreach (IDocument *document, d->m_documentsWithoutWatch) {
        if (document->isModified())
            modified << document;
dt's avatar
dt committed
498
499
    }

500
    return modified;
con's avatar
con committed
501
502
}

503
504
505
/*!
    Any subsequent change to \a fileName is treated as a expected file change.

506
    \see DocumentManager::unexpectFileChange(const QString &fileName)
507
*/
508
void DocumentManager::expectFileChange(const QString &fileName)
con's avatar
con committed
509
{
510
511
512
    if (fileName.isEmpty())
        return;
    d->m_expectedFileNames.insert(fileName);
513
514
}

515
516
517
518
519
520
521
522
523
524
525
526
/* only called from unblock and unexpect file change methods */
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();
    }
}

527
528
529
/*!
    Any change to \a fileName are unexpected again.

530
    \see DocumentManager::expectFileChange(const QString &fileName)
531
*/
532
void DocumentManager::unexpectFileChange(const QString &fileName)
533
534
535
536
537
538
{
    // 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

539
540
    if (fileName.isEmpty())
        return;
541
    d->m_expectedFileNames.remove(fileName);
542
543
544
545
546
    const QString fixedName = fixFileName(fileName, KeepLinks);
    updateExpectedState(fixedName);
    const QString fixedResolvedName = fixFileName(fileName, ResolveLinks);
    if (fixedName != fixedResolvedName)
        updateExpectedState(fixedResolvedName);
547
548
}

con's avatar
con committed
549

Orgad Shaneh's avatar
Orgad Shaneh committed
550
/*!
551
    Tries to save the files listed in \a documents. The \a cancelled argument is set to true
552
553
554
    if the user cancelled the dialog. Returns the files that could not be saved. If the files
    listed in documents have no write permissions an additional dialog will be prompted to
    query the user for these permissions.
con's avatar
con committed
555
*/
556
QList<IDocument *> DocumentManager::saveModifiedDocumentsSilently(const QList<IDocument *> &documents, bool *cancelled)
con's avatar
con committed
557
{
558
    return saveModifiedFilesHelper(documents, cancelled, true, QString());
con's avatar
con committed
559
560
561
}

/*!
562
    Asks the user whether to save the files listed in \a documents .
con's avatar
con committed
563
564
565
566
567
    Opens a dialog with the given \a message, and a additional
    text that should be used to ask if the user wants to enabled automatic save
    of modified files (in this context).
    The \a cancelled argument is set to true if the user cancelled the dialog,
    \a alwaysSave is set to match the selection of the user, if files should
568
569
    always automatically be saved. If the files listed in documents have no write
    permissions an additional dialog will be prompted to query the user for these permissions.
con's avatar
con committed
570
    Returns the files that have not been saved.
con's avatar
con committed
571
*/
572
QList<IDocument *> DocumentManager::saveModifiedDocuments(const QList<IDocument *> &documents,
573
574
575
                                              bool *cancelled, const QString &message,
                                              const QString &alwaysSaveMessage,
                                              bool *alwaysSave)
con's avatar
con committed
576
{
577
    return saveModifiedFilesHelper(documents, cancelled, false, message, alwaysSaveMessage, alwaysSave);
con's avatar
con committed
578
579
}

580
static QList<IDocument *> saveModifiedFilesHelper(const QList<IDocument *> &documents,
581
582
583
584
585
                                              bool *cancelled,
                                              bool silently,
                                              const QString &message,
                                              const QString &alwaysSaveMessage,
                                              bool *alwaysSave)
con's avatar
con committed
586
587
588
589
{
    if (cancelled)
        (*cancelled) = false;

590
591
592
    QList<IDocument *> notSaved;
    QMap<IDocument *, QString> modifiedDocumentsMap;
    QList<IDocument *> modifiedDocuments;
con's avatar
con committed
593

594
595
596
    foreach (IDocument *document, documents) {
        if (document->isModified()) {
            QString name = document->fileName();
con's avatar
con committed
597
            if (name.isEmpty())
598
                name = document->suggestedFileName();
con's avatar
con committed
599

600
            // There can be several IDocuments pointing to the same file
601
            // Prefer one that is not readonly
602
603
604
            // (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
605
606
        }
    }
607
608
609
    modifiedDocuments = modifiedDocumentsMap.keys();
    if (!modifiedDocuments.isEmpty()) {
        QList<IDocument *> documentsToSave;
con's avatar
con committed
610
        if (silently) {
611
            documentsToSave = modifiedDocuments;
con's avatar
con committed
612
        } else {
613
            SaveItemsDialog dia(d->m_mainWindow, modifiedDocuments);
con's avatar
con committed
614
615
            if (!message.isEmpty())
                dia.setMessage(message);
616
617
            if (!alwaysSaveMessage.isNull())
                dia.setAlwaysSaveMessage(alwaysSaveMessage);
con's avatar
con committed
618
619
620
            if (dia.exec() != QDialog::Accepted) {
                if (cancelled)
                    (*cancelled) = true;
621
622
                if (alwaysSave)
                    *alwaysSave = dia.alwaysSaveChecked();
623
                notSaved = modifiedDocuments;
con's avatar
con committed
624
625
                return notSaved;
            }
626
627
            if (alwaysSave)
                *alwaysSave = dia.alwaysSaveChecked();
628
            documentsToSave = dia.itemsToSave();
con's avatar
con committed
629
        }
630
631
632
633
634
635
636
637
        // Check for files without write permissions.
        QList<IDocument *> roDocuments;
        foreach (IDocument *document, documentsToSave) {
            if (document->isFileReadOnly())
                roDocuments << document;
        }
        if (!roDocuments.isEmpty()) {
            Core::Internal::ReadOnlyFilesDialog roDialog(roDocuments, d->m_mainWindow);
638
            roDialog.setShowFailWarning(true, DocumentManager::tr(
639
640
641
642
643
644
645
646
647
                                            "Could not save the files.",
                                            "error message"));
            if (roDialog.exec() == Core::Internal::ReadOnlyFilesDialog::RO_Cancel) {
                if (cancelled)
                    (*cancelled) = true;
                notSaved = modifiedDocuments;
                return notSaved;
            }
        }
648
649
        foreach (IDocument *document, documentsToSave) {
            if (!EditorManager::instance()->saveDocument(document)) {
650
651
                if (cancelled)
                    *cancelled = true;
652
                notSaved.append(document);
con's avatar
con committed
653
654
655
656
657
658
            }
        }
    }
    return notSaved;
}

659
bool DocumentManager::saveDocument(IDocument *document, const QString &fileName, bool *isReadOnly)
660
{
661
    bool ret = true;
662
663
664
    QString effName = fileName.isEmpty() ? document->fileName() : fileName;
    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
665

666
    QString errorString;
667
    if (!document->save(&errorString, fileName, false)) {
668
669
670
        if (isReadOnly) {
            QFile ofi(effName);
            // Check whether the existing file is writable
671
            if (!ofi.open(QIODevice::ReadWrite) && ofi.open(QIODevice::ReadOnly)) {
672
                *isReadOnly = true;
673
                goto out;
674
675
676
            }
            *isReadOnly = false;
        }
677
678
        QMessageBox::critical(d->m_mainWindow, tr("File Error"),
                              tr("Error while saving file: %1").arg(errorString));
679
680
      out:
        ret = false;
681
    }
682

683
    addDocument(document, addWatcher);
684
685
    unexpectFileChange(effName);
    return ret;
686
687
}

688
QString DocumentManager::getSaveFileName(const QString &title, const QString &pathIn,
689
                                     const QString &filter, QString *selectedFilter)
con's avatar
con committed
690
{
691
    const QString &path = pathIn.isEmpty() ? fileDialogInitialDirectory() : pathIn;
con's avatar
con committed
692
693
694
695
    QString fileName;
    bool repeat;
    do {
        repeat = false;
696
697
698
699
700
701
702
703
704
705
        fileName = QFileDialog::getSaveFileName(
            d->m_mainWindow, title, path, filter, selectedFilter, QFileDialog::DontConfirmOverwrite);
        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)
706
                QRegExp regExp(QLatin1String(".*\\s+\\((.*)\\)$"));
707
708
709
                const int index = regExp.lastIndexIn(*selectedFilter);
                bool suffixOk = false;
                if (index != -1) {
710
                    const QStringList &suffixes = regExp.cap(1).remove(QLatin1Char('*')).split(QLatin1Char(' '));
711
712
713
714
715
716
717
718
719
                    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
720
            if (QFile::exists(fileName)) {
721
                if (QMessageBox::warning(d->m_mainWindow, tr("Overwrite?"),
722
723
724
                    tr("An item named '%1' already exists at this location. "
                       "Do you want to overwrite it?").arg(fileName),
                    QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) {
con's avatar
con committed
725
                    repeat = true;
726
                }
con's avatar
con committed
727
728
729
            }
        }
    } while (repeat);
730
731
    if (!fileName.isEmpty())
        setFileDialogLastVisitedDirectory(QFileInfo(fileName).absolutePath());
con's avatar
con committed
732
733
734
    return fileName;
}

735
QString DocumentManager::getSaveFileNameWithExtension(const QString &title, const QString &pathIn,
736
737
738
739
740
741
                                                  const QString &filter)
{
    QString selected = filter;
    return getSaveFileName(title, pathIn, filter, &selected);
}

con's avatar
con committed
742
/*!
743
    Asks the user for a new file name (Save File As) for /arg document.
con's avatar
con committed
744
*/
745
QString DocumentManager::getSaveAsFileName(const IDocument *document, const QString &filter, QString *selectedFilter)
con's avatar
con committed
746
{
747
    if (!document)
con's avatar
con committed
748
        return QLatin1String("");
749
    QString absoluteFilePath = document->fileName();
con's avatar
con committed
750
751
752
753
    const QFileInfo fi(absoluteFilePath);
    QString fileName = fi.fileName();
    QString path = fi.absolutePath();
    if (absoluteFilePath.isEmpty()) {
754
755
        fileName = document->suggestedFileName();
        const QString defaultPath = document->defaultPath();
con's avatar
con committed
756
757
758
        if (!defaultPath.isEmpty())
            path = defaultPath;
    }
759

con's avatar
con committed
760
    QString filterString;
761
    if (filter.isEmpty()) {
hjk's avatar
hjk committed
762
        if (const MimeType &mt = Core::ICore::mimeDatabase()->findByFile(fi))
763
764
765
766
            filterString = mt.filterString();
        selectedFilter = &filterString;
    } else {
        filterString = filter;
con's avatar
con committed
767
768
    }

769
    absoluteFilePath = getSaveFileName(tr("Save File As"),
con's avatar
con committed
770
771
        path + QDir::separator() + fileName,
        filterString,
772
        selectedFilter);
con's avatar
con committed
773
774
775
    return absoluteFilePath;
}

776
/*!
con's avatar
con committed
777
778
779
780
    Asks the user for a set of file names to be opened. The \a filters
    and \a selectedFilter parameters is interpreted like in
    QFileDialog::getOpenFileNames(), \a pathIn specifies a path to open the dialog
    in, if that is not overridden by the users policy.
781
782
*/

783
QStringList DocumentManager::getOpenFileNames(const QString &filters,
784
785
786
                                          const QString pathIn,
                                          QString *selectedFilter)
{
787
788
789
790
    QString path = pathIn;
    if (path.isEmpty()) {
        if (!d->m_currentFile.isEmpty())
            path = QFileInfo(d->m_currentFile).absoluteFilePath();
791
792
        if (path.isEmpty() && useProjectsDirectory())
            path = projectsDirectory();
793
    }
794
795
796
797
798
799
800
801
802
    const QStringList files = QFileDialog::getOpenFileNames(d->m_mainWindow,
                                                      tr("Open File"),
                                                      path, filters,
                                                      selectedFilter);
    if (!files.isEmpty())
        setFileDialogLastVisitedDirectory(QFileInfo(files.front()).absolutePath());
    return files;
}

803
void DocumentManager::changedFile(const QString &fileName)
con's avatar
con committed
804
{
805
    const bool wasempty = d->m_changedFiles.isEmpty();
806

807
808
    if (d->m_states.contains(fileName))
        d->m_changedFiles.insert(fileName);
809

810
    if (wasempty && !d->m_changedFiles.isEmpty())
811
        QTimer::singleShot(200, this, SLOT(checkForReload()));
con's avatar
con committed
812
813
}

814
void DocumentManager::checkForReload()
con's avatar
con committed
815
{
816
817
    if (d->m_changedFiles.isEmpty())
        return;
818
    if (!QApplication::activeWindow() || QApplication::activeModalWidget())
819
        return;
con's avatar
con committed
820

821
822
823
824
825
    if (d->m_blockActivated)
        return;

    d->m_blockActivated = true;

826
    IDocument::ReloadSetting defaultBehavior = EditorManager::instance()->reloadSetting();
827
    Utils::ReloadPromptAnswer previousAnswer = Utils::ReloadCurrent;
828

829
    QList<IEditor*> editorsToClose;
830
    QMap<IDocument*, QString> documentsToSave;
831
832

    // collect file information
833
    QMap<QString, FileStateItem> currentStates;
834
835
    QMap<QString, IDocument::ChangeType> changeTypes;
    QSet<IDocument *> changedIDocuments;
836
    foreach (const QString &fileName, d->m_changedFiles) {
837
        IDocument::ChangeType type = IDocument::TypeContents;
838
        FileStateItem state;
839
        QFileInfo fi(fileName);
840
        if (!fi.exists()) {
841
            type = IDocument::TypeRemoved;
842
        } else {
843
844
            state.modified = fi.lastModified();
            state.permissions = fi.permissions();
845
        }
846
847
        currentStates.insert(fileName, state);
        changeTypes.insert(fileName, type);
848
849
        foreach (IDocument *document, d->m_states.value(fileName).lastUpdatedState.keys())
            changedIDocuments.insert(document);
850
851
    }

852
853
854
855
    // clean up. do this before we may enter the main loop, otherwise we would
    // lose consecutive notifications.
    d->m_changedFiles.clear();

856
857
858
859
860
861
862
863
864
865
866
867
868
    // collect information about "expected" file names
    // we can't do the "resolving" already in expectFileChange, because
    // if the resolved names are different when unexpectFileChange is called
    // we would end up with never-unexpected file names
    QSet<QString> expectedFileNames;
    foreach (const QString &fileName, d->m_expectedFileNames) {
        const QString fixedName = fixFileName(fileName, KeepLinks);
        expectedFileNames.insert(fixedName);
        const QString fixedResolvedName = fixFileName(fileName, ResolveLinks);
        if (fixedName != fixedResolvedName)
            expectedFileNames.insert(fixedResolvedName);
    }

869
    // handle the IDocuments
870
    QStringList errorStrings;
871
872
873
    foreach (IDocument *document, changedIDocuments) {
        IDocument::ChangeTrigger trigger = IDocument::TriggerInternal;
        IDocument::ChangeType type = IDocument::TypePermissions;
874
875
876
877
        bool changed = false;
        // find out the type & behavior from the two possible files
        // behavior is internal if all changes are expected (and none removed)
        // type is "max" of both types (remove > contents > permissions)
878
        foreach (const QString & fileName, d->m_documentsWithWatch.value(document)) {
879
880
881
            // was the file reported?
            if (!currentStates.contains(fileName))
                continue;