externaltool.cpp 17.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** GNU Lesser General Public License Usage
**
** 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.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/

#include "externaltool.h"
con's avatar
con committed
31 32 33
#include "actionmanager/actionmanager.h"
#include "actionmanager/actioncontainer.h"
#include "actionmanager/command.h"
34
#include "coreconstants.h"
con's avatar
con committed
35 36 37 38 39 40
#include "variablemanager.h"

#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
#include <utils/qtcassert.h>
#include <utils/stringutils.h>
41
#include <utils/environment.h>
42 43

#include <QtCore/QXmlStreamReader>
44 45 46 47 48
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtGui/QMenu>
#include <QtGui/QMenuItem>
#include <QtGui/QAction>
49 50 51

#include <QtDebug>

52
using namespace Core;
53 54 55 56 57 58 59 60 61 62 63
using namespace Core::Internal;

namespace {
    const char * const kExternalTool = "externaltool";
    const char * const kDescription = "description";
    const char * const kDisplayName = "displayname";
    const char * const kCategory = "category";
    const char * const kOrder = "order";
    const char * const kExecutable = "executable";
    const char * const kPath = "path";
    const char * const kArguments = "arguments";
64
    const char * const kInput = "input";
65 66 67 68
    const char * const kWorkingDirectory = "workingdirectory";

    const char * const kXmlLang = "xml:lang";
    const char * const kOutput = "output";
con's avatar
con committed
69
    const char * const kError = "error";
70 71 72
    const char * const kOutputShowInPane = "showinpane";
    const char * const kOutputReplaceSelection = "replaceselection";
    const char * const kOutputReloadDocument = "reloaddocument";
con's avatar
con committed
73
    const char * const kOutputIgnore = "ignore";
74 75
}

76 77
// #pragma mark -- ExternalTool

78 79
ExternalTool::ExternalTool() :
    m_order(-1),
con's avatar
con committed
80 81
    m_outputHandling(ShowInPane),
    m_errorHandling(ShowInPane)
82 83 84
{
}

con's avatar
con committed
85 86 87 88 89
ExternalTool::~ExternalTool()
{
    // TODO kill running process
}

90 91 92 93 94
QString ExternalTool::id() const
{
    return m_id;
}

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
QString ExternalTool::description() const
{
    return m_description;
}

QString ExternalTool::displayName() const
{
    return m_displayName;
}

QString ExternalTool::displayCategory() const
{
    return m_displayCategory;
}

int ExternalTool::order() const
{
    return m_order;
}

QStringList ExternalTool::executables() const
{
    return m_executables;
}

QString ExternalTool::arguments() const
{
    return m_arguments;
}

125 126 127 128 129
QString ExternalTool::input() const
{
    return m_input;
}

130 131 132 133 134 135 136 137 138 139
QString ExternalTool::workingDirectory() const
{
    return m_workingDirectory;
}

ExternalTool::OutputHandling ExternalTool::outputHandling() const
{
    return m_outputHandling;
}

con's avatar
con committed
140 141 142 143 144
ExternalTool::OutputHandling ExternalTool::errorHandling() const
{
    return m_errorHandling;
}

145
static QStringList splitLocale(const QString &locale)
146
{
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
    QString value = locale;
    QStringList values;
    if (!value.isEmpty())
        values << value;
    int index = value.indexOf(QLatin1Char('.'));
    if (index >= 0) {
        value = value.left(index);
        if (!value.isEmpty())
            values << value;
    }
    index = value.indexOf(QLatin1Char('_'));
    if (index >= 0) {
        value = value.left(index);
        if (!value.isEmpty())
            values << value;
    }
    return values;
}

static void localizedText(const QStringList &locales, QXmlStreamReader *reader, int *currentLocale, QString *currentText)
{
    Q_ASSERT(reader);
    Q_ASSERT(currentLocale);
    Q_ASSERT(currentText);
    if (reader->attributes().hasAttribute(QLatin1String(kXmlLang))) {
        int index = locales.indexOf(reader->attributes().value(QLatin1String(kXmlLang)).toString());
        if (index >= 0 && (index < *currentLocale || *currentLocale < 0)) {
            *currentText = reader->readElementText();
            *currentLocale = index;
        } else {
            reader->skipCurrentElement();
        }
    } else {
        if (*currentLocale < 0 && currentText->isEmpty()) {
            *currentText = reader->readElementText();
        } else {
            reader->skipCurrentElement();
        }
    }
}

