documentmanager.cpp 49.3 KB
Newer Older
1
/**************************************************************************
con's avatar
con committed
2 3 4
**
** This file is part of Qt Creator
**
hjk's avatar
hjk committed
5
** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
con's avatar
con committed
6
**
7
** Contact: Nokia Corporation (qt-info@nokia.com)
con's avatar
con committed
8
**
9
**
10
** GNU Lesser General Public License Usage
11
**
hjk's avatar
hjk committed
12 13 14 15 16 17
** 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.
18
**
con's avatar
con committed
19
** In addition, as a special exception, Nokia gives you certain additional
hjk's avatar
hjk committed
20
** rights. These rights are described in the Nokia Qt LGPL Exception
con's avatar
con committed
21 22
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
23 24 25 26 27
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
con's avatar
con committed
28
** If you have questions regarding the use of this file, please contact
29
** Nokia at qt-info@nokia.com.
con's avatar
con committed
30
**
31
**************************************************************************/
hjk's avatar
hjk committed
32

33
#include "documentmanager.h"
hjk's avatar
hjk committed
34

35 36
#include "editormanager.h"
#include "icore.h"
37 38 39
#include "ieditor.h"
#include "ieditorfactory.h"
#include "iexternaleditor.h"
40
#include "idocument.h"
41 42
#include "iversioncontrol.h"
#include "mimedatabase.h"
con's avatar
con committed
43 44
#include "saveitemsdialog.h"
#include "vcsmanager.h"
45
#include "coreconstants.h"
con's avatar
con committed
46

hjk's avatar
hjk committed
47
#include <utils/qtcassert.h>
48
#include <utils/pathchooser.h>
49
#include <utils/reloadpromptutils.h>
hjk's avatar
hjk committed
50

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

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

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

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

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

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

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

99 100 101
static const char directoryGroupC[] = "Directories";
static const char projectDirectoryKeyC[] = "Projects";
static const char useProjectDirectoryKeyC[] = "UseProjectsDirectory";
con's avatar
con committed
102

103

104
namespace Core {
105 106 107

static void readSettings();

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

114 115
namespace Internal {

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

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

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

dt's avatar
dt committed
136

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

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

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

    QString m_currentFile;

    QMainWindow *m_mainWindow;
155
    QFileSystemWatcher *m_fileWatcher; // Delayed creation.
156
    QFileSystemWatcher *m_linkWatcher; // Delayed creation (only UNIX/if a link is seen).
157
    bool m_blockActivated;
158 159 160
    QString m_lastVisitedDirectory;
    QString m_projectsDirectory;
    bool m_useProjectsDirectory;
161
    // When we are callling into a 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
{
#ifdef Q_OS_UNIX
    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)));
    }
190
    return m_linkWatcher;
191
#else
192
    return fileWatcher();
193 194 195
#endif
}

196
DocumentManagerPrivate::DocumentManagerPrivate(QMainWindow *mw) :
con's avatar
con committed
197
    m_mainWindow(mw),
198 199
    m_fileWatcher(0),
    m_linkWatcher(0),
200 201 202
    m_blockActivated(false),
    m_lastVisitedDirectory(QDir::currentPath()),
#ifdef Q_OS_MAC  // Creator is in bizarre places when launched via finder.
dt's avatar
dt committed
203
    m_useProjectsDirectory(true),
204
#else
dt's avatar
dt committed
205
    m_useProjectsDirectory(false),
206
#endif
207
    m_blockedIDocument(0)
con's avatar
con committed
208
{
209 210 211
}

} // namespace Internal
212 213 214 215 216
} // namespace Core

Q_DECLARE_METATYPE(Core::Internal::OpenWithEntry)

namespace Core {
217

218 219
using namespace Internal;

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

230
    readSettings();
con's avatar
con committed
231 232
}

233
DocumentManager::~DocumentManager()
234 235 236 237
{
    delete d;
}

238
DocumentManager *DocumentManager::instance()
239
{
240 241 242
    return m_instance;
}

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

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

264
        d->m_states[fileName].lastUpdatedState.insert(document, state);
265
    }
266
    d->m_documentsWithWatch[document].append(fileName); // inserts a new QStringList if not already there
267 268
}

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

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

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

294 295 296 297
        foreach (IDocument *document, documents) {
            if (document && !d->m_documentsWithoutWatch.contains(document)) {
                connect(document, SIGNAL(destroyed(QObject *)), m_instance, SLOT(documentDestroyed(QObject *)));
                d->m_documentsWithoutWatch.append(document);
298 299
            }
        }
con's avatar
con committed
300
        return;
dt's avatar
dt committed
301 302
    }

303 304 305 306 307
    foreach (IDocument *document, documents) {
        if (document && !d->m_documentsWithWatch.contains(document)) {
            connect(document, SIGNAL(changed()), m_instance, SLOT(checkForNewFileName()));
            connect(document, SIGNAL(destroyed(QObject *)), m_instance, SLOT(documentDestroyed(QObject *)));
            addFileInfo(document);
308
        }
con's avatar
con committed
309 310 311
    }
}

312

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

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

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

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

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

393 394 395 396 397 398 399
    // rename the IDocuments
    foreach (IDocument *document, documentsToRename) {
        d->m_blockedIDocument = document;
        removeFileInfo(document);
        document->rename(to);
        addFileInfo(document);
        d->m_blockedIDocument = 0;
400
    }
401
}
con's avatar
con committed
402
/*!
403
    \fn bool DocumentManager::addFile(IDocument *document, bool addWatcher)
con's avatar
con committed
404

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

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

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

425
    Removes a IDocument object from the collection.
con's avatar
con committed
426

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

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

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

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

462
/*!
463
    \fn QString DocumentManager::fixFileName(const QString &fileName, FixMode fixmode)
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);
con's avatar
con committed
481 482 483
#ifdef Q_OS_WIN
    s = s.toLower();
#endif
484
    return s;
con's avatar
con committed
485 486 487
}

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

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

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

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

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

509
/*!
510
    \fn void DocumentManager::expectFileChange(const QString &fileName)
511 512 513

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

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

523 524 525 526 527 528 529 530 531 532 533 534
/* 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();
    }
}

535
/*!
536
    \fn void DocumentManager::unexpectFileChange(const QString &fileName)
537 538 539

    Any change to \a fileName are unexpected again.

540
    \see DocumentManager::expectFileChange(const QString &fileName)
541
*/
542
void DocumentManager::unexpectFileChange(const QString &fileName)
543 544 545 546 547 548
{
    // 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

549 550
    if (fileName.isEmpty())
        return;
551
    d->m_expectedFileNames.remove(fileName);
552 553 554 555 556
    const QString fixedName = fixFileName(fileName, KeepLinks);
    updateExpectedState(fixedName);
    const QString fixedResolvedName = fixFileName(fileName, ResolveLinks);
    if (fixedName != fixedResolvedName)
        updateExpectedState(fixedResolvedName);
557 558
}

con's avatar
con committed
559
/*!
560
    \fn QList<IDocument*> DocumentManager::saveModifiedFilesSilently(const QList<IDocument*> &documents)
con's avatar
con committed
561

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

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

573
    Asks the user whether to save the files listed in \a documents .
con's avatar
con committed
574 575 576 577 578 579 580
    Opens a dialog with the given \a message, and a additional
    text that should be used to ask if the user wants to enabled automatic save
    of modified files (in this context).
    The \a cancelled argument is set to true if the user cancelled the dialog,
    \a alwaysSave is set to match the selection of the user, if files should
    always automatically be saved.
    Returns the files that have not been saved.
con's avatar
con committed
581
*/
582
QList<IDocument *> DocumentManager::saveModifiedDocuments(const QList<IDocument *> &documents,
583 584 585
                                              bool *cancelled, const QString &message,
                                              const QString &alwaysSaveMessage,
                                              bool *alwaysSave)
con's avatar
con committed
586
{
587
    return saveModifiedFilesHelper(documents, cancelled, false, message, alwaysSaveMessage, alwaysSave);
con's avatar
con committed
588 589
}

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

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

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

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

641 642
        foreach (IDocument *document, documentsToSave) {
            if (!EditorManager::instance()->saveDocument(document)) {
643 644
                if (cancelled)
                    *cancelled = true;
645
                notSaved.append(document);
con's avatar
con committed
646 647 648 649 650 651
            }
        }
    }
    return notSaved;
}

652
bool DocumentManager::saveDocument(IDocument *document, const QString &fileName, bool *isReadOnly)
653
{
654
    bool ret = true;
655 656 657
    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
658

659
    QString errorString;
660
    if (!document->save(&errorString, fileName, false)) {
661 662 663
        if (isReadOnly) {
            QFile ofi(effName);
            // Check whether the existing file is writable
664
            if (!ofi.open(QIODevice::ReadWrite) && ofi.open(QIODevice::ReadOnly)) {
665
                *isReadOnly = true;
666
                goto out;
667 668 669 670
            }
            *isReadOnly = false;
        }
        QMessageBox::critical(d->m_mainWindow, tr("File Error"), errorString);
671 672
      out:
        ret = false;
673
    }
674

675
    addDocument(document, addWatcher);
676 677
    unexpectFileChange(effName);
    return ret;
678 679
}

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

727
QString DocumentManager::getSaveFileNameWithExtension(const QString &title, const QString &pathIn,
728 729 730 731 732 733
                                                  const QString &filter)
{
    QString selected = filter;
    return getSaveFileName(title, pathIn, filter, &selected);
}

con's avatar
con committed
734
/*!
735
    \fn QString DocumentManager::getSaveAsFileName(IDocument *document, const QString &filter, QString *selectedFilter)
con's avatar
con committed
736

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

con's avatar
con committed
754
    QString filterString;
755
    if (filter.isEmpty()) {
hjk's avatar
hjk committed
756
        if (const MimeType &mt = Core::ICore::mimeDatabase()->findByFile(fi))
757 758 759 760
            filterString = mt.filterString();
        selectedFilter = &filterString;
    } else {
        filterString = filter;
con's avatar
con committed
761 762
    }

763
    absoluteFilePath = getSaveFileName(tr("Save File As"),
con's avatar
con committed
764 765
        path + QDir::separator() + fileName,
        filterString,
766
        selectedFilter);
con's avatar
con committed
767 768 769
    return absoluteFilePath;
}

770
/*!
771
    \fn QStringList DocumentManager::getOpenFileNames(const QString &filters,
772 773
                                                  const QString pathIn,
                                                  QString *selectedFilter)
774

con's avatar
con committed
775 776 777 778
    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.
779 780
*/

781
QStringList DocumentManager::getOpenFileNames(const QString &filters,
782 783 784
                                          const QString pathIn,
                                          QString *selectedFilter)
{
785 786 787 788
    QString path = pathIn;
    if (path.isEmpty()) {
        if (!d->m_currentFile.isEmpty())
            path = QFileInfo(d->m_currentFile).absoluteFilePath();
789 790
        if (path.isEmpty() && useProjectsDirectory())
            path = projectsDirectory();
791
    }