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 methods 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
    explicit DocumentManagerPrivate(QMainWindow *mw);
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 153
    static const int m_maxRecentFiles = 7;

    QString m_currentFile;

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

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

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

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

193
    return fileWatcher();
194 195
}

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

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

Q_DECLARE_METATYPE(Core::Internal::OpenWithEntry)

namespace Core {
213

214 215
using namespace Internal;

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

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

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

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

238 239
/* only called from addFileInfo(IDocument *) */
static void addFileInfo(const QString &fileName, IDocument *document, bool isLink)
240 241 242 243 244 245 246
{
    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
247
        if (!d->m_states.contains(fileName))
248 249 250 251 252 253 254 255 256 257
            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
    const QString fixedName = DocumentManager::fixFileName(document->filePath(), DocumentManager::KeepLinks);
    const QString fixedResolvedName = DocumentManager::fixFileName(document->filePath(), DocumentManager::ResolveLinks);
271
    addFileInfo(fixedResolvedName, document, false);
272
    if (fixedName != fixedResolvedName)
273
        addFileInfo(fixedName, document, true);
274 275
}

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

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

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

306

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

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

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

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

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

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

387 388 389 390
    // rename the IDocuments
    foreach (IDocument *document, documentsToRename) {
        d->m_blockedIDocument = document;
        removeFileInfo(document);
391
        document->setFilePath(to);
392 393
        addFileInfo(document);
        d->m_blockedIDocument = 0;
394
    }
395
    emit m_instance->allDocumentsRenamed(from, to);
396
}
397

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

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

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

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

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

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

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

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

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

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

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

505
    return modified;
con's avatar
con committed
506 507
}

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

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

520 521 522 523 524 525 526 527 528 529 530 531
/* 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();
    }
}

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

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

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

con's avatar
con committed
554

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

/*!
Leena Miettinen's avatar
Leena Miettinen committed
568 569 570
    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
571
    of modified files (in this context).
Leena Miettinen's avatar
Leena Miettinen committed
572 573 574

    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
575
    always automatically be saved. If the files listed in documents have no write
Leena Miettinen's avatar
Leena Miettinen committed
576 577 578
    permissions, an additional dialog will be displayed to query the user for
    these permissions.

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

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

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

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

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

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

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

692
    addDocument(document, addWatcher);
693 694
    unexpectFileChange(effName);
    return ret;
695 696
}

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

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

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

con's avatar
con committed
769
    QString filterString;
770
    if (filter.isEmpty()) {
hjk's avatar
hjk committed
771
        if (const MimeType &mt = MimeDatabase::findByFile(fi))
772 773 774 775
            filterString = mt