con's avatar
con committed
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
static bool parseOutputAttribute(const QString &attribute, QXmlStreamReader *reader, ExternalTool::OutputHandling *value)
{
    const QString output = reader->attributes().value(attribute).toString();
    if (output == QLatin1String(kOutputShowInPane)) {
        *value = ExternalTool::ShowInPane;
    } else if (output == QLatin1String(kOutputReplaceSelection)) {
        *value = ExternalTool::ReplaceSelection;
    } else if (output == QLatin1String(kOutputReloadDocument)) {
        *value = ExternalTool::ReloadDocument;
    } else if (output == QLatin1String(kOutputIgnore)) {
        *value = ExternalTool::Ignore;
    } else {
        reader->raiseError(QLatin1String("Allowed values for output attribute are 'showinpane','replaceselection','reloaddocument'"));
        return false;
    }
    return true;
}

206
ExternalTool * ExternalTool::createFromXml(const QByteArray &xml, QString *errorMessage, const QString &locale)
207 208 209 210 211
{
    int descriptionLocale = -1;
    int nameLocale = -1;
    int categoryLocale = -1;
    const QStringList &locales = splitLocale(locale);
212 213
    ExternalTool *tool = new ExternalTool;
    QXmlStreamReader reader(xml);
214

215 216
    if (!reader.readNextStartElement() || reader.name() != QLatin1String(kExternalTool))
        reader.raiseError(QLatin1String("Missing start element <externaltool>"));
217 218 219
    tool->m_id = reader.attributes().value(QLatin1String("id")).toString();
    if (tool->m_id.isEmpty())
        reader.raiseError(QLatin1String("Missing or empty id attribute for <externaltool>"));
220 221
    while (reader.readNextStartElement()) {
        if (reader.name() == QLatin1String(kDescription)) {
222
            localizedText(locales, &reader, &descriptionLocale, &tool->m_description);
223
        } else if (reader.name() == QLatin1String(kDisplayName)) {
224
            localizedText(locales, &reader, &nameLocale, &tool->m_displayName);
225
        } else if (reader.name() == QLatin1String(kCategory)) {
226
            localizedText(locales, &reader, &categoryLocale, &tool->m_displayCategory);
227 228 229 230 231 232 233 234 235 236 237
        } else if (reader.name() == QLatin1String(kOrder)) {
            if (tool->m_order >= 0) {
                reader.raiseError(QLatin1String("only one <order> element allowed"));
                break;
            }
            bool ok;
            tool->m_order = reader.readElementText().toInt(&ok);
            if (!ok || tool->m_order < 0)
                reader.raiseError(QLatin1String("<order> element requires non-negative integer value"));
        } else if (reader.name() == QLatin1String(kExecutable)) {
            if (reader.attributes().hasAttribute(QLatin1String(kOutput))) {
con's avatar
con committed
238 239 240 241 242
                if (!parseOutputAttribute(QLatin1String(kOutput), &reader, &tool->m_outputHandling))
                    break;
            }
            if (reader.attributes().hasAttribute(QLatin1String(kError))) {
                if (!parseOutputAttribute(QLatin1String(kError), &reader, &tool->m_errorHandling))
243 244 245 246 247 248 249 250 251 252 253
                    break;
            }
            while (reader.readNextStartElement()) {
                if (reader.name() == QLatin1String(kPath)) {
                    tool->m_executables.append(reader.readElementText());
                } else if (reader.name() == QLatin1String(kArguments)) {
                    if (!tool->m_arguments.isEmpty()) {
                        reader.raiseError(QLatin1String("only one <arguments> element allowed"));
                        break;
                    }
                    tool->m_arguments = reader.readElementText();
254 255 256 257 258 259
                } else if (reader.name() == QLatin1String(kInput)) {
                    if (!tool->m_input.isEmpty()) {
                        reader.raiseError(QLatin1String("only one <input> element allowed"));
                        break;
                    }
                    tool->m_input = reader.readElementText();
260 261 262 263 264 265
                } else if (reader.name() == QLatin1String(kWorkingDirectory)) {
                    if (!tool->m_workingDirectory.isEmpty()) {
                        reader.raiseError(QLatin1String("only one <workingdirectory> element allowed"));
                        break;
                    }
                    tool->m_workingDirectory = reader.readElementText();
con's avatar
con committed
266 267 268 269
                } else {
                    reader.raiseError(QString::fromLatin1("Unknown element <%1> as subelement of <%2>").arg(
                                          reader.qualifiedName().toString(), QLatin1String(kExecutable)));
                    break;
270 271 272 273 274 275 276 277 278 279 280 281 282 283
                }
            }
        } else {
            reader.raiseError(QString::fromLatin1("Unknown element <%1>").arg(reader.qualifiedName().toString()));
        }
    }
    if (reader.hasError()) {
        if (errorMessage)
            *errorMessage = reader.errorString();
        delete tool;
        return 0;
    }
    return tool;
}
284

con's avatar
con committed
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
// #pragma mark -- ExternalToolRunner

