basefilewizardfactory.cpp 17.5 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
con's avatar
con committed
2
**
Eike Ziller's avatar
Eike Ziller committed
3
4
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing
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
** 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
Eike Ziller's avatar
Eike Ziller committed
12
13
** a written agreement between you and The Qt Company.  For licensing terms and
** conditions see http://www.qt.io/terms-conditions.  For further information
Eike Ziller's avatar
Eike Ziller committed
14
** use the contact form at http://www.qt.io/contact-us.
15
**
16
** GNU Lesser General Public License Usage
hjk's avatar
hjk committed
17
** Alternatively, this file may be used under the terms of the GNU Lesser
Eike Ziller's avatar
Eike Ziller committed
18
19
20
21
22
23
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file.  Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
hjk's avatar
hjk committed
24
**
Eike Ziller's avatar
Eike Ziller committed
25
26
** In addition, as a special exception, The Qt Company gives you certain additional
** rights.  These rights are described in The Qt Company LGPL Exception
con's avatar
con committed
27
28
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
29
****************************************************************************/
hjk's avatar
hjk committed
30

31
#include "basefilewizardfactory.h"
Tobias Hunger's avatar
Tobias Hunger committed
32
33

#include "basefilewizard.h"
34
#include "icontext.h"
35
36
37
#include "icore.h"
#include "ifilewizardextension.h"
#include "editormanager/editormanager.h"
38
#include "dialogs/promptoverwritedialog.h"
39
#include <extensionsystem/pluginmanager.h>
40
#include <utils/filewizardpage.h>
Eike Ziller's avatar
Eike Ziller committed
41
#include <utils/mimetypes/mimedatabase.h>
42
#include <utils/qtcassert.h>
43
#include <utils/stringutils.h>
Eike Ziller's avatar
Eike Ziller committed
44
#include <utils/wizard.h>
con's avatar
con committed
45

46
47
48
49
50
51
#include <QDir>
#include <QFileInfo>
#include <QDebug>
#include <QSharedData>
#include <QEventLoop>
#include <QScopedPointer>
con's avatar
con committed
52

53
54
55
#include <QMessageBox>
#include <QWizard>
#include <QIcon>
con's avatar
con committed
56
57
58
59
60

enum { debugWizard = 0 };

namespace Core {

61
62
63
64
65
66
67
68
69
static int indexOfFile(const GeneratedFiles &f, const QString &path)
{
    const int size = f.size();
    for (int i = 0; i < size; ++i)
        if (f.at(i).path() == path)
            return i;
    return -1;
}

70
71
/*!
    \class Core::Internal::WizardEventLoop
Leena Miettinen's avatar
Leena Miettinen committed
72
73
    \brief The WizardEventLoop class implements a special event
    loop that runs a QWizard and terminates if the page changes.
74

Leena Miettinen's avatar
Leena Miettinen committed
75
    Used by Core::BaseFileWizard to intercept the change from the standard wizard pages
76
77
78
79
    to the extension pages (as the latter require the list of Core::GeneratedFile generated).

    Synopsis:
    \code
con's avatar
con committed
80
81
82
83
84
    Wizard wizard(parent);
    WizardEventLoop::WizardResult wr;
    do {
        wr = WizardEventLoop::execWizardPage(wizard);
    } while (wr == WizardEventLoop::PageChanged);
85
86
87
88
    \endcode

    \sa Core::GeneratedFile, Core::BaseFileWizardParameters, Core::BaseFileWizard, Core::StandardFileWizard
*/
con's avatar
con committed
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126

class WizardEventLoop : public QEventLoop
{
    Q_OBJECT
    WizardEventLoop(QObject *parent);

public:
    enum WizardResult { Accepted, Rejected , PageChanged };

    static WizardResult execWizardPage(QWizard &w);

private slots:
    void pageChanged(int);
    void accepted();
    void rejected();

private:
    WizardResult execWizardPageI();

    WizardResult m_result;
};

WizardEventLoop::WizardEventLoop(QObject *parent) :
    QEventLoop(parent),
    m_result(Rejected)
{
}

WizardEventLoop::WizardResult WizardEventLoop::execWizardPage(QWizard &wizard)
{
    /* Install ourselves on the wizard. Main trick is here to connect
     * to the page changed signal and quit() on it. */
    WizardEventLoop *eventLoop = wizard.findChild<WizardEventLoop *>();
    if (!eventLoop) {
        eventLoop = new WizardEventLoop(&wizard);
        connect(&wizard, SIGNAL(currentIdChanged(int)), eventLoop, SLOT(pageChanged(int)));
        connect(&wizard, SIGNAL(accepted()), eventLoop, SLOT(accepted()));
        connect(&wizard, SIGNAL(rejected()), eventLoop, SLOT(rejected()));
127
        wizard.setWindowFlags(wizard.windowFlags());
con's avatar
con committed
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
        wizard.show();
    }
    const WizardResult result = eventLoop->execWizardPageI();
    // Quitting?
    if (result != PageChanged)
        delete eventLoop;
    if (debugWizard)
        qDebug() << "WizardEventLoop::runWizard" << wizard.pageIds() << " returns " << result;

    return result;
}

WizardEventLoop::WizardResult WizardEventLoop::execWizardPageI()
{
    m_result = Rejected;
143
    exec();
con's avatar
con committed
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
    return m_result;
}

void WizardEventLoop::pageChanged(int /*page*/)
{
    m_result = PageChanged;
    quit(); // !
}

void WizardEventLoop::accepted()
{
    m_result = Accepted;
    quit();
}

void WizardEventLoop::rejected()
{
    m_result = Rejected;
    quit();
}

165
166
/*!
    \class Core::BaseFileWizard
167
168
    \brief The BaseFileWizard class implements a generic wizard for
    creating files.
169

170
    The following abstract functions must be implemented:
171
    \list
172
    \li create(): Called to create the QWizard dialog to be shown.
Leena Miettinen's avatar
Leena Miettinen committed
173
    \li generateFiles(): Generates file content.
174
175
    \endlist

176
    The behaviour can be further customized by overwriting the virtual function \c postGenerateFiles(),
177
178
179
180
181
182
    which is called after generating the files.

    \sa Core::GeneratedFile, Core::BaseFileWizardParameters, Core::StandardFileWizard
    \sa Core::Internal::WizardEventLoop
*/

183
184
185
Utils::Wizard *BaseFileWizardFactory::runWizardImpl(const QString &path, QWidget *parent,
                                                    const QString &platform,
                                                    const QVariantMap &extraValues)
con's avatar
con committed
186
{
187
    QTC_ASSERT(!path.isEmpty(), return 0);
188

con's avatar
con committed
189
190
    QString errorMessage;
    // Compile extension pages, purge out unused ones
191
    QList<IFileWizardExtension *> extensionList = ExtensionSystem::PluginManager::getObjects<IFileWizardExtension>();
con's avatar
con committed
192
    WizardPageList  allExtensionPages;
193
    for (auto it = extensionList.begin(); it !=  extensionList.end(); ) {
con's avatar
con committed
194
195
        const WizardPageList extensionPages = (*it)->extensionPages(this);
        if (extensionPages.empty()) {
196
            it = extensionList.erase(it);
con's avatar
con committed
197
198
199
200
201
202
203
        } else {
            allExtensionPages += extensionPages;
            ++it;
        }
    }

    if (debugWizard)
204
        qDebug() << Q_FUNC_INFO <<  path << parent << "exs" <<  extensionList.size() << allExtensionPages.size();
con's avatar
con committed
205
206
207
208
209
210
211
212

    QWizardPage *firstExtensionPage = 0;
    if (!allExtensionPages.empty())
        firstExtensionPage = allExtensionPages.front();

    // Create dialog and run it. Ensure that the dialog is deleted when
    // leaving the func, but not before the IFileWizardExtension::process
    // has been called
213
214
215
216
217
218

    WizardDialogParameters::DialogParameterFlags dialogParameterFlags;

    if (flags().testFlag(ForceCapitalLetterForFileName))
        dialogParameterFlags |= WizardDialogParameters::ForceCapitalLetterForFileName;

219
220
221
222
223
224
    const QScopedPointer<QWizard> wizard(create(parent, WizardDialogParameters(path,
                                                                               allExtensionPages,
                                                                               platform,
                                                                               requiredFeatures(),
                                                                               dialogParameterFlags,
                                                                               extraValues)));
225
    QTC_ASSERT(!wizard.isNull(), return 0);
226
    ICore::registerWindow(wizard.data(), Context("Core.NewWizard"));
227

con's avatar
con committed
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
    GeneratedFiles files;
    // Run the wizard: Call generate files on switching to the first extension
    // page is OR after 'Accepted' if there are no extension pages
    while (true) {
        const WizardEventLoop::WizardResult wr = WizardEventLoop::execWizardPage(*wizard);
        if (wr == WizardEventLoop::Rejected) {
            files.clear();
            break;
        }
        const bool accepted = wr == WizardEventLoop::Accepted;
        const bool firstExtensionPageHit = wr == WizardEventLoop::PageChanged
                                           && wizard->page(wizard->currentId()) == firstExtensionPage;
        const bool needGenerateFiles = firstExtensionPageHit || (accepted && allExtensionPages.empty());
        if (needGenerateFiles) {
            QString errorMessage;
            files = generateFiles(wizard.data(), &errorMessage);
            if (files.empty()) {
                QMessageBox::critical(0, tr("File Generation Failure"), errorMessage);
                break;
            }
        }
        if (firstExtensionPageHit)
250
            foreach (IFileWizardExtension *ex, extensionList)
251
                ex->firstExtensionPageShown(files, extraValues);
con's avatar
con committed
252
253
254
255
        if (accepted)
            break;
    }
    if (files.empty())
256
        return 0;
con's avatar
con committed
257
    // Compile result list and prompt for overwrite
258
    switch (promptOverwrite(&files, &errorMessage)) {
con's avatar
con committed
259
    case OverwriteCanceled:
260
        return 0;
con's avatar
con committed
261
262
    case OverwriteError:
        QMessageBox::critical(0, tr("Existing files"), errorMessage);
263
        return 0;
con's avatar
con committed
264
265
266
    case OverwriteOk:
        break;
    }
267

268
    foreach (IFileWizardExtension *ex, extensionList) {
269
270
271
272
273
        for (int i = 0; i < files.count(); i++) {
            ex->applyCodeStyle(&files[i]);
        }
    }

con's avatar
con committed
274
    // Write
275
276
    if (!writeFiles(files, &errorMessage)) {
        QMessageBox::critical(parent, tr("File Generation Failure"), errorMessage);
277
        return 0;
con's avatar
con committed
278
    }
279

280
    bool removeOpenProjectAttribute = false;
con's avatar
con committed
281
    // Run the extensions
282
    foreach (IFileWizardExtension *ex, extensionList) {
283
        bool remove;
284
        if (!ex->processFiles(files, &remove, &errorMessage)) {
285
286
            if (!errorMessage.isEmpty())
                QMessageBox::critical(parent, tr("File Generation Failure"), errorMessage);
287
            return 0;
con's avatar
con committed
288
        }
289
290
291
292
293
        removeOpenProjectAttribute |= remove;
    }

    if (removeOpenProjectAttribute) {
        for (int i = 0; i < files.count(); i++) {
hjk's avatar
hjk committed
294
295
            if (files[i].attributes() & GeneratedFile::OpenProjectAttribute)
                files[i].setAttributes(GeneratedFile::OpenEditorAttribute);
296
297
        }
    }
con's avatar
con committed
298
299

    // Post generation handler
300
    if (!postGenerateFiles(wizard.data(), files, &errorMessage))
301
302
        if (!errorMessage.isEmpty())
            QMessageBox::critical(0, tr("File Generation Failure"), errorMessage);
303
304

    return 0;
con's avatar
con committed
305
306
}

307
/*!
308
309
    \fn virtual QWizard *Core::BaseFileWizard::create(QWidget *parent,
                                                      const WizardDialogParameters &parameters) const
Leena Miettinen's avatar
Leena Miettinen committed
310

311
    Creates the wizard on the \a parent with the \a parameters.
312
313
314
315
316
*/

/*!
    \fn virtual Core::GeneratedFiles Core::BaseFileWizard::generateFiles(const QWizard *w,
                                                                         QString *errorMessage) const = 0
Leena Miettinen's avatar
Leena Miettinen committed
317
    Overwrite to query the parameters from the dialog and generate the files.
318

Leena Miettinen's avatar
Leena Miettinen committed
319
320
    \note This does not generate physical files, but merely the list of
    Core::GeneratedFile.
321
322
323
*/

/*!
Leena Miettinen's avatar
Leena Miettinen committed
324
    Physically writes files.
325
326
327
328

    Re-implement (calling the base implementation) to create files with CustomGeneratorAttribute set.
*/

329
bool BaseFileWizardFactory::writeFiles(const GeneratedFiles &files, QString *errorMessage)
330
{
331
332
    const GeneratedFile::Attributes noWriteAttributes
        = GeneratedFile::CustomGeneratorAttribute|GeneratedFile::KeepExistingFileAttribute;
333
    foreach (const GeneratedFile &generatedFile, files)
334
        if (!(generatedFile.attributes() & noWriteAttributes ))
335
336
337
338
339
            if (!generatedFile.write(errorMessage))
                return false;
    return true;
}

340
/*!
Leena Miettinen's avatar
Leena Miettinen committed
341
    Overwrite to perform steps to be done after files are actually created.
342
343
344
345

    The default implementation opens editors with the newly generated files.
*/

346
bool BaseFileWizardFactory::postGenerateFiles(const QWizard *, const GeneratedFiles &l, QString *errorMessage)
347
{
348
    return BaseFileWizardFactory::postGenerateOpenEditors(l, errorMessage);
349
350
}

351
/*!
Leena Miettinen's avatar
Leena Miettinen committed
352
    Opens the editors for the files whose attribute is set accordingly.
353
354
*/

355
bool BaseFileWizardFactory::postGenerateOpenEditors(const GeneratedFiles &l, QString *errorMessage)
con's avatar
con committed
356
{
hjk's avatar
hjk committed
357
358
359
    foreach (const GeneratedFile &file, l) {
        if (file.attributes() & GeneratedFile::OpenEditorAttribute) {
            if (!EditorManager::openEditor(file.path(), file.editorId())) {
360
                if (errorMessage)
361
                    *errorMessage = tr("Failed to open an editor for \"%1\".").arg(QDir::toNativeSeparators(file.path()));
362
363
                return false;
            }
con's avatar
con committed
364
365
366
367
368
        }
    }
    return true;
}

369
/*!
Leena Miettinen's avatar
Leena Miettinen committed
370
371
    Performs an overwrite check on a set of \a files. Checks if the file exists and
    can be overwritten at all, and then prompts the user with a summary.
372
373
*/

374
BaseFileWizardFactory::OverwriteResult BaseFileWizardFactory::promptOverwrite(GeneratedFiles *files,
con's avatar
con committed
375
376
377
                                                                QString *errorMessage) const
{
    if (debugWizard)
378
        qDebug() << Q_FUNC_INFO << files;
con's avatar
con committed
379

380
    QStringList existingFiles;
con's avatar
con committed
381
382
    bool oddStuffFound = false;

383
384
385
    static const QString readOnlyMsg = tr("[read only]");
    static const QString directoryMsg = tr("[folder]");
    static const QString symLinkMsg = tr("[symbolic link]");
con's avatar
con committed
386

387
    foreach (const GeneratedFile &file, *files) {
388
389
390
        const QString path = file.path();
        if (QFileInfo::exists(path))
            existingFiles.append(path);
391
    }
392
393
394
395
396
    if (existingFiles.isEmpty())
        return OverwriteOk;
    // Before prompting to overwrite existing files, loop over files and check
    // if there is anything blocking overwriting them (like them being links or folders).
    // Format a file list message as ( "<file1> [readonly], <file2> [folder]").
397
    const QString commonExistingPath = Utils::commonPath(existingFiles);
con's avatar
con committed
398
    QString fileNamesMsgPart;
399
    foreach (const QString &fileName, existingFiles) {
con's avatar
con committed
400
401
402
403
        const QFileInfo fi(fileName);
        if (fi.exists()) {
            if (!fileNamesMsgPart.isEmpty())
                fileNamesMsgPart += QLatin1String(", ");
404
            fileNamesMsgPart += QDir::toNativeSeparators(fileName.mid(commonExistingPath.size() + 1));
con's avatar
con committed
405
406
407
            do {
                if (fi.isDir()) {
                    oddStuffFound = true;
408
                    fileNamesMsgPart += QLatin1Char(' ') + directoryMsg;
con's avatar
con committed
409
410
411
412
                    break;
                }
                if (fi.isSymLink()) {
                    oddStuffFound = true;
413
                    fileNamesMsgPart += QLatin1Char(' ') + symLinkMsg;
con's avatar
con committed
414
415
416
417
                    break;
            }
                if (!fi.isWritable()) {
                    oddStuffFound = true;
418
                    fileNamesMsgPart += QLatin1Char(' ') + readOnlyMsg;
con's avatar
con committed
419
420
421
422
423
424
                }
            } while (false);
        }
    }

    if (oddStuffFound) {
425
426
        *errorMessage = tr("The project directory %1 contains files which cannot be overwritten:\n%2.")
                .arg(QDir::toNativeSeparators(commonExistingPath)).arg(fileNamesMsgPart);
con's avatar
con committed
427
428
        return OverwriteError;
    }
429
    // Prompt to overwrite existing files.
430
    PromptOverwriteDialog overwriteDialog;
431
432
433
434
435
436
437
438
439
440
441
442
443
    // Scripts cannot handle overwrite
    overwriteDialog.setFiles(existingFiles);
    foreach (const GeneratedFile &file, *files)
        if (file.attributes() & GeneratedFile::CustomGeneratorAttribute)
            overwriteDialog.setFileEnabled(file.path(), false);
    if (overwriteDialog.exec() != QDialog::Accepted)
        return OverwriteCanceled;
    const QStringList existingFilesToKeep = overwriteDialog.uncheckedFiles();
    if (existingFilesToKeep.size() == files->size()) // All exist & all unchecked->Cancel.
        return OverwriteCanceled;
    // Set 'keep' attribute in files
    foreach (const QString &keepFile, existingFilesToKeep) {
        const int i = indexOfFile(*files, keepFile);
444
        QTC_ASSERT(i != -1, return OverwriteCanceled);
445
446
447
448
        GeneratedFile &file = (*files)[i];
        file.setAttributes(file.attributes() | GeneratedFile::KeepExistingFileAttribute);
    }
    return OverwriteOk;
con's avatar
con committed
449
450
}

451
/*!
Leena Miettinen's avatar
Leena Miettinen committed
452
453
    Constructs a file name, adding the \a extension unless \a baseName already has
    one.
454
455
*/

456
QString BaseFileWizardFactory::buildFileName(const QString &path,
con's avatar
con committed
457
458
459
460
                                      const QString &baseName,
                                      const QString &extension)
{
    QString rc = path;
461
462
463
    const QChar slash = QLatin1Char('/');
    if (!rc.isEmpty() && !rc.endsWith(slash))
        rc += slash;
con's avatar
con committed
464
465
466
467
468
469
470
471
472
473
474
475
476
    rc += baseName;
    // Add extension unless user specified something else
    const QChar dot = QLatin1Char('.');
    if (!extension.isEmpty() && !baseName.contains(dot)) {
        if (!extension.startsWith(dot))
            rc += dot;
        rc += extension;
    }
    if (debugWizard)
        qDebug() << Q_FUNC_INFO << rc;
    return rc;
}

477
/*!
Leena Miettinen's avatar
Leena Miettinen committed
478
    Returns the preferred suffix for \a mimeType.
479
480
*/

481
QString BaseFileWizardFactory::preferredSuffix(const QString &mimeType)
con's avatar
con committed
482
{
Eike Ziller's avatar
Eike Ziller committed
483
484
485
486
487
    QString rc;
    Utils::MimeDatabase mdb;
    Utils::MimeType mt = mdb.mimeTypeForName(mimeType);
    if (mt.isValid())
        rc = mt.preferredSuffix();
con's avatar
con committed
488
489
490
491
492
493
    if (rc.isEmpty())
        qWarning("%s: WARNING: Unable to find a preferred suffix for %s.",
                 Q_FUNC_INFO, mimeType.toUtf8().constData());
    return rc;
}

494
495
/*!
    \class Core::StandardFileWizard
496
497
    \brief The StandardFileWizard class is a convenience class for
    creating one file.
498
499
500
501
502
503
504
505

    It uses Utils::FileWizardDialog and introduces a new virtual to generate the
    files from path and name.

    \sa Core::GeneratedFile, Core::BaseFileWizardParameters, Core::BaseFileWizard
    \sa Core::Internal::WizardEventLoop
*/

con's avatar
con committed
506
} // namespace Core
hjk's avatar
hjk committed
507

508
#include "basefilewizardfactory.moc"