documentmanager.cpp 49.9 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
#include "icore.h"
33
#include "idocument.h"
34
#include "mimedatabase.h"
35
#include "coreconstants.h"
con's avatar
con committed
36

37 38 39 40 41 42
#include <coreplugin/dialogs/readonlyfilesdialog.h>
#include <coreplugin/dialogs/saveitemsdialog.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/editormanager/ieditorfactory.h>
#include <coreplugin/editormanager/iexternaleditor.h>
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
#include <QStringList>
50 51 52 53 54 55 56 57 58 59 60 61
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFileSystemWatcher>
#include <QSettings>
#include <QTimer>
#include <QAction>
#include <QFileDialog>
#include <QMainWindow>
#include <QMenu>
#include <QMessageBox>
con's avatar
con committed
62 63

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

Leena Miettinen's avatar
Leena Miettinen committed
69 70 71 72
  The DocumentManager service monitors a set of IDocument objects. Plugins
  should register files they work with at the service. The files the IDocument
  objects point to will be monitored at filesystem level. If a file changes,
  the status of the IDocument object
con's avatar
con committed
73 74 75
  will be adjusted accordingly. Furthermore, on application exit the user will
  be asked to save all modified files.

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

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

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

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

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

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

103
namespace Core {
104 105 106

static void readSettings();

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

113 114
namespace Internal {

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

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

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

dt's avatar
dt committed
135

136
struct DocumentManagerPrivate
137
{
138
    DocumentManagerPrivate();
139 140
    QFileSystemWatcher *fileWatcher();
    QFileSystemWatcher *linkWatcher();
141

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

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

    QString m_currentFile;

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;
Leena Miettinen's avatar
Leena Miettinen committed
160
    // When we are calling into an 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() :
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 215
DocumentManager::DocumentManager(QObject *parent)
  : QObject(parent)
216
{
217
    d = new DocumentManagerPrivate;
218
    m_instance = this;
219 220
    connect(ICore::instance(), SIGNAL(contextChanged(QList<Core::IContext*>,Core::Context)),
        this, SLOT(syncWithEditor(QList<Core::IContext*>)));
221
    qApp->installEventFilter(this);
con's avatar
con committed
222

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

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

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

236 237
/* only called from addFileInfo(IDocument *) */
static void addFileInfo(const QString &fileName, IDocument *document, bool isLink)
238 239 240 241 242 243 244
{
    FileStateItem state;
    if (!fileName.isEmpty()) {
        const QFileInfo fi(fileName);
        state.modified = fi.lastModified();
        state.permissions = fi.permissions();
        // Add watcher if we don't have that already
245
        if (!d->m_states.contains(fileName))
246 247 248 249 250 251 252 253 254 255
            d->m_states.insert(fileName, FileState());

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

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

261
/* Adds the IDocument's file and possibly it's final link target to both m_states
262 263 264
   (if it's file name is not empty), and the m_filesWithWatch list,
   and adds a file watcher for each if not already done.
   (The added file names are guaranteed to be absolute and cleaned.) */
265
static void addFileInfo(IDocument *document)
266
{
267 268
    const QString fixedName = DocumentManager::fixFileName(document->filePath(), DocumentManager::KeepLinks);
    const QString fixedResolvedName = DocumentManager::fixFileName(document->filePath(), DocumentManager::ResolveLinks);
269
    addFileInfo(fixedResolvedName, document, false);
270
    if (fixedName != fixedResolvedName)
271
        addFileInfo(fixedName, document, true);
272 273
}

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

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

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

304

305 306
/* Removes all occurrences of the IDocument from m_filesWithWatch and m_states.
   If that results in a file no longer being referenced by any IDocument, this
307 308
   also removes the file watcher.
*/
309
static void removeFileInfo(IDocument *document)
310
{
311
    if (!d->m_documentsWithWatch.contains(document))
312
        return;
313
    foreach (const QString &fileName, d->m_documentsWithWatch.value(document)) {
314 315
        if (!d->m_states.contains(fileName))
            continue;
316
        d->m_states[fileName].lastUpdatedState.remove(document);
317 318 319 320 321 322
        if (d->m_states.value(fileName).lastUpdatedState.isEmpty()) {
            if (d->m_fileWatcher && d->m_fileWatcher->files().contains(fileName))
                d->m_fileWatcher->removePath(fileName);
            if (d->m_linkWatcher && d->m_linkWatcher->files().contains(fileName))
                d->m_linkWatcher->removePath(fileName);
            d->m_states.remove(fileName);
323
        }
324
    }
325
    d->m_documentsWithWatch.remove(document);
326 327
}

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

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

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

Leena Miettinen's avatar
Leena Miettinen committed
364 365
    Needs to be called right after the actual renaming on disk (that is, before
    the file system
366
    watcher can report the event during the next event loop run). \a from needs to be an absolute file path.
367
    This will notify all IDocument objects pointing to that file of the rename
Leena Miettinen's avatar
Leena Miettinen committed
368
    by calling \c 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
    // rename the IDocuments
    foreach (IDocument *document, documentsToRename) {
        d->m_blockedIDocument = document;
        removeFileInfo(document);
389
        document->setFilePath(to);
390 391
        addFileInfo(document);
        d->m_blockedIDocument = 0;
392
    }
393
    emit m_instance->allDocumentsRenamed(from, to);
394
}
395

396
void DocumentManager::filePathChanged(const QString &oldName, const QString &newName)
397 398 399 400 401 402 403 404
{
    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
/*!
Leena Miettinen's avatar
Leena Miettinen committed
406 407
    Adds an IDocument object to the collection. If \a addWatcher is \c true
    (the default),
con's avatar
con committed
408 409
    the file is added to a file system watcher that notifies the file manager
    about file changes.
con's avatar
con committed
410
*/
411
void DocumentManager::addDocument(IDocument *document, bool addWatcher)
con's avatar
con committed
412
{
413
    addDocuments(QList<IDocument *>() << document, addWatcher);
con's avatar
con committed
414 415
}

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

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

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

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

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

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

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

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

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

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

503
    return modified;
con's avatar
con committed
504 505
}

506
/*!
Leena Miettinen's avatar
Leena Miettinen committed
507
    Any subsequent change to \a fileName is treated as an expected file change.
508

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

518
/* only called from unblock and unexpect file change functions */
519 520 521 522 523 524 525 526 527 528 529
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();
    }
}

530
/*!
Leena Miettinen's avatar
Leena Miettinen committed
531
    Any changes to \a fileName are unexpected again.
532

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

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

con's avatar
con committed
552

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

/*!
Leena Miettinen's avatar
Leena Miettinen committed
566 567 568
    Asks the user whether to save the files listed in \a documents. Opens a
    dialog that displays the \a message, and additional text to ask the users
    if they want to enable automatic saving
con's avatar
con committed
569
    of modified files (in this context).
Leena Miettinen's avatar
Leena Miettinen committed
570 571 572

    The \a cancelled argument is set to true if the user cancels the dialog.
    \a alwaysSave is set to match the selection of the user if files should
573
    always automatically be saved. If the files listed in documents have no write
Leena Miettinen's avatar
Leena Miettinen committed
574 575 576
    permissions, an additional dialog will be displayed to query the user for
    these permissions.

con's avatar
con committed
577
    Returns the files that have not been saved.
con's avatar
con committed
578
*/
579
QList<IDocument *> DocumentManager::saveModifiedDocuments(const QList<IDocument *> &documents,
580 581 582
                                              bool *cancelled, const QString &message,
                                              const QString &alwaysSaveMessage,
                                              bool *alwaysSave)
con's avatar
con committed
583
{
584
    return saveModifiedFilesHelper(documents, cancelled, false, message, alwaysSaveMessage, alwaysSave);
con's avatar
con committed
585 586
}

587
static QList<IDocument *> saveModifiedFilesHelper(const QList<IDocument *> &documents,
588 589 590 591 592
                                              bool *cancelled,
                                              bool silently,
                                              const QString &message,
                                              const QString &alwaysSaveMessage,
                                              bool *alwaysSave)
con's avatar
con committed
593 594 595 596
{
    if (cancelled)
        (*cancelled) = false;

597 598 599
    QList<IDocument *> notSaved;
    QMap<IDocument *, QString> modifiedDocumentsMap;
    QList<IDocument *> modifiedDocuments;
con's avatar
con committed
600

601 602
    foreach (IDocument *document, documents) {
        if (document->isModified()) {
603
            QString name = document->filePath();
con's avatar
con committed
604
            if (name.isEmpty())
605
                name = document->suggestedFileName();
con's avatar
con committed
606

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

666
bool DocumentManager::saveDocument(IDocument *document, const QString &fileName, bool *isReadOnly)
667
{
668
    bool ret = true;
669
    QString effName = fileName.isEmpty() ? document->filePath() : fileName;
670 671
    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
672

673
    QString errorString;
674
    if (!document->save(&errorString, fileName, false)) {
675 676 677
        if (isReadOnly) {
            QFile ofi(effName);
            // Check whether the existing file is writable
678
            if (!ofi.open(QIODevice::ReadWrite) && ofi.open(QIODevice::ReadOnly)) {
679
                *isReadOnly = true;
680
                goto out;
681 682 683
            }
            *isReadOnly = false;
        }
684
        QMessageBox::critical(ICore::dialogParent(), tr("File Error"),
685
                              tr("Error while saving file: %1").arg(errorString));
686 687
      out:
        ret = false;
688
    }
689

690
    addDocument(document, addWatcher);
691 692
    unexpectFileChange(effName);
    return ret;
693 694
}

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

742
QString DocumentManager::getSaveFileNameWithExtension(const QString &title, const QString &pathIn,
743 744 745 746 747 748
                                                  const QString &filter)
{
    QString selected = filter;
    return getSaveFileName(title, pathIn, filter, &selected);
}

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

con's avatar
con committed
767
    QString filterString;
768
    if (filter.isEmpty()) {
hjk's avatar
hjk committed
769
        if (const MimeType &mt = MimeDatabase::findByFile(fi))