/* * Copyright (C) OpenTX * * Based on code named * th9x - http://code.google.com/p/th9x * er9x - http://code.google.com/p/er9x * gruvin9x - http://code.google.com/p/gruvin9x * * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License version 2 as * published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. */ #include "debugoutput.h" #include "ui_debugoutput.h" #include "appdebugmessagehandler.h" #include "appdata.h" #include "filteredtextbuffer.h" #include #include #include #include #include #include #define DEBUG_OUTPUT_STATE_VERSION 1 extern AppData g; // ensure what "g" means const quint16 DebugOutput::m_savedViewStateVersion = 1; DebugOutput::DebugOutput(QWidget * parent, SimulatorInterface *simulator): QWidget(parent), ui(new Ui::DebugOutput), m_simulator(simulator), m_dataBufferDevice(NULL), m_radioProfileId(g.sessionId()), m_filterEnable(false), m_filterExclude(false) { ui->setupUi(this); #ifdef __APPLE__ ui->console->setFont(QFont("Courier", 13)); #endif // TODO : allow selecting multiple filters, but needs to be efficient at output stage QStringList stockFilters; stockFilters << "/^(lua[A-Z]|script).*/i"; stockFilters << "/(error|warning|-(E|W)-)/i"; stockFilters << "!^(GC Use|(play|load|write|find(True)?)File|convert(To|From)Simu|\\t(not found|found( in map|\\:))|eeprom |f_[a-z]+\\(|(push|(p|P)op(up)?|chain)? ?Menu( .+ display)?|RamBackup).*$"; foreach (const QString & fltr, stockFilters) ui->filterText->addItem(fltr, "no_delete"); ui->filterText->setValidator(new DebugOutputFilterValidator(ui->filterText)); ui->filterText->installEventFilter(new DeleteComboBoxItemEventFilter(this)); ui->actionShowFilterHelp->setIcon(SimulatorIcon("info")); ui->actionWordWrap->setIcon(SimulatorIcon("word_wrap")); ui->actionClearScr->setIcon(SimulatorIcon("eraser")); ui->btnFilter->setDefaultAction(ui->actionToggleFilter); ui->btnShowFilterHelp->setDefaultAction(ui->actionShowFilterHelp); ui->btnWordWrap->setDefaultAction(ui->actionWordWrap); ui->btnClearScr->setDefaultAction(ui->actionClearScr); m_dataBufferDevice = new FilteredTextBuffer(); m_dataBufferDevice->setDataBufferMaxSize(DEBUG_OUTPUT_WIDGET_OUT_BUFF_SIZE); m_dataBufferDevice->setInputBufferMaxSize(DEBUG_OUTPUT_WIDGET_INP_BUFF_SIZE); m_dataBufferDevice->open(QIODevice::ReadWrite | QIODevice::Text); connect(m_dataBufferDevice, &FilteredTextBuffer::readyRead, this, &DebugOutput::processBytesReceived, Qt::QueuedConnection); connect(m_dataBufferDevice, &FilteredTextBuffer::bufferOverflow, this, &DebugOutput::onDataBufferOverflow, Qt::QueuedConnection); connect(this, &DebugOutput::filterChanged, m_dataBufferDevice, &FilteredTextBuffer::setLineFilter); connect(this, &DebugOutput::filterEnabledChanged, m_dataBufferDevice, &FilteredTextBuffer::setLineFilterEnabled); connect(this, &DebugOutput::filterExprChanged, m_dataBufferDevice, &FilteredTextBuffer::setLineFilterExpr); connect(this, &DebugOutput::filterExclusiveChanged, m_dataBufferDevice, &FilteredTextBuffer::setLineFilterExclusive); restoreState(); ui->bufferSize->setValue(ui->console->maximumBlockCount()); connect(ui->actionToggleFilter, &QAction::toggled, this, &DebugOutput::onFilterToggled); connect(ui->filterText, &QComboBox::currentTextChanged, this, &DebugOutput::onFilterTextChanged); if (AppDebugMessageHandler::instance()) connect(AppDebugMessageHandler::instance(), &AppDebugMessageHandler::messageOutput, this, &DebugOutput::onAppDebugMessage); // send firmware TRACE events to our data collector m_simulator->addTracebackDevice(m_dataBufferDevice); } DebugOutput::~DebugOutput() { if (AppDebugMessageHandler::instance()) disconnect(AppDebugMessageHandler::instance(), 0, this, 0); if (m_dataBufferDevice) { m_simulator->removeTracebackDevice(m_dataBufferDevice); disconnect(m_dataBufferDevice, 0, this, 0); disconnect(this, 0, m_dataBufferDevice, 0); delete m_dataBufferDevice; m_dataBufferDevice = Q_NULLPTR; } saveState(); delete ui; } void DebugOutput::saveState() { QStringList filters; for (int i = 0; i < ui->filterText->count(); ++i) { if (!ui->filterText->itemText(i).isEmpty() && ui->filterText->itemData(i).toString() != "no_delete") filters << ui->filterText->itemText(i); } g.simuDbgFilters(filters); QByteArray state; QDataStream stream(&state, QIODevice::WriteOnly); stream << m_savedViewStateVersion << (qint16)ui->filterText->currentIndex() << (qint32)ui->console->maximumBlockCount() << m_filterEnable << ui->actionWordWrap->isChecked(); SimulatorOptions opts = g.profile[m_radioProfileId].simulatorOptions(); opts.dbgConsoleState = state; g.profile[m_radioProfileId].simulatorOptions(opts); } void DebugOutput::restoreState() { quint16 ver = 0; qint16 fci = -1; qint32 mbc = 10000; bool flten = false, wwen = false; QByteArray state = g.profile[m_radioProfileId].simulatorOptions().dbgConsoleState; QDataStream stream(state); stream >> ver; if (ver && ver <= m_savedViewStateVersion) stream >> fci >> mbc >> flten >> wwen; ui->filterText->insertItems(0, g.simuDbgFilters()); ui->filterText->setCurrentIndex(fci); ui->console->setMaximumBlockCount(mbc); ui->actionWordWrap->setChecked(wwen); onFilterToggled(flten); } void DebugOutput::processBytesReceived() { static char buf[512]; const QTextCursor savedCursor(ui->console->textCursor()); const int sbValue = ui->console->verticalScrollBar()->value(); const bool sbAtBottom = (sbValue == ui->console->verticalScrollBar()->maximum()); qint64 len; QString text; while (m_dataBufferDevice && m_dataBufferDevice->bytesAvailable() > 0) { len = m_dataBufferDevice->read(buf, sizeof(buf)); if (len <= 0) break; text = QString::fromLocal8Bit(buf, len); ui->console->moveCursor(QTextCursor::End); ui->console->textCursor().insertText(text); if (sbAtBottom) { ui->console->moveCursor(QTextCursor::End); ui->console->verticalScrollBar()->setValue(ui->console->verticalScrollBar()->maximum()); } else { ui->console->setTextCursor(savedCursor); ui->console->verticalScrollBar()->setValue(sbValue); } QCoreApplication::processEvents(); } } void DebugOutput::onDataBufferOverflow(const qint64 len) { static QElapsedTimer reportTimer; if (len <= 0) { reportTimer.invalidate(); } else if (!reportTimer.isValid() || reportTimer.elapsed() > 1000 * 30) { qWarning("Data buffer overflow by %lld bytes!", len); reportTimer.start(); } } void DebugOutput::onAppDebugMessage(quint8 level, const QString & msg) { if (level > 0 && m_dataBufferDevice) { m_dataBufferDevice->write(qPrintable(msg % "\n")); } } /* * UI handlers */ void DebugOutput::onFilterStateChanged() { const QString fText = ui->filterText->currentText(); if (fText.isEmpty()) { onFilterToggled(false); return; } QRegularExpression filterRegEx = makeRegEx(fText, &m_filterExclude); if (!m_filterEnable || filterRegEx.isValid()) ui->filterText->setStyleSheet(""); else if (m_filterEnable) ui->filterText->setStyleSheet("background-color: rgba(255, 205, 185, 200);"); if (filterRegEx.isValid()) emit filterChanged(m_filterEnable, m_filterExclude, filterRegEx); else onFilterToggled(false); } void DebugOutput::onFilterTextChanged(const QString &) { onFilterStateChanged(); } void DebugOutput::onFilterToggled(bool enable) { if (enable != m_filterEnable) { m_filterEnable = enable; if (ui->actionToggleFilter->isChecked() != enable) ui->actionToggleFilter->setChecked(enable); if (enable) onFilterStateChanged(); else emit filterEnabledChanged(false); } } void DebugOutput::on_bufferSize_editingFinished() { ui->console->setMaximumBlockCount(ui->bufferSize->value()); } void DebugOutput::on_actionWordWrap_toggled(bool checked) { ui->console->setLineWrapMode(checked ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap); } void DebugOutput::on_actionClearScr_triggered() { ui->console->clear(); } void DebugOutput::on_actionShowFilterHelp_triggered() { // TODO : find some place better for this. QString help = tr( \ "" "

