| 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 | */ |
| 15 | const QColor kTextColorNormal(150, 150, 150); |
| 16 | const QColor kErrorMessageTextColor(255, 108, 108); |
| 17 | const QColor kMessageOutput(0, 135, 135); |
| 18 | |
| 19 | class OutputPanePrivate |
| 20 | { |
| 21 | public: |
| 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 * = nullptr; |
| 36 | }; |
| 37 | |
| 38 | OutputPane::OutputPane(QWidget *parent) |
| 39 | : QPlainTextEdit(parent) |
| 40 | , d(new OutputPanePrivate(document())) |
| 41 | { |
| 42 | setReadOnly(true); |
| 43 | } |
| 44 | |
| 45 | OutputPane::~OutputPane() |
| 46 | { |
| 47 | if (d) { |
| 48 | delete d; |
| 49 | d = nullptr; |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | void OutputPane::clearContents() |
| 54 | { |
| 55 | clear(); |
| 56 | } |
| 57 | |
| 58 | QString OutputPane::normalizeNewlines(const QString &text) |
| 59 | { |
| 60 | QString res = text; |
| 61 | res.replace(QLatin1String("\r\n" ), QLatin1String("\n" )); |
| 62 | return res; |
| 63 | } |
| 64 | |
| 65 | bool OutputPane::isScrollbarAtBottom() const |
| 66 | { |
| 67 | return verticalScrollBar()->value() == verticalScrollBar()->maximum(); |
| 68 | } |
| 69 | |
| 70 | QString 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 | |
| 87 | void 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 | |
| 96 | void 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 | |
| 124 | void 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 | |
| 151 | OutputPane *OutputPane::instance() |
| 152 | { |
| 153 | static OutputPane *ins = new OutputPane(); |
| 154 | return ins; |
| 155 | } |
| 156 | |
| 157 | void OutputPane::(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 | |
| 169 | QList<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 | |