ExternalToolRunner::ExternalToolRunner(const ExternalTool *tool)
    : m_tool(tool),
      m_process(0),
      m_outputCodec(QTextCodec::codecForLocale())
{
    run();
}

bool ExternalToolRunner::resolve()
{
    if (!m_tool)
        return false;
    m_resolvedExecutable = QString::null;
    m_resolvedArguments.clear();
    m_resolvedWorkingDirectory = QString::null;
    { // executable
        foreach (const QString &executable, m_tool->executables()) {
            QString resolved = Utils::expandMacros(executable,
                                                   Core::VariableManager::instance()->macroExpander());
306 307
            m_resolvedExecutable =
                    Utils::Environment::systemEnvironment().searchInPath(resolved);
con's avatar
con committed
308
        }
309
        if (m_resolvedExecutable.isEmpty())
con's avatar
con committed
310 311 312 313 314 315 316 317
            return false;
    }
    { // arguments
        QString resolved = Utils::expandMacros(m_tool->arguments(),
                                               Core::VariableManager::instance()->macroExpander());
        // TODO stupid, do it right
        m_resolvedArguments = resolved.split(QLatin1Char(' '), QString::SkipEmptyParts);
    }
318 319 320 321
    { // input
        m_resolvedInput = Utils::expandMacros(m_tool->input(),
                                              Core::VariableManager::instance()->macroExpander());
    }
con's avatar
con committed
322 323 324 325 326 327 328 329 330 331 332 333 334
    { // working directory
        m_resolvedWorkingDirectory = Utils::expandMacros(m_tool->workingDirectory(),
                                               Core::VariableManager::instance()->macroExpander());
    }
    return true;
}

void ExternalToolRunner::run()
{
    if (!resolve()) {
        deleteLater();
        return;
    }
335 336 337 338
    if (m_tool->outputHandling() == ExternalTool::ReloadDocument
               || m_tool->errorHandling() == ExternalTool::ReloadDocument) {
        // TODO ask modified file to save, block modification notifications
    }
con's avatar
con committed
339 340
    m_process = new QProcess;
    // TODO error handling, finish reporting, reading output, etc
341
    connect(m_process, SIGNAL(started()), this, SLOT(started()));
con's avatar
con committed
342 343 344 345 346 347
    connect(m_process, SIGNAL(finished(int)), this, SLOT(finished()));
    connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError)));
    connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(readStandardOutput()));
    connect(m_process, SIGNAL(readyReadStandardError()), this, SLOT(readStandardError()));
    if (!m_resolvedWorkingDirectory.isEmpty())
        m_process->setWorkingDirectory(m_resolvedWorkingDirectory);
348 349 350 351 352 353 354 355 356 357 358
    ICore::instance()->messageManager()->printToOutputPane(
                tr("Starting external tool '%1'").arg(m_resolvedExecutable), false);
    m_process->start(m_resolvedExecutable, m_resolvedArguments, QIODevice::ReadWrite);
}

void ExternalToolRunner::started()
{
    if (!m_resolvedInput.isEmpty()) {
        m_process->write(m_resolvedInput.toLocal8Bit());
    }
    m_process->closeWriteChannel();
con's avatar
con committed
359 360 361 362
}

void ExternalToolRunner::finished()
{
363 364 365
    if (m_tool->outputHandling() == ExternalTool::ReplaceSelection
            || m_tool->errorHandling() == ExternalTool::ReplaceSelection) {
        emit ExternalToolManager::instance()->replaceSelectionRequested(m_processOutput);
366 367 368
    } else if (m_tool->outputHandling() == ExternalTool::ReloadDocument
               || m_tool->errorHandling() == ExternalTool::ReloadDocument) {
        // TODO reload document without popup
369
    }
370 371
    ICore::instance()->messageManager()->printToOutputPane(
                tr("'%1' finished").arg(m_resolvedExecutable), false);
con's avatar
con committed
372
    // TODO handle the ReplaceSelection and ReloadDocument flags
con's avatar
con committed
373
    m_process->deleteLater();
con's avatar
con committed
374 375 376 377 378 379
    deleteLater();
}

void ExternalToolRunner::error(QProcess::ProcessError error)
{
    // TODO inform about errors
con's avatar
con committed
380
    m_process->deleteLater();
con's avatar
con committed
381 382 383 384 385
    deleteLater();
}

void ExternalToolRunner::readStandardOutput()
{
con's avatar
con committed
386 387
    if (m_tool->outputHandling() == ExternalTool::Ignore)
        return;
con's avatar
con committed
388 389 390 391 392
    QByteArray data = m_process->readAllStandardOutput();
    QString output = m_outputCodec->toUnicode(data.constData(), data.length(), &m_outputCodecState);
    // TODO handle the ReplaceSelection flag
    if (m_tool->outputHandling() == ExternalTool::ShowInPane) {
        ICore::instance()->messageManager()->printToOutputPane(output, true);
393 394
    } else if (m_tool->outputHandling() == ExternalTool::ReplaceSelection) {
        m_processOutput.append(output);
con's avatar
con committed
395 396 397 398 399
    }
}

