Files
qt-creator/src/plugins/coreplugin/outputwindow.cpp

599 lines
19 KiB
C++
Raw Normal View History

/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** 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 The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "outputwindow.h"
#include "actionmanager/actionmanager.h"
#include "coreconstants.h"
#include "icore.h"
#include <utils/outputformatter.h>
#include <utils/synchronousprocess.h>
#include <QAction>
#include <QCursor>
#include <QMimeData>
#include <QPointer>
#include <QRegularExpression>
#include <QScrollBar>
#include <QTextBlock>
using namespace Utils;
namespace Core {
namespace Internal {
class OutputWindowPrivate
{
public:
OutputWindowPrivate(QTextDocument *document)
: cursor(document)
{
}
~OutputWindowPrivate()
{
ICore::removeContextObject(outputWindowContext);
delete outputWindowContext;
}
IContext *outputWindowContext = nullptr;
QPointer<Utils::OutputFormatter> formatter;
QString settingsKey;
bool enforceNewline = false;
bool prependCarriageReturn = false;
bool scrollToBottom = true;
bool linksActive = true;
bool zoomEnabled = false;
float originalFontSize = 0.;
bool originalReadOnly = false;
int maxCharCount = Core::Constants::DEFAULT_MAX_CHAR_COUNT;
Qt::MouseButton mouseButtonPressed = Qt::NoButton;
QTextCursor cursor;
QString filterText;
int lastFilteredBlockNumber = -1;
QPalette originalPalette;
OutputWindow::FilterModeFlags filterMode = OutputWindow::FilterModeFlag::Default;
};
} // namespace Internal
/*******************/
OutputWindow::OutputWindow(Context context, const QString &settingsKey, QWidget *parent)
: QPlainTextEdit(parent)
, d(new Internal::OutputWindowPrivate(document()))
{
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
//setCenterOnScroll(false);
setFrameShape(QFrame::NoFrame);
setMouseTracking(true);
setUndoRedoEnabled(false);
d->settingsKey = settingsKey;
d->outputWindowContext = new IContext;
d->outputWindowContext->setContext(context);
d->outputWindowContext->setWidget(this);
ICore::addContextObject(d->outputWindowContext);
auto undoAction = new QAction(this);
auto redoAction = new QAction(this);
auto cutAction = new QAction(this);
auto copyAction = new QAction(this);
auto pasteAction = new QAction(this);
auto selectAllAction = new QAction(this);
ActionManager::registerAction(undoAction, Constants::UNDO, context);
ActionManager::registerAction(redoAction, Constants::REDO, context);
ActionManager::registerAction(cutAction, Constants::CUT, context);
ActionManager::registerAction(copyAction, Constants::COPY, context);
ActionManager::registerAction(pasteAction, Constants::PASTE, context);
ActionManager::registerAction(selectAllAction, Constants::SELECTALL, context);
connect(undoAction, &QAction::triggered, this, &QPlainTextEdit::undo);
connect(redoAction, &QAction::triggered, this, &QPlainTextEdit::redo);
connect(cutAction, &QAction::triggered, this, &QPlainTextEdit::cut);
connect(copyAction, &QAction::triggered, this, &QPlainTextEdit::copy);
connect(pasteAction, &QAction::triggered, this, &QPlainTextEdit::paste);
connect(selectAllAction, &QAction::triggered, this, &QPlainTextEdit::selectAll);
connect(this, &QPlainTextEdit::blockCountChanged, this, [this] {
if (!d->filterText.isEmpty())
filterNewContent();
});
connect(this, &QPlainTextEdit::undoAvailable, undoAction, &QAction::setEnabled);
connect(this, &QPlainTextEdit::redoAvailable, redoAction, &QAction::setEnabled);
connect(this, &QPlainTextEdit::copyAvailable, cutAction, &QAction::setEnabled); // OutputWindow never read-only
connect(this, &QPlainTextEdit::copyAvailable, copyAction, &QAction::setEnabled);
connect(Core::ICore::instance(), &Core::ICore::saveSettingsRequested, this, [this] {
if (!d->settingsKey.isEmpty())
Core::ICore::settings()->setValue(d->settingsKey, fontZoom());
});
undoAction->setEnabled(false);
redoAction->setEnabled(false);
cutAction->setEnabled(false);
copyAction->setEnabled(false);
m_scrollTimer.setInterval(10);
m_scrollTimer.setSingleShot(true);
connect(&m_scrollTimer, &QTimer::timeout,
this, &OutputWindow::scrollToBottom);
m_lastMessage.start();
d->originalFontSize = font().pointSizeF();
if (!d->settingsKey.isEmpty()) {
float zoom = Core::ICore::settings()->value(d->settingsKey).toFloat();
setFontZoom(zoom);
}
}
OutputWindow::~OutputWindow()
{
delete d;
}
void OutputWindow::mousePressEvent(QMouseEvent * e)
{
d->mouseButtonPressed = e->button();
QPlainTextEdit::mousePressEvent(e);
}
void OutputWindow::mouseReleaseEvent(QMouseEvent *e)
{
if (d->linksActive && d->mouseButtonPressed == Qt::LeftButton) {
const QString href = anchorAt(e->pos());
if (d->formatter)
d->formatter->handleLink(href);
}
// Mouse was released, activate links again
d->linksActive = true;
d->mouseButtonPressed = Qt::NoButton;
QPlainTextEdit::mouseReleaseEvent(e);
}
void OutputWindow::mouseMoveEvent(QMouseEvent *e)
{
// Cursor was dragged to make a selection, deactivate links
if (d->mouseButtonPressed != Qt::NoButton && textCursor().hasSelection())
d->linksActive = false;
if (!d->linksActive || anchorAt(e->pos()).isEmpty())
viewport()->setCursor(Qt::IBeamCursor);
else
viewport()->setCursor(Qt::PointingHandCursor);
QPlainTextEdit::mouseMoveEvent(e);
}
void OutputWindow::resizeEvent(QResizeEvent *e)
{
//Keep scrollbar at bottom of window while resizing, to ensure we keep scrolling
//This can happen if window is resized while building, or if the horizontal scrollbar appears
bool atBottom = isScrollbarAtBottom();
QPlainTextEdit::resizeEvent(e);
if (atBottom)
scrollToBottom();
}
void OutputWindow::keyPressEvent(QKeyEvent *ev)
{
QPlainTextEdit::keyPressEvent(ev);
//Ensure we scroll also on Ctrl+Home or Ctrl+End
if (ev->matches(QKeySequence::MoveToStartOfDocument))
verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMinimum);
else if (ev->matches(QKeySequence::MoveToEndOfDocument))
verticalScrollBar()->triggerAction(QAbstractSlider::SliderToMaximum);
}
OutputFormatter *OutputWindow::formatter() const
{
return d->formatter;
}
void OutputWindow::setFormatter(OutputFormatter *formatter)
{
d->formatter = formatter;
if (d->formatter)
d->formatter->setPlainTextEdit(this);
}
void OutputWindow::showEvent(QShowEvent *e)
{
QPlainTextEdit::showEvent(e);
if (d->scrollToBottom)
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
d->scrollToBottom = false;
}
void OutputWindow::wheelEvent(QWheelEvent *e)
{
if (d->zoomEnabled) {
if (e->modifiers() & Qt::ControlModifier) {
float delta = e->angleDelta().y() / 120.f;
// Workaround for QTCREATORBUG-22721, remove when properly fixed in Qt
const float newSize = float(font().pointSizeF()) + delta;
if (delta < 0.f && newSize < 4.f)
return;
zoomInF(delta);
emit wheelZoom();
return;
}
}
QAbstractScrollArea::wheelEvent(e);
updateMicroFocus();
}
void OutputWindow::setBaseFont(const QFont &newFont)
{
float zoom = fontZoom();
d->originalFontSize = newFont.pointSizeF();
QFont tmp = newFont;
float newZoom = qMax(d->originalFontSize + zoom, 4.0f);
tmp.setPointSizeF(newZoom);
setFont(tmp);
}
float OutputWindow::fontZoom() const
{
return font().pointSizeF() - d->originalFontSize;
}
void OutputWindow::setFontZoom(float zoom)
{
QFont f = font();
if (f.pointSizeF() == d->originalFontSize + zoom)
return;
float newZoom = qMax(d->originalFontSize + zoom, 4.0f);
f.setPointSizeF(newZoom);
setFont(f);
}
void OutputWindow::setWheelZoomEnabled(bool enabled)
{
d->zoomEnabled = enabled;
}
void OutputWindow::updateFilterProperties(
const QString &filterText,
Qt::CaseSensitivity caseSensitivity,
bool isRegexp,
bool isInverted
)
{
FilterModeFlags flags;
flags.setFlag(FilterModeFlag::CaseSensitive, caseSensitivity == Qt::CaseSensitive)
.setFlag(FilterModeFlag::RegExp, isRegexp)
.setFlag(FilterModeFlag::Inverted, isInverted);
if (d->filterMode == flags && d->filterText == filterText)
return;
d->lastFilteredBlockNumber = -1;
if (d->filterText != filterText) {
const bool filterTextWasEmpty = d->filterText.isEmpty();
d->filterText = filterText;
// Update textedit's background color
if (filterText.isEmpty() && !filterTextWasEmpty) {
setPalette(d->originalPalette);
setReadOnly(d->originalReadOnly);
}
if (!filterText.isEmpty() && filterTextWasEmpty) {
d->originalReadOnly = isReadOnly();
setReadOnly(true);
const auto newBgColor = [this] {
const QColor currentColor = palette().color(QPalette::Base);
const int factor = 120;
return currentColor.value() < 128 ? currentColor.lighter(factor)
: currentColor.darker(factor);
};
QPalette p = palette();
p.setColor(QPalette::Base, newBgColor());
setPalette(p);
}
}
d->filterMode = flags;
filterNewContent();
}
void OutputWindow::filterNewContent()
{
bool atBottom = isScrollbarAtBottom();
QTextBlock lastBlock = document()->findBlockByNumber(d->lastFilteredBlockNumber);
if (!lastBlock.isValid())
lastBlock = document()->begin();
const bool invert = d->filterMode.testFlag(FilterModeFlag::Inverted);
if (d->filterMode.testFlag(OutputWindow::FilterModeFlag::RegExp)) {
QRegularExpression regExp(d->filterText);
if (!d->filterMode.testFlag(OutputWindow::FilterModeFlag::CaseSensitive))
regExp.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
for (; lastBlock != document()->end(); lastBlock = lastBlock.next())
lastBlock.setVisible(d->filterText.isEmpty()
|| regExp.match(lastBlock.text()).hasMatch() != invert);
} else {
if (d->filterMode.testFlag(OutputWindow::FilterModeFlag::CaseSensitive)) {
for (; lastBlock != document()->end(); lastBlock = lastBlock.next())
lastBlock.setVisible(d->filterText.isEmpty()
|| lastBlock.text().contains(d->filterText) != invert);
} else {
for (; lastBlock != document()->end(); lastBlock = lastBlock.next()) {
lastBlock.setVisible(d->filterText.isEmpty() || lastBlock.text().toLower()
.contains(d->filterText.toLower()) != invert);
}
}
}
d->lastFilteredBlockNumber = document()->lastBlock().blockNumber();
// FIXME: Why on earth is this necessary? We should probably do something else instead...
setDocument(document());
if (atBottom)
scrollToBottom();
}
QString OutputWindow::doNewlineEnforcement(const QString &out)
{
d->scrollToBottom = true;
QString s = out;
if (d->enforceNewline) {
s.prepend(QLatin1Char('\n'));
d->enforceNewline = false;
}
if (s.endsWith(QLatin1Char('\n'))) {
d->enforceNewline = true; // make appendOutputInline put in a newline next time
s.chop(1);
}
return s;
}
void OutputWindow::setMaxCharCount(int count)
{
d->maxCharCount = count;
setMaximumBlockCount(count / 100);
}
int OutputWindow::maxCharCount() const
{
return d->maxCharCount;
}
void OutputWindow::appendMessage(const QString &output, OutputFormat format)
{
QString out = output;
if (d->prependCarriageReturn) {
d->prependCarriageReturn = false;
out.prepend('\r');
}
out = SynchronousProcess::normalizeNewlines(out);
if (out.endsWith('\r')) {
d->prependCarriageReturn = true;
out.chop(1);
}
if (out.size() > d->maxCharCount) {
// Current line alone exceeds limit, we need to cut it.
out.truncate(d->maxCharCount);
out.append("[...]");
setMaximumBlockCount(1);
} else {
int plannedChars = document()->characterCount() + out.size();
if (plannedChars > d->maxCharCount) {
int plannedBlockCount = document()->blockCount();
QTextBlock tb = document()->firstBlock();
while (tb.isValid() && plannedChars > d->maxCharCount && plannedBlockCount > 1) {
plannedChars -= tb.length();
plannedBlockCount -= 1;
tb = tb.next();
}
setMaximumBlockCount(plannedBlockCount);
} else {
setMaximumBlockCount(-1);
}
}
const bool atBottom = isScrollbarAtBottom() || m_scrollTimer.isActive();
if (format == ErrorMessageFormat || format == NormalMessageFormat) {
if (d->formatter)
d->formatter->appendMessage(doNewlineEnforcement(out), format);
} else {
bool sameLine = format == StdOutFormatSameLine
|| format == StdErrFormatSameLine;
if (sameLine) {
d->scrollToBottom = true;
bool enforceNewline = d->enforceNewline;
d->enforceNewline = false;
if (enforceNewline) {
out.prepend('\n');
} else {
const int newline = out.indexOf(QLatin1Char('\n'));
moveCursor(QTextCursor::End);
if (newline != -1) {
if (d->formatter)
d->formatter->appendMessage(out.left(newline), format);// doesn't enforce new paragraph like appendPlainText
out = out.mid(newline);
}
}
if (out.isEmpty()) {
d->enforceNewline = true;
} else {
if (out.endsWith(QLatin1Char('\n'))) {
d->enforceNewline = true;
out.chop(1);
}
if (d->formatter)
d->formatter->appendMessage(out, format);
}
} else {
if (d->formatter)
d->formatter->appendMessage(doNewlineEnforcement(out), format);
}
}
if (atBottom) {
if (m_lastMessage.elapsed() < 5) {
m_scrollTimer.start();
} else {
m_scrollTimer.stop();
scrollToBottom();
}
}
m_lastMessage.start();
enableUndoRedo();
}
// TODO rename
void OutputWindow::appendText(const QString &textIn, const QTextCharFormat &format)
{
const QString text = SynchronousProcess::normalizeNewlines(textIn);
if (d->maxCharCount > 0 && document()->characterCount() >= d->maxCharCount)
return;
const bool atBottom = isScrollbarAtBottom();
if (!d->cursor.atEnd())
d->cursor.movePosition(QTextCursor::End);
d->cursor.beginEditBlock();
d->cursor.insertText(doNewlineEnforcement(text), format);
if (d->maxCharCount > 0 && document()->characterCount() >= d->maxCharCount) {
QTextCharFormat tmp;
tmp.setFontWeight(QFont::Bold);
d->cursor.insertText(doNewlineEnforcement(tr("Additional output omitted. You can increase "
"the limit in the \"Build & Run\" settings.")
+ QLatin1Char('\n')), tmp);
}
d->cursor.endEditBlock();
if (atBottom)
scrollToBottom();
}
bool OutputWindow::isScrollbarAtBottom() const
{
return verticalScrollBar()->value() == verticalScrollBar()->maximum();
}
QMimeData *OutputWindow::createMimeDataFromSelection() const
{
const auto mimeData = new QMimeData;
QString content;
const int selStart = textCursor().selectionStart();
const int selEnd = textCursor().selectionEnd();
const QTextBlock firstBlock = document()->findBlock(selStart);
const QTextBlock lastBlock = document()->findBlock(selEnd);
for (QTextBlock curBlock = firstBlock; curBlock != lastBlock; curBlock = curBlock.next()) {
if (!curBlock.isVisible())
continue;
if (curBlock == firstBlock)
content += curBlock.text().mid(selStart - firstBlock.position());
else
content += curBlock.text();
content += '\n';
}
if (lastBlock.isValid() && lastBlock.isVisible()) {
if (firstBlock == lastBlock)
content = textCursor().selectedText();
else
content += lastBlock.text().mid(0, selEnd - lastBlock.position());
}
mimeData->setText(content);
return mimeData;
}
void OutputWindow::clear()
{
d->enforceNewline = false;
d->prependCarriageReturn = false;
QPlainTextEdit::clear();
if (d->formatter)
d->formatter->clear();
}
void OutputWindow::scrollToBottom()
{
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
// QPlainTextEdit destroys the first calls value in case of multiline
// text, so make sure that the scroll bar actually gets the value set.
// Is a noop if the first call succeeded.
verticalScrollBar()->setValue(verticalScrollBar()->maximum());
}
void OutputWindow::grayOutOldContent()
{
if (!d->cursor.atEnd())
d->cursor.movePosition(QTextCursor::End);
QTextCharFormat endFormat = d->cursor.charFormat();
d->cursor.select(QTextCursor::Document);
QTextCharFormat format;
const QColor bkgColor = palette().base().color();
const QColor fgdColor = palette().text().color();
double bkgFactor = 0.50;
double fgdFactor = 1.-bkgFactor;
format.setForeground(QColor((bkgFactor * bkgColor.red() + fgdFactor * fgdColor.red()),
(bkgFactor * bkgColor.green() + fgdFactor * fgdColor.green()),
(bkgFactor * bkgColor.blue() + fgdFactor * fgdColor.blue()) ));
d->cursor.mergeCharFormat(format);
d->cursor.movePosition(QTextCursor::End);
d->cursor.setCharFormat(endFormat);
d->cursor.insertBlock(QTextBlockFormat());
}
void OutputWindow::enableUndoRedo()
{
setMaximumBlockCount(0);
setUndoRedoEnabled(true);
}
void OutputWindow::setWordWrapEnabled(bool wrap)
{
if (wrap)
setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
else
setWordWrapMode(QTextOption::NoWrap);
}
} // namespace Core