The filter supports two syntax types: basic matching with common wildcards as well as full Perl-style (pcre) Regular Expressions.

" "

By default a filter will only show lines which match (inclusive). To make an exclusive filter which removes matching lines, " "prefix the filter expression with a ! (exclamation mark).

" "

To use Regular Expressions (RegEx), prefix the filter text with a / (slash) or ^ (up caret). " "

    " "
  • Put the / or ^ after the exclusive ! indicator if you're using one.
  • " "
  • By default the match is case-sensitive. To make it insensitive, add the typical /i (slash i) operator at the end of your RegEx.
  • " "
  • If you use a caret (^) to denote a RegEx, it will become part of the Reg. Ex. (that is, matches from start of line).
  • " "
  • If the RegEx is invalid, the filter edit field should show a red border and you will not be able to enable the filter.
  • " "
  • A useful resource for testing REs (with a full reference) can be found at http://www.regexr.com/
  • " "

" "

To use basic matching just type any text." "

    " "
  • Wildcards: * (asterisk) matches zero or more of any character(s), and ? (question mark) matches any single character.
  • " "
  • The match is always case-insensitive.
  • " "
  • The match always starts from the beginning of a log line. To ignore characters at the start, use a leading * wildcard.
  • " "
  • A trailing * is always implied (that is, matches anything to the end of the log line). To avoid this, use a RegEx.
  • " "
  • You can match literal wildcard characters by prefixing them with a \\ (backslash) character (eg. \"foo\\*bar\" matches \"foo*bar\").
  • " "

" "

After editing text, press ENTER or TAB key (or click anywhere outside the box) to update the filter.

" "

To remove an entry from the filter selector list, first choose it, and while in the line editor press Shift-Delete (or Shift-Backspace) key combination. " "The default filters cannot be removed. Up to 50 filters are stored.

" "" ); QMessageBox * msgbox = new QMessageBox(QMessageBox::NoIcon, tr("Debug Console Filter Help"), help, QMessageBox::Ok, this); msgbox->exec(); } // static QRegularExpression DebugOutput::makeRegEx(const QString & input, bool * isExlusive) { QString output(input); QRegularExpression re; QRegularExpression::PatternOptions reFlags = QRegularExpression::DontCaptureOption; #if (QT_VERSION >= QT_VERSION_CHECK(5, 4, 0)) reFlags |= QRegularExpression::OptimizeOnFirstUsageOption; #endif if (input.left(1) == "!") { output.remove(0, 1); if (isExlusive) *isExlusive = true; } else if (isExlusive) { *isExlusive = false; } // regex? if (output.left(1) == "/" || output.left(1) == "^") { if (output.left(1) == "/") output.remove(0, 1); // check for case-insensitive flag at end ("/.../i") if (output.endsWith("/i")) { output.chop(2); reFlags |= QRegularExpression::CaseInsensitiveOption; } else if (output.endsWith("/")) { output.chop(1); } re.setPattern(output); } // no, convert arbitrary string to regex else { output.replace(QRegExp("^\\\\/"), "/"); // remove escape before fwd-slash ("\/...") // escape all special chars except * and ? output.replace(QRegExp("(\\\\|\\.|\\+|\\^|\\$|\\||\\)|\\(|\\]|\\[|\\}|\\{)"), "\\\\1"); output.replace("\\*", "\x30").replace("\\?", "\x31"); // save escaped wildcard chars output.replace("*", ".*").replace("?", "."); // convert common wildcards output.replace("\x30", "\\\\*").replace("\x31", "\\\\?"); // replace escaped wildcard chars output.prepend("^"); // match from start of line; .append("$"); // match whole line reFlags |= QRegularExpression::CaseInsensitiveOption; re.setPattern(output); } re.setPatternOptions(reFlags); return re; } /* * Filter input validator for RegEx syntax. */ QValidator::State DebugOutputFilterValidator::validate(QString & input, int &) const { QRegularExpression re = DebugOutput::makeRegEx(input); if (re.isValid()) return QValidator::Acceptable; else return QValidator::Intermediate; } /* * Event filter for editable QComboBox to allow deleting items with Shift-Delete/Backspace */ bool DeleteComboBoxItemEventFilter::eventFilter(QObject *obj, QEvent *event) { QComboBox * cb = dynamic_cast(obj); if (cb && cb->isEditable() && cb->currentIndex() > -1 && cb->currentData().toString() != "no_delete") { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if ((keyEvent->key() == Qt::Key::Key_Delete || keyEvent->key() == Qt::Key::Key_Backspace) && keyEvent->modifiers() == Qt::ShiftModifier) { cb->removeItem(cb->currentIndex()); cb->setCurrentIndex(-1); return true; } } } return QObject::eventFilter(obj, event); }