outputwindow.cpp 11.8 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
2
**
Eike Ziller's avatar
Eike Ziller committed
3
4
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
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
27
28
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
29
****************************************************************************/
30
31
32

#include "outputwindow.h"

33
34
35
#include "actionmanager/actionmanager.h"
#include "coreconstants.h"
#include "icore.h"
36

Orgad Shaneh's avatar
Orgad Shaneh committed
37
#include <utils/outputformatter.h>
38
39
#include <utils/synchronousprocess.h>

40
41
#include <QAction>
#include <QScrollBar>
42
43
44

using namespace Utils;

45
namespace Core {
46

Orgad Shaneh's avatar
Orgad Shaneh committed
47
48
49
50
51
namespace Internal {

class OutputWindowPrivate
{
public:
52
    OutputWindowPrivate(QTextDocument *document)
Orgad Shaneh's avatar
Orgad Shaneh committed
53
54
55
56
57
58
59
        : outputWindowContext(0)
        , formatter(0)
        , enforceNewline(false)
        , scrollToBottom(false)
        , linksActive(true)
        , mousePressed(false)
        , maxLineCount(100000)
60
        , cursor(document)
Orgad Shaneh's avatar
Orgad Shaneh committed
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
    {
    }

    ~OutputWindowPrivate()
    {
        ICore::removeContextObject(outputWindowContext);
        delete outputWindowContext;
    }

    IContext *outputWindowContext;
    Utils::OutputFormatter *formatter;

    bool enforceNewline;
    bool scrollToBottom;
    bool linksActive;
    bool mousePressed;
    int maxLineCount;
78
    QTextCursor cursor;
Orgad Shaneh's avatar
Orgad Shaneh committed
79
80
81
82
};

} // namespace Internal

83
84
/*******************/

Orgad Shaneh's avatar
Orgad Shaneh committed
85
OutputWindow::OutputWindow(Context context, QWidget *parent)
86
    : QPlainTextEdit(parent)
87
    , d(new Internal::OutputWindowPrivate(document()))
88
89
90
91
92
{
    setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
    //setCenterOnScroll(false);
    setFrameShape(QFrame::NoFrame);
    setMouseTracking(true);
hjk's avatar
hjk committed
93
    setUndoRedoEnabled(false);
94

Orgad Shaneh's avatar
Orgad Shaneh committed
95
96
97
98
    d->outputWindowContext = new IContext;
    d->outputWindowContext->setContext(context);
    d->outputWindowContext->setWidget(this);
    ICore::addContextObject(d->outputWindowContext);
99
100
101
102
103
104
105
106

    QAction *undoAction = new QAction(this);
    QAction *redoAction = new QAction(this);
    QAction *cutAction = new QAction(this);
    QAction *copyAction = new QAction(this);
    QAction *pasteAction = new QAction(this);
    QAction *selectAllAction = new QAction(this);

Orgad Shaneh's avatar
Orgad Shaneh committed
107
108
109
110
111
112
    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);
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129

    connect(undoAction, SIGNAL(triggered()), this, SLOT(undo()));
    connect(redoAction, SIGNAL(triggered()), this, SLOT(redo()));
    connect(cutAction, SIGNAL(triggered()), this, SLOT(cut()));
    connect(copyAction, SIGNAL(triggered()), this, SLOT(copy()));
    connect(pasteAction, SIGNAL(triggered()), this, SLOT(paste()));
    connect(selectAllAction, SIGNAL(triggered()), this, SLOT(selectAll()));

    connect(this, SIGNAL(undoAvailable(bool)), undoAction, SLOT(setEnabled(bool)));
    connect(this, SIGNAL(redoAvailable(bool)), redoAction, SLOT(setEnabled(bool)));
    connect(this, SIGNAL(copyAvailable(bool)), cutAction, SLOT(setEnabled(bool)));  // OutputWindow never read-only
    connect(this, SIGNAL(copyAvailable(bool)), copyAction, SLOT(setEnabled(bool)));

    undoAction->setEnabled(false);
    redoAction->setEnabled(false);
    cutAction->setEnabled(false);
    copyAction->setEnabled(false);
Daniel Teske's avatar
Daniel Teske committed
130
131
132
133
134
135

    m_scrollTimer.setInterval(10);
    m_scrollTimer.setSingleShot(true);
    connect(&m_scrollTimer, &QTimer::timeout,
            this, &OutputWindow::scrollToBottom);
    m_lastMessage.start();
136
137
138
139
}

OutputWindow::~OutputWindow()
{
Orgad Shaneh's avatar
Orgad Shaneh committed
140
    delete d;
141
142
143
144
}

void OutputWindow::mousePressEvent(QMouseEvent * e)
{
Orgad Shaneh's avatar
Orgad Shaneh committed
145
    d->mousePressed = true;
146
147
148
149
150
    QPlainTextEdit::mousePressEvent(e);
}

void OutputWindow::mouseReleaseEvent(QMouseEvent *e)
{
Orgad Shaneh's avatar
Orgad Shaneh committed
151
    d->mousePressed = false;
152

Orgad Shaneh's avatar
Orgad Shaneh committed
153
    if (d->linksActive) {
154
        const QString href = anchorAt(e->pos());
Orgad Shaneh's avatar
Orgad Shaneh committed
155
156
        if (d->formatter)
            d->formatter->handleLink(href);
157
158
159
    }

    // Mouse was released, activate links again
Orgad Shaneh's avatar
Orgad Shaneh committed
160
    d->linksActive = true;
161
162
163
164
165
166
167

    QPlainTextEdit::mouseReleaseEvent(e);
}

void OutputWindow::mouseMoveEvent(QMouseEvent *e)
{
    // Cursor was dragged to make a selection, deactivate links
Orgad Shaneh's avatar
Orgad Shaneh committed
168
169
    if (d->mousePressed && textCursor().hasSelection())
        d->linksActive = false;
170

Orgad Shaneh's avatar
Orgad Shaneh committed
171
    if (!d->linksActive || anchorAt(e->pos()).isEmpty())
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
        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
{
Orgad Shaneh's avatar
Orgad Shaneh committed
201
    return d->formatter;
202
203
204
205
}

void OutputWindow::setFormatter(OutputFormatter *formatter)
{
Orgad Shaneh's avatar
Orgad Shaneh committed
206
207
    d->formatter = formatter;
    d->formatter->setPlainTextEdit(this);
208
209
210
211
212
}

void OutputWindow::showEvent(QShowEvent *e)
{
    QPlainTextEdit::showEvent(e);
Orgad Shaneh's avatar
Orgad Shaneh committed
213
    if (d->scrollToBottom)
214
        verticalScrollBar()->setValue(verticalScrollBar()->maximum());
Orgad Shaneh's avatar
Orgad Shaneh committed
215
    d->scrollToBottom = false;
216
217
}

218
QString OutputWindow::doNewlineEnforcement(const QString &out)
219
{
Orgad Shaneh's avatar
Orgad Shaneh committed
220
    d->scrollToBottom = true;
221
    QString s = out;
Orgad Shaneh's avatar
Orgad Shaneh committed
222
    if (d->enforceNewline) {
223
        s.prepend(QLatin1Char('\n'));
Orgad Shaneh's avatar
Orgad Shaneh committed
224
        d->enforceNewline = false;
225
226
227
    }

    if (s.endsWith(QLatin1Char('\n'))) {
Orgad Shaneh's avatar
Orgad Shaneh committed
228
        d->enforceNewline = true; // make appendOutputInline put in a newline next time
229
230
231
232
233
234
        s.chop(1);
    }

    return s;
}

235
236
void OutputWindow::setMaxLineCount(int count)
{
Orgad Shaneh's avatar
Orgad Shaneh committed
237
238
239
240
241
242
243
    d->maxLineCount = count;
    setMaximumBlockCount(d->maxLineCount);
}

int OutputWindow::maxLineCount() const
{
    return d->maxLineCount;
244
245
}

246
247
void OutputWindow::appendMessage(const QString &output, OutputFormat format)
{
248
    const QString out = SynchronousProcess::normalizeNewlines(output);
Orgad Shaneh's avatar
Orgad Shaneh committed
249
    setMaximumBlockCount(d->maxLineCount);
Daniel Teske's avatar
Daniel Teske committed
250
    const bool atBottom = isScrollbarAtBottom() || m_scrollTimer.isActive();
251
252
253

    if (format == ErrorMessageFormat || format == NormalMessageFormat) {

Orgad Shaneh's avatar
Orgad Shaneh committed
254
        d->formatter->appendMessage(doNewlineEnforcement(out), format);
255
256
257
258
259
260
261

    } else {

        bool sameLine = format == StdOutFormatSameLine
                     || format == StdErrFormatSameLine;

        if (sameLine) {
Orgad Shaneh's avatar
Orgad Shaneh committed
262
            d->scrollToBottom = true;
263
264

            int newline = -1;
Orgad Shaneh's avatar
Orgad Shaneh committed
265
266
            bool enforceNewline = d->enforceNewline;
            d->enforceNewline = false;
267
268
269
270
271

            if (!enforceNewline) {
                newline = out.indexOf(QLatin1Char('\n'));
                moveCursor(QTextCursor::End);
                if (newline != -1)
Orgad Shaneh's avatar
Orgad Shaneh committed
272
                    d->formatter->appendMessage(out.left(newline), format);// doesn't enforce new paragraph like appendPlainText
273
274
275
276
            }

            QString s = out.mid(newline+1);
            if (s.isEmpty()) {
Orgad Shaneh's avatar
Orgad Shaneh committed
277
                d->enforceNewline = true;
278
279
            } else {
                if (s.endsWith(QLatin1Char('\n'))) {
Orgad Shaneh's avatar
Orgad Shaneh committed
280
                    d->enforceNewline = true;
281
282
                    s.chop(1);
                }
Orgad Shaneh's avatar
Orgad Shaneh committed
283
                d->formatter->appendMessage(QLatin1Char('\n') + s, format);
284
285
            }
        } else {
Orgad Shaneh's avatar
Orgad Shaneh committed
286
            d->formatter->appendMessage(doNewlineEnforcement(out), format);
287
288
289
        }
    }

Daniel Teske's avatar
Daniel Teske committed
290
291
292
293
294
295
296
297
298
299
    if (atBottom) {
        if (m_lastMessage.elapsed() < 5) {
            m_scrollTimer.start();
        } else {
            m_scrollTimer.stop();
            scrollToBottom();
        }
    }

    m_lastMessage.start();
300
301
302
303
    enableUndoRedo();
}

// TODO rename
304
void OutputWindow::appendText(const QString &textIn, const QTextCharFormat &format)
305
{
306
    const QString text = SynchronousProcess::normalizeNewlines(textIn);
Orgad Shaneh's avatar
Orgad Shaneh committed
307
    if (d->maxLineCount > 0 && document()->blockCount() >= d->maxLineCount)
308
309
        return;
    const bool atBottom = isScrollbarAtBottom();
310
311
312
313
    if (!d->cursor.atEnd())
        d->cursor.movePosition(QTextCursor::End);
    d->cursor.beginEditBlock();
    d->cursor.insertText(doNewlineEnforcement(text), format);
314

Orgad Shaneh's avatar
Orgad Shaneh committed
315
    if (d->maxLineCount > 0 && document()->blockCount() >= d->maxLineCount) {
316
317
        QTextCharFormat tmp;
        tmp.setFontWeight(QFont::Bold);
318
        d->cursor.insertText(doNewlineEnforcement(tr("Additional output omitted") + QLatin1Char('\n')), tmp);
319
320
    }

321
    d->cursor.endEditBlock();
322
323
324
325
326
327
328
329
330
331
332
    if (atBottom)
        scrollToBottom();
}

bool OutputWindow::isScrollbarAtBottom() const
{
    return verticalScrollBar()->value() == verticalScrollBar()->maximum();
}

void OutputWindow::clear()
{
Orgad Shaneh's avatar
Orgad Shaneh committed
333
    d->enforceNewline = false;
334
335
336
337
338
339
    QPlainTextEdit::clear();
}

void OutputWindow::scrollToBottom()
{
    verticalScrollBar()->setValue(verticalScrollBar()->maximum());
340
341
342
343
    // 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());
344
345
346
347
}

void OutputWindow::grayOutOldContent()
{
348
349
350
    if (!d->cursor.atEnd())
        d->cursor.movePosition(QTextCursor::End);
    QTextCharFormat endFormat = d->cursor.charFormat();
351

352
    d->cursor.select(QTextCursor::Document);
353
354
355
356
357
358
359
360
361

    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()) ));
362
    d->cursor.mergeCharFormat(format);
363

364
    d->cursor.movePosition(QTextCursor::End);
365
366
    d->cursor.setCharFormat(endFormat);
    d->cursor.insertBlock(QTextBlockFormat());
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
}

void OutputWindow::enableUndoRedo()
{
    setMaximumBlockCount(0);
    setUndoRedoEnabled(true);
}

void OutputWindow::setWordWrapEnabled(bool wrap)
{
    if (wrap)
        setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
    else
        setWordWrapMode(QTextOption::NoWrap);
}

383
} // namespace Core