void ExternalToolRunner::readStandardError()
{
con's avatar
con committed
400 401
    if (m_tool->errorHandling() == ExternalTool::Ignore)
        return;
con's avatar
con committed
402 403 404
    QByteArray data = m_process->readAllStandardError();
    QString output = m_outputCodec->toUnicode(data.constData(), data.length(), &m_errorCodecState);
    // TODO handle the ReplaceSelection flag
con's avatar
con committed
405
    if (m_tool->errorHandling() == ExternalTool::ShowInPane) {
con's avatar
con committed
406
        ICore::instance()->messageManager()->printToOutputPane(output, true);
407 408
    } else if (m_tool->errorHandling() == ExternalTool::ReplaceSelection) {
        m_processOutput.append(output);
con's avatar
con committed
409 410 411
    }
}

412 413
// #pragma mark -- ExternalToolManager

414 415
ExternalToolManager *ExternalToolManager::m_instance = 0;

416 417 418
ExternalToolManager::ExternalToolManager(Core::ICore *core)
    : QObject(core), m_core(core)
{
419
    m_instance = this;
420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465
    initialize();
}

ExternalToolManager::~ExternalToolManager()
{
    qDeleteAll(m_tools);
}

void ExternalToolManager::initialize()
{
    ActionManager *am = m_core->actionManager();
    ActionContainer *mexternaltools = am->createMenu(Id(Constants::M_TOOLS_EXTERNAL));
    mexternaltools->menu()->setTitle(tr("External"));
    ActionContainer *mtools = am->actionContainer(Constants::M_TOOLS);
    Command *cmd;

    QAction *sep = new QAction(this);
    sep->setSeparator(true);
    cmd = am->registerAction(sep, Id("Tools.Separator"), Context(Constants::C_GLOBAL));
    mtools->addAction(cmd, Constants::G_DEFAULT_THREE);
    mtools->addMenu(mexternaltools, Constants::G_DEFAULT_THREE);

    QMap<QString, ActionContainer *> categoryMenus;
    QDir dir(m_core->resourcePath() + QLatin1String("/externaltools"),
             QLatin1String("*.xml"), QDir::Unsorted, QDir::Files | QDir::Readable);
    foreach (const QFileInfo &info, dir.entryInfoList()) {
        QFile file(info.absoluteFilePath());
        if (file.open(QIODevice::ReadOnly)) {
            const QByteArray &bytes = file.readAll();
            file.close();
            QString error;
            ExternalTool *tool = ExternalTool::createFromXml(bytes, &error, m_core->userInterfaceLanguage());
            if (!tool) {
                // TODO error handling
                qDebug() << tr("Error while parsing external tool %1: %2").arg(file.fileName(), error);
                continue;
            }
            if (m_tools.contains(tool->id())) {
                // TODO error handling
                qDebug() << tr("Error: External tool in %1 has duplicate id").arg(file.fileName());
                delete tool;
                continue;
            }
            m_tools.insert(tool->id(), tool);

            // category menus
con's avatar
con committed
466
            // TODO sort alphabetically
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484
            ActionContainer *container;
            if (tool->displayCategory().isEmpty())
                container = mexternaltools;
            else
                container = categoryMenus.value(tool->displayCategory());
            if (!container) {
                container = am->createMenu(Id("Tools.External.Category." + tool->displayCategory()));
                container->menu()->setTitle(tool->displayCategory());
                mexternaltools->addMenu(container, Constants::G_DEFAULT_ONE);
            }

            // tool action
            QAction *action = new QAction(tool->displayName(), this);
            action->setToolTip(tool->description());
            action->setWhatsThis(tool->description());
            action->setData(tool->id());
            cmd = am->registerAction(action, Id("Tools.External." + tool->id()), Context(Constants::C_GLOBAL));
            container->addAction(cmd, Constants::G_DEFAULT_TWO);
con's avatar
con committed
485
            connect(action, SIGNAL(triggered()), this, SLOT(menuActivated()));
486 487 488 489 490 491
        }
    }
}

void ExternalToolManager::menuActivated()
{
con's avatar
con committed
492 493 494 495 496
    QAction *action = qobject_cast<QAction *>(sender());
    QTC_ASSERT(action, return);
    ExternalTool *tool = m_tools.value(action->data().toString());
    QTC_ASSERT(tool, return);
    new ExternalToolRunner(tool);
497
}