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 67
  \inheaderfile documentmanager.h
  \brief 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
    connect(d->m_mainWindow, SIGNAL(windowActivated()),
con's avatar
con committed
220
        this, SLOT(mainWindowActivated()));
221 222
    connect(ICore::instance(), SIGNAL(contextChanged(QList<Core::IContext*>,Core::Context)),
        this, SLOT(syncWithEditor(QList<Core::IContext*>)));
con's avatar
con committed
223

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
{
    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
246
        if (!d->m_states.contains(fileName))
247 248 249 250 251 252 253 254 255 256
            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);

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

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

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

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

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

305

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

516 517 518 519 520 521 522 523 524 525 526 527
/* 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();
    }
}

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

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

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

con's avatar
con committed
550

Orgad Shaneh's avatar
Orgad Shaneh committed
551
/*!
552
    Tries to save the files listed in \a documents. The \a cancelled argument is set to true
553 554 555
    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
556
*/
557
QList<IDocument *> DocumentManager::saveModifiedDocumentsSilently(const QList<IDocument *> &documents, bool *cancelled)
con's avatar
con committed
558
{
559
    return saveModifiedFilesHelper(documents, cancelled, true, QString());
con's avatar
con committed
560 561 562
}

/*!
563
    Asks the user whether to save the files listed in \a documents .
con's avatar
con committed
564 565 566 567 568
    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
569 570
    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
571
    Returns the files that have not been saved.
con's avatar
con committed
572
*/
573
QList<IDocument *> DocumentManager::saveModifiedDocuments(const QList<IDocument *> &documents,
574 575 576
                                              bool *cancelled, const QString &message,
                                              const QString &alwaysSaveMessage,
                                              bool *alwaysSave)
con's avatar
con committed
577
{
578
    return saveModifiedFilesHelper(documents, cancelled, false, message, alwaysSaveMessage, alwaysSave);
con's avatar
con committed
579 580
}

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

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

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

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

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

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

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

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

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

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(const 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
/*!
con's avatar
con committed
778 779 780 781
    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.
782 783
*/

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

804
void DocumentManager::changedFile(const QString &fileName)