documentmanager.cpp 50.7 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";
con's avatar
con committed
100

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
    // 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
    connect(d->m_mainWindow, SIGNAL(windowActivated()),
con's avatar
con committed
220
        this, SLOT(mainWindowActivated()));
hjk's avatar
hjk committed
221
    connect(ICore::instance(), SIGNAL(contextChanged(Core::IContext*,Core::Context)),
con's avatar
con committed
222 223
        this, SLOT(syncWithEditor(Core::IContext*)));

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

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

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

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

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

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

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

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

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

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

308

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Any change to \a fileName are unexpected again.

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

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

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

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

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

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

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

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

610 611 612
    foreach (IDocument *document, documents) {
        if (document->isModified()) {
            QString name = document->fileName();
con's avatar
con committed
613
            if (name.isEmpty())
614
                name = document->suggestedFileName();
con's avatar
con committed
615

616
            // There can be several IDocuments pointing to the same file
617
            // Prefer one that is not readonly
618 619 620
            // (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
621 622
        }
    }
623 624 625
    modifiedDocuments = modifiedDocumentsMap.keys();
    if (!modifiedDocuments.isEmpty()) {
        QList<IDocument *> documentsToSave;
con's avatar
con committed
626
        if (silently) {
627
            documentsToSave = modifiedDocuments;
con's avatar
con committed
628
        } else {
629
            SaveItemsDialog dia(d->m_mainWindow, modifiedDocuments);
con's avatar
con committed
630 631
            if (!message.isEmpty())
                dia.setMessage(message);
632 633
            if (!alwaysSaveMessage.isNull())
                dia.setAlwaysSaveMessage(alwaysSaveMessage);
con's avatar
con committed
634 635 636
            if (dia.exec() != QDialog::Accepted) {
                if (cancelled)
                    (*cancelled) = true;
637 638
                if (alwaysSave)
                    *alwaysSave = dia.alwaysSaveChecked();
639
                notSaved = modifiedDocuments;
con's avatar
con committed
640 641
                return notSaved;
            }
642 643
            if (alwaysSave)
                *alwaysSave = dia.alwaysSaveChecked();
644
            documentsToSave = dia.itemsToSave();
con's avatar
con committed
645 646
        }

647 648
        foreach (IDocument *document, documentsToSave) {
            if (!EditorManager::instance()->saveDocument(document)) {
649 650
                if (cancelled)
                    *cancelled = true;
651
                notSaved.append(document);
con's avatar
con committed
652 653 654 655 656 657
            }
        }
    }
    return notSaved;
}

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

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

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

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

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

con's avatar
con committed
741
/*!
742
    \fn QString DocumentManager::getSaveAsFileName(IDocument *document, const QString &filter, QString *selectedFilter)
con's avatar
con committed
743

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

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

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

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

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

788
QStringList DocumentManager::getOpenFileNames(const QString &filters,
789 790 791
                                          const QString pathIn,
                                          QString *selectedFilter)
{