1// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd.
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5#include "outputpane.h"
6#include "common/common.h"
7
8#include <QScrollBar>
9#include <QMenu>
10#include <QDebug>
11
12/**
13 * @brief Output text color.
14 */
15const QColor kTextColorNormal(150, 150, 150);
16const QColor kErrorMessageTextColor(255, 108, 108);
17const QColor kMessageOutput(0, 135, 135);
18
19class OutputPanePrivate
20{
21public:
22 explicit OutputPanePrivate(QTextDocument *document)
23 : cursor(document)
24 {
25 }
26
27 ~OutputPanePrivate()
28 {
29 }
30
31 bool enforceNewline = false;
32 bool scrollToBottom = true;
33 int maxCharCount = default_max_char_count();
34 QTextCursor cursor;
35 QMenu *menu = nullptr;
36};
37
38OutputPane::OutputPane(QWidget *parent)
39 : QPlainTextEdit(parent)
40 , d(new OutputPanePrivate(document()))
41{
42 setReadOnly(true);
43}
44
45OutputPane::~OutputPane()
46{
47 if (d) {
48 delete d;
49 d = nullptr;
50 }
51}
52
53void OutputPane::clearContents()
54{
55 clear();
56}
57
58QString OutputPane::normalizeNewlines(const QString &text)
59{
60 QString res = text;
61 res.replace(QLatin1String("\r\n"), QLatin1String("\n"));
62 return res;
63}
64
65bool OutputPane::isScrollbarAtBottom() const
66{
67 return verticalScrollBar()->value() == verticalScrollBar()->maximum();
68}
69
70QString OutputPane::doNewlineEnforcement(const QString &out)
71{
72 d->scrollToBottom = true;
73 QString s = out;
74 if (d->enforceNewline) {
75 s.prepend(QLatin1Char('\n'));
76 d->enforceNewline = false;
77 }
78
79 if (s.endsWith(QLatin1Char('\n'))) {
80 d->enforceNewline = true; // make appendOutputInline put in a newline next time
81 s.chop(1);
82 }
83
84 return s;
85}
86
87void OutputPane::scrollToBottom()
88{
89 verticalScrollBar()->setValue(verticalScrollBar()->maximum());
90 // QPlainTextEdit destroys the first calls value in case of multiline
91 // text, so make sure that the scroll bar actually gets the value set.
92 // Is a noop if the first call succeeded.
93 verticalScrollBar()->setValue(verticalScrollBar()->maximum());
94}
95
96void OutputPane::appendCustomText(const QString &textIn, AppendMode mode, const QTextCharFormat &format)
97{
98 if (d->maxCharCount > 0 && document()->characterCount() >= d->maxCharCount) {
99 qDebug() << "Maximum limit exceeded : " << d->maxCharCount;
100 return;
101 }
102 if (!d->cursor.atEnd())
103 d->cursor.movePosition(QTextCursor::End);
104
105 if (mode == OverWrite) {
106 d->cursor.select(QTextCursor::LineUnderCursor);
107 d->cursor.removeSelectedText();
108 }
109
110 d->cursor.beginEditBlock();
111 auto text = mode == OverWrite ? textIn.trimmed() : normalizeNewlines(doNewlineEnforcement(textIn));
112 d->cursor.insertText(text, format);
113
114 if (d->maxCharCount > 0 && document()->characterCount() >= d->maxCharCount) {
115 QTextCharFormat tmp;
116 tmp.setFontWeight(QFont::Bold);
117 d->cursor.insertText(doNewlineEnforcement(tr("Additional output omitted") + QLatin1Char('\n')), tmp);
118 }
119 d->cursor.endEditBlock();
120
121 scrollToBottom();
122}
123
124void OutputPane::appendText(const QString &text, OutputFormat format, AppendMode mode)
125{
126 QTextCharFormat textFormat;
127 switch (format) {
128 case OutputFormat::StdOut:
129 textFormat.setForeground(kTextColorNormal);
130 textFormat.setFontWeight(QFont::Normal);
131 break;
132 case OutputFormat::StdErr:
133 textFormat.setForeground(kErrorMessageTextColor);
134 textFormat.setFontWeight(QFont::Normal);
135 break;
136 case OutputFormat::NormalMessage:
137 textFormat.setForeground(kMessageOutput);
138 break;
139 case OutputFormat::ErrorMessage:
140 textFormat.setForeground(kErrorMessageTextColor);
141 textFormat.setFontWeight(QFont::Bold);
142 break;
143 default:
144 textFormat.setForeground(kTextColorNormal);
145 textFormat.setFontWeight(QFont::Normal);
146 }
147
148 appendCustomText(text, mode, textFormat);
149}
150
151OutputPane *OutputPane::instance()
152{
153 static OutputPane *ins = new OutputPane();
154 return ins;
155}
156
157void OutputPane::contextMenuEvent(QContextMenuEvent * event)
158{
159 if (nullptr == d->menu) {
160 d->menu = new QMenu(this);
161 d->menu->setParent(this);
162 d->menu->addActions(actionFactory());
163 }
164
165 d->menu->move(event->globalX(), event->globalY());
166 d->menu->show();
167}
168
169QList<QAction*> OutputPane::actionFactory()
170{
171 QList<QAction*> list;
172
173 {
174 auto action = new QAction(this);
175 action->setText(tr("Copy"));
176 connect(action, &QAction::triggered, [this](){
177 if (!document()->toPlainText().isEmpty())
178 copy();
179 });
180 list.append(action);
181 }
182
183 {
184 auto action = new QAction(this);
185 action->setText(tr("Clear"));
186 connect(action, &QAction::triggered, [this](){
187 if (!document()->toPlainText().isEmpty())
188 clear();
189 });
190 list.append(action);
191 }
192
193 {
194 auto action = new QAction(this);
195 action->setText(tr("Select All"));
196 connect(action, &QAction::triggered, [this](){
197 if (!document()->toPlainText().isEmpty())
198 selectAll();
199 });
200 list.append(action);
201 }
202
203 return list;
204}
205