documentmanager.cpp 51.5 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
hjk's avatar
hjk committed
3 4
** Copyright (C) 2012 Digia Plc and/or its subsidiary(-ies).
** 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 39
#include "iversioncontrol.h"
#include "mimedatabase.h"
con's avatar
con committed
40 41
#include "saveitemsdialog.h"
#include "vcsmanager.h"
42
#include "coreconstants.h"
con's avatar
con committed
43

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 50 51 52 53 54 55 56 57 58 59 60 61 62
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QPair>
#include <QSettings>
#include <QTimer>
#include <QAction>
#include <QFileDialog>
#include <QMainWindow>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
con's avatar
con committed
63 64

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

70 71 72
  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
73 74 75
  will be adjusted accordingly. Furthermore, on application exit the user will
  be asked to save all modified files.

76 77
  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
78 79
  enabled again by unblockFileChange().

80
  The functions expectFileChange() and unexpectFileChange() mark a file change
81
  as expected. On expected file changes all IDocument objects are notified to reload
82 83
  themselves.

84
  The DocumentManager service also provides two convenience methods for saving
con's avatar
con committed
85 86 87 88 89 90 91 92
  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()).
 */

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";
101

102
namespace Core {
103 104 105

static void readSettings();

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

112 113
namespace Internal {

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

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

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

dt's avatar
dt committed
134

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

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

147
    QList<DocumentManager::RecentFile> m_recentFiles;
148 149 150 151 152
    static const int m_maxRecentFiles = 7;

    QString m_currentFile;

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

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

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

180
QFileSystemWatcher *DocumentManagerPrivate::linkWatcher()
181
{
182 183 184 185 186 187 188 189
    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;
190
    }
191

192
    return fileWatcher();
193 194
}

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

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

Q_DECLARE_METATYPE(Core::Internal::OpenWithEntry)

namespace Core {
212

213 214
using namespace Internal;

215
DocumentManager::DocumentManager(QMainWindow *mw)
216
  : QObject(mw)
217
{
218
    d = new DocumentManagerPrivate(mw);
219
    m_instance = this;
220
    connect(d->m_mainWindow, SIGNAL(windowActivated()),
con's avatar
con committed
221
        this, SLOT(mainWindowActivated()));
hjk's avatar
hjk committed
222
    connect(ICore::instance(), SIGNAL(contextChanged(Core::IContext*,Core::Context)),
con's avatar
con committed
223 224
        this, SLOT(syncWithEditor(Core::IContext*)));

225
    readSettings();
con's avatar
con committed
226 227
}

228
DocumentManager::~DocumentManager()
229 230 231 232
{
    delete d;
}

233
DocumentManager *DocumentManager::instance()
234
{
235 236 237
    return m_instance;
}

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

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

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

con's avatar
con committed
277
/*!
278
    \fn bool DocumentManager::addFiles(const QList<IDocument *> &documents, bool addWatcher)
con's avatar
con committed
279

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

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

299 300 301
    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
302
            connect(document, SIGNAL(destroyed(QObject*)), m_instance, SLOT(documentDestroyed(QObject*)));
Robert Loehning's avatar
Robert Loehning committed
303
            connect(document, SIGNAL(fileNameChanged(QString,QString)), m_instance, SLOT(fileNameChanged(QString,QString)));
304
            addFileInfo(document);
305
        }
con's avatar
con committed
306 307 308
    }
}

309

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

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

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

366
/*!
367
    \fn void DocumentManager::renamedFile(const QString &from, const QString &to)
368 369 370 371
    \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.
372 373
    This will notify all IDocument objects pointing to that file of the rename
    by calling IDocument::rename, and update the cached time and permission
374 375 376
    information to avoid annoying the user with "file has been removed"
    popups.
*/
377
void DocumentManager::renamedFile(const QString &from, const QString &to)
dt's avatar
dt committed
378
{
379
    const QString &fixedFrom = fixFileName(from, KeepLinks);
dt's avatar
dt committed
380

381 382 383
    // gather the list of IDocuments
    QList<IDocument *> documentsToRename;
    QMapIterator<IDocument *, QStringList> it(d->m_documentsWithWatch);
384 385 386
    while (it.hasNext()) {
        it.next();
        if (it.value().contains(fixedFrom))
387
            documentsToRename.append(it.key());
388 389
    }

390 391 392 393 394 395 396
    // rename the IDocuments
    foreach (IDocument *document, documentsToRename) {
        d->m_blockedIDocument = document;
        removeFileInfo(document);
        document->rename(to);
        addFileInfo(document);
        d->m_blockedIDocument = 0;
397
    }
398
    emit m_instance->allDocumentsRenamed(from, to);
399
}
400 401 402 403 404 405 406 407 408 409

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
410
/*!
411
    \fn bool DocumentManager::addFile(IDocument *document, bool addWatcher)
con's avatar
con committed
412

413
    Adds a IDocument object to the collection. If \a addWatcher is true (the default),
con's avatar
con committed
414 415
    the file is added to a file system watcher that notifies the file manager
    about file changes.
con's avatar
con committed
416
*/
417
void DocumentManager::addDocument(IDocument *document, bool addWatcher)
con's avatar
con committed
418
{
419
    addDocuments(QList<IDocument *>() << document, addWatcher);
con's avatar
con committed
420 421
}

422
void DocumentManager::documentDestroyed(QObject *obj)
con's avatar
con committed
423
{
424
    IDocument *document = static_cast<IDocument*>(obj);
dt's avatar
dt committed
425
    // Check the special unwatched first:
426 427
    if (!d->m_documentsWithoutWatch.removeOne(document))
        removeFileInfo(document);
con's avatar
con committed
428 429 430
}

/*!
431
    \fn bool DocumentManager::removeFile(IDocument *document)
con's avatar
con committed
432

433
    Removes a IDocument object from the collection.
con's avatar
con committed
434

435
    Returns true if the file specified by \a document had the addWatcher argument to addDocument() set.
con's avatar
con committed
436
*/
437
bool DocumentManager::removeDocument(IDocument *document)
con's avatar
con committed
438
{
439
    QTC_ASSERT(document, return false);
con's avatar
con committed
440

441
    bool addWatcher = false;
dt's avatar
dt committed
442
    // Special casing unwatched files
443
    if (!d->m_documentsWithoutWatch.removeOne(document)) {
444
        addWatcher = true;
445 446
        removeFileInfo(document);
        disconnect(document, SIGNAL(changed()), m_instance, SLOT(checkForNewFileName()));
dt's avatar
dt committed
447
    }
Robert Loehning's avatar
Robert Loehning committed
448
    disconnect(document, SIGNAL(destroyed(QObject*)), m_instance, SLOT(documentDestroyed(QObject*)));
449
    return addWatcher;
con's avatar
con committed
450 451
}

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

464
    // Maybe the name has changed or file has been deleted and created again ...
465
    // This also updates the state to the on disk state
466 467
    removeFileInfo(document);
    addFileInfo(document);
con's avatar
con committed
468 469
}

470
/*!
471
    \fn QString DocumentManager::fixFileName(const QString &fileName, FixMode fixmode)
472 473 474 475
    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).
*/
476
QString DocumentManager::fixFileName(const QString &fileName, FixMode fixmode)
con's avatar
con committed
477 478
{
    QString s = fileName;
479
    QFileInfo fi(s);
480 481 482 483 484 485 486 487 488
    if (fi.exists()) {
        if (fixmode == ResolveLinks)
            s = fi.canonicalFilePath();
        else
            s = QDir::cleanPath(fi.absoluteFilePath());
    } else {
        s = QDir::cleanPath(s);
    }
    s = QDir::toNativeSeparators(s);
489 490
    if (Utils::HostOsInfo::isWindowsHost())
        s = s.toLower();
491
    return s;
con's avatar
con committed
492 493 494
}

/*!
495
    \fn QList<IDocument*> DocumentManager::modifiedFiles() const
con's avatar
con committed
496

497
    Returns the list of IDocument's that have been modified.
con's avatar
con committed
498
*/
499
QList<IDocument *> DocumentManager::modifiedDocuments()
con's avatar
con committed
500
{
501
    QList<IDocument *> modified;
con's avatar
con committed
502

503 504 505
    foreach (IDocument *document, d->m_documentsWithWatch.keys()) {
        if (document->isModified())
            modified << document;
con's avatar
con committed
506
    }
507

508 509 510
    foreach (IDocument *document, d->m_documentsWithoutWatch) {
        if (document->isModified())
            modified << document;
dt's avatar
dt committed
511 512
    }

513
    return modified;
con's avatar
con committed
514 515
}

516
/*!
517
    \fn void DocumentManager::expectFileChange(const QString &fileName)
518 519 520

    Any subsequent change to \a fileName is treated as a expected file change.

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

530 531 532 533 534 535 536 537 538 539 540 541
/* 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();
    }
}

542
/*!
543
    \fn void DocumentManager::unexpectFileChange(const QString &fileName)
544 545 546

    Any change to \a fileName are unexpected again.

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
}

con's avatar
con committed
566
/*!
567
    \fn QList<IDocument*> DocumentManager::saveModifiedFilesSilently(const QList<IDocument*> &documents)
con's avatar
con committed
568

569
    Tries to save the files listed in \a documents. The \a cancelled argument is set to true
570
    if the user cancelled the dialog. Returns the files that could not be saved.
con's avatar
con committed
571
*/
572
QList<IDocument *> DocumentManager::saveModifiedDocumentsSilently(const QList<IDocument *> &documents, bool *cancelled)
con's avatar
con committed
573
{
574
    return saveModifiedFilesHelper(documents, cancelled, true, QString());
con's avatar
con committed
575 576 577
}

/*!
578
    \fn QList<IDocument*> DocumentManager::saveModifiedFiles(const QList<IDocument *> &documents, bool *cancelled, const QString &message, const QString &alwaysSaveMessage, bool *alwaysSave)
con's avatar
con committed
579

580
    Asks the user whether to save the files listed in \a documents .
con's avatar
con committed
581 582 583 584 585 586 587
    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
    always automatically be saved.
    Returns the files that have not been saved.
con's avatar
con committed
588
*/
589
QList<IDocument *> DocumentManager::saveModifiedDocuments(const QList<IDocument *> &documents,
590 591 592
                                              bool *cancelled, const QString &message,
                                              const QString &alwaysSaveMessage,
                                              bool *alwaysSave)
con's avatar
con committed
593
{
594
    return saveModifiedFilesHelper(documents, cancelled, false, message, alwaysSaveMessage, alwaysSave);
con's avatar
con committed
595 596
}

597
static QList<IDocument *> saveModifiedFilesHelper(const QList<IDocument *> &documents,
598 599 600 601 602
                                              bool *cancelled,
                                              bool silently,
                                              const QString &message,
                                              const QString &alwaysSaveMessage,
                                              bool *alwaysSave)
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 612 613
    foreach (IDocument *document, documents) {
        if (document->isModified()) {
            QString name = document->fileName();
con's avatar
con committed
614
            if (name.isEmpty())
615
                name = document->suggestedFileName();
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 620 621
            // (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
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(d->m_mainWindow, 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 639
                if (alwaysSave)
                    *alwaysSave = dia.alwaysSaveChecked();
640
                notSaved = modifiedDocuments;
con's avatar
con committed
641 642
                return notSaved;
            }
643 644
            if (alwaysSave)
                *alwaysSave = dia.alwaysSaveChecked();
645
            documentsToSave = dia.itemsToSave();
con's avatar
con committed
646 647
        }

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
    \fn QString DocumentManager::getSaveAsFileName(IDocument *document, const QString &filter, QString *selectedFilter)
con's avatar
con committed
744

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

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

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

778
/*!
779
    \fn QStringList DocumentManager::getOpenFileNames(const QString &filters,
780 781
                                                  const QString pathIn,
                                                  QString *selectedFilter)
782

con's avatar
con committed
783 784 785 786
    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.
787 788
*/

789
QStringList DocumentManager::getOpenFileNames(const QString &filters,