Files
qt-creator/src/plugins/serialterminal/serialoutputpane.cpp
Christian Kandeler 04a99c1de1 Remove the limitation that output formatters have to be exclusive
Introduce an aggregating output formatter that forwards its input to a
sub-formatter that feels responsible for it, or otherwise lets the base
class handle it.
Our output panes now use such an aggregating formatter.
In particular, this means that in the future, we won't have to stuff all
run control output formatting into the Qt output formatter anymore.

Change-Id: I5498f200a61db10ccff3ec8974c6825da7f7072d
Reviewed-by: hjk <hjk@qt.io>
2020-03-20 13:48:15 +00:00

767 lines
23 KiB
C++

/****************************************************************************
**
** Copyright (C) 2018 Benjamin Balga
** 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 "serialoutputpane.h"
#include "consolelineedit.h"
#include "serialcontrol.h"
#include "serialterminalconstants.h"
#include <coreplugin/actionmanager/actioncontainer.h>
#include <coreplugin/actionmanager/actionmanager.h>
#include <coreplugin/actionmanager/command.h>
#include <coreplugin/coreconstants.h>
#include <coreplugin/icontext.h>
#include <coreplugin/icore.h>
#include <coreplugin/outputwindow.h>
#include <texteditor/fontsettings.h>
#include <texteditor/texteditorsettings.h>
#include <utils/algorithm.h>
#include <utils/icon.h>
#include <utils/outputformatter.h>
#include <utils/qtcassert.h>
#include <utils/theme/theme.h>
#include <utils/utilsicons.h>
#include <QAction>
#include <QComboBox>
#include <QLoggingCategory>
#include <QMenu>
#include <QTabBar>
#include <QToolButton>
#include <QVBoxLayout>
namespace SerialTerminal {
namespace Internal {
static Q_LOGGING_CATEGORY(log, Constants::LOGGING_CATEGORY, QtWarningMsg)
// Tab Widget helper for middle click tab close
class TabWidget : public QTabWidget
{
Q_OBJECT
public:
explicit TabWidget(QWidget *parent = nullptr);
signals:
void contextMenuRequested(const QPoint &pos, int index);
protected:
bool eventFilter(QObject *object, QEvent *event) final;
private:
int m_tabIndexForMiddleClick = -1;
};
TabWidget::TabWidget(QWidget *parent) :
QTabWidget(parent)
{
tabBar()->installEventFilter(this);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &QWidget::customContextMenuRequested,
[this](const QPoint &pos) {
emit contextMenuRequested(pos, tabBar()->tabAt(pos));
});
}
bool TabWidget::eventFilter(QObject *object, QEvent *event)
{
if (object == tabBar()) {
if (event->type() == QEvent::MouseButtonPress) {
const auto *me = static_cast<QMouseEvent *>(event);
if (me->button() == Qt::MiddleButton) {
m_tabIndexForMiddleClick = tabBar()->tabAt(me->pos());
event->accept();
return true;
}
} else if (event->type() == QEvent::MouseButtonRelease) {
const auto *me = static_cast<QMouseEvent *>(event);
if (me->button() == Qt::MiddleButton) {
int tabIndex = tabBar()->tabAt(me->pos());
if (tabIndex != -1 && tabIndex == m_tabIndexForMiddleClick)
emit tabCloseRequested(tabIndex);
m_tabIndexForMiddleClick = -1;
event->accept();
return true;
}
}
}
return QTabWidget::eventFilter(object, event);
}
// QComboBox with a signal emitted before showPopup() to update on opening
class ComboBox : public QComboBox
{
Q_OBJECT
public:
void showPopup() final;
signals:
void opened();
};
void ComboBox::showPopup()
{
emit opened();
QComboBox::showPopup();
}
SerialOutputPane::SerialControlTab::SerialControlTab(SerialControl *serialControl, Core::OutputWindow *w) :
serialControl(serialControl), window(w)
{}
SerialOutputPane::SerialOutputPane(Settings &settings) :
m_mainWidget(new QWidget),
m_inputLine(new ConsoleLineEdit),
m_tabWidget(new TabWidget),
m_settings(settings),
m_devicesModel(new SerialDeviceModel),
m_closeCurrentTabAction(new QAction(tr("Close Tab"), this)),
m_closeAllTabsAction(new QAction(tr("Close All Tabs"), this)),
m_closeOtherTabsAction(new QAction(tr("Close Other Tabs"), this))
{
createToolButtons();
auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
m_tabWidget->setDocumentMode(true);
m_tabWidget->setTabsClosable(true);
m_tabWidget->setMovable(true);
connect(m_tabWidget, &QTabWidget::tabCloseRequested,
this, [this](int index) { closeTab(index); });
layout->addWidget(m_tabWidget);
connect(m_tabWidget, &QTabWidget::currentChanged, this, &SerialOutputPane::tabChanged);
connect(m_tabWidget, &TabWidget::contextMenuRequested,
this, &SerialOutputPane::contextMenuRequested);
auto inputLayout = new QHBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(2);
m_inputLine->setPlaceholderText(tr("Type text and hit Enter to send."));
inputLayout->addWidget(m_inputLine);
connect(m_inputLine, &QLineEdit::returnPressed, this, &SerialOutputPane::sendInput);
m_lineEndingsSelection = new QComboBox;
updateLineEndingsComboBox();
inputLayout->addWidget(m_lineEndingsSelection);
connect(m_lineEndingsSelection, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &SerialOutputPane::defaultLineEndingChanged);
layout->addLayout(inputLayout);
m_mainWidget->setLayout(layout);
enableDefaultButtons();
}
QWidget *SerialOutputPane::outputWidget(QWidget *parent)
{
Q_UNUSED(parent)
return m_mainWidget.get();
}
QList<QWidget *> SerialOutputPane::toolBarWidgets() const
{
return { m_newButton,
m_portsSelection, m_baudRateSelection,
m_connectButton, m_disconnectButton,
m_resetButton };
}
QString SerialOutputPane::displayName() const
{
return tr(Constants::OUTPUT_PANE_TITLE);
}
int SerialOutputPane::priorityInStatusBar() const
{
return 30;
}
void SerialOutputPane::clearContents()
{
auto currentWindow = qobject_cast<Core::OutputWindow *>(m_tabWidget->currentWidget());
if (currentWindow)
currentWindow->clear();
}
void SerialOutputPane::visibilityChanged(bool)
{
// Unused but pure virtual
}
bool SerialOutputPane::canFocus() const
{
return m_tabWidget->currentWidget();
}
bool SerialOutputPane::hasFocus() const
{
const QWidget *widget = m_tabWidget->currentWidget();
return widget ? widget->window()->focusWidget() == widget : false;
}
void SerialOutputPane::setFocus()
{
if (m_tabWidget->currentWidget())
m_tabWidget->currentWidget()->setFocus();
}
bool SerialOutputPane::canNext() const
{
return false;
}
bool SerialOutputPane::canPrevious() const
{
return false;
}
void SerialOutputPane::goToNext()
{
// Unused but pure virtual
}
void SerialOutputPane::goToPrev()
{
// Unused but pure virtual
}
bool SerialOutputPane::canNavigate() const
{
return false;
}
void SerialOutputPane::appendMessage(SerialControl *rc, const QString &out, Utils::OutputFormat format)
{
const int index = indexOf(rc);
if (index != -1) {
Core::OutputWindow *window = m_serialControlTabs.at(index).window;
window->appendMessage(out, format);
if (format != Utils::NormalMessageFormat) {
if (m_serialControlTabs.at(index).behaviorOnOutput == Flash)
flash();
else
popup(NoModeSwitch);
}
}
}
void SerialOutputPane::setSettings(const Settings &settings)
{
m_settings = settings;
}
void SerialOutputPane::createNewOutputWindow(SerialControl *rc)
{
if (!rc)
return;
// Signals to update buttons
connect(rc, &SerialControl::started,
[this, rc]() {
if (isCurrent(rc))
enableButtons(rc, true);
});
connect(rc, &SerialControl::finished,
[this, rc]() {
rc->outputFormatter()->flush();
if (isCurrent(rc))
enableButtons(rc, false);
});
connect(rc, &SerialControl::appendMessageRequested,
this, &SerialOutputPane::appendMessage);
Utils::OutputFormatter *formatter = rc->outputFormatter();
// Create new
static int counter = 0;
Core::Id contextId = Core::Id(Constants::C_SERIAL_OUTPUT).withSuffix(counter++);
Core::Context context(contextId);
auto ow = new Core::OutputWindow(context, QString(), m_tabWidget);
using TextEditor::TextEditorSettings;
auto fontSettingsChanged = [ow] {
ow->setBaseFont(TextEditorSettings::fontSettings().font());
};
connect(TextEditorSettings::instance(), &TextEditorSettings::fontSettingsChanged,
this, fontSettingsChanged);
fontSettingsChanged();
ow->setWindowTitle(tr("Serial Terminal Window"));
ow->setFormatters({formatter});
// TODO: wordwrap, maxLineCount, zoom/wheelZoom (add to settings)
auto controlTab = SerialControlTab(rc, ow);
controlTab.lineEndingIndex = m_settings.defaultLineEndingIndex;
controlTab.lineEnd = m_settings.defaultLineEnding();
m_serialControlTabs.push_back(controlTab);
m_tabWidget->addTab(ow, rc->displayName());
m_tabWidget->setCurrentIndex(m_tabWidget->count()-1); // Focus new tab
qCDebug(log) << "Adding tab for " << rc;
updateCloseActions();
}
bool SerialOutputPane::closeTabs(CloseTabMode mode)
{
bool allClosed = true;
for (int t = m_tabWidget->count() - 1; t >= 0; t--) {
if (!closeTab(t, mode))
allClosed = false;
}
qCDebug(log) << "all tabs closed: " << allClosed;
return allClosed;
}
void SerialOutputPane::createToolButtons()
{
// TODO: add actions for keyboard shortcuts?
// Connect button
m_connectButton = new QToolButton;
m_connectButton->setIcon(Utils::Icons::RUN_SMALL_TOOLBAR.icon());
m_connectButton->setToolTip(tr("Connect"));
m_connectButton->setEnabled(false);
connect(m_connectButton, &QToolButton::clicked,
this, &SerialOutputPane::connectControl);
// Disconnect button
m_disconnectButton = new QToolButton;
m_disconnectButton->setIcon(Utils::Icons::STOP_SMALL_TOOLBAR.icon());
m_disconnectButton->setToolTip(tr("Disconnect"));
m_disconnectButton->setEnabled(false);
connect(m_disconnectButton, &QToolButton::clicked,
this, &SerialOutputPane::disconnectControl);
// Reset button
m_resetButton = new QToolButton;
m_resetButton->setIcon(Utils::Icons::RELOAD_TOOLBAR.icon());
m_resetButton->setToolTip(tr("Reset Board"));
m_resetButton->setEnabled(false);
connect(m_resetButton, &QToolButton::clicked,
this, &SerialOutputPane::resetControl);
// New terminal button
m_newButton = new QToolButton;
m_newButton->setIcon(Utils::Icons::PLUS_TOOLBAR.icon());
m_newButton->setToolTip(tr("Add New Terminal"));
m_newButton->setEnabled(true);
connect(m_newButton, &QToolButton::clicked,
this, &SerialOutputPane::openNewTerminalControl);
// Available devices box
m_portsSelection = new ComboBox;
m_portsSelection->setSizeAdjustPolicy(QComboBox::AdjustToContents);
m_portsSelection->setModel(m_devicesModel);
updatePortsList();
connect(m_portsSelection, &ComboBox::opened, this, &SerialOutputPane::updatePortsList);
connect(m_portsSelection, QOverload<int>::of(&ComboBox::currentIndexChanged),
this, &SerialOutputPane::activePortNameChanged);
// TODO: the ports are not updated with the box opened (if the user wait for it) -> add a timer?
// Baud rates box
// TODO: add only most common bauds and custom field
m_baudRateSelection = new ComboBox;
m_baudRateSelection->setSizeAdjustPolicy(QComboBox::AdjustToContents);
m_baudRateSelection->addItems(m_devicesModel->baudRates());
connect(m_baudRateSelection, QOverload<int>::of(&ComboBox::currentIndexChanged),
this, &SerialOutputPane::activeBaudRateChanged);
if (m_settings.baudRate > 0)
m_baudRateSelection->setCurrentIndex(m_devicesModel->indexForBaudRate(m_settings.baudRate));
else // Set a default baudrate
m_baudRateSelection->setCurrentIndex(m_devicesModel->indexForBaudRate(115200));
}
void SerialOutputPane::updateLineEndingsComboBox()
{
m_lineEndingsSelection->clear();
for (auto &value : m_settings.lineEndings)
m_lineEndingsSelection->addItem(value.first, value.second);
m_lineEndingsSelection->setCurrentIndex(m_settings.defaultLineEndingIndex);
}
void SerialOutputPane::updatePortsList()
{
m_devicesModel->update();
m_portsSelection->setCurrentIndex(m_devicesModel->indexForPort(m_settings.portName));
}
int SerialOutputPane::indexOf(const SerialControl *rc) const
{
return Utils::indexOf(m_serialControlTabs, [rc](const SerialControlTab &tab) {
return tab.serialControl == rc;
});
}
int SerialOutputPane::indexOf(const QWidget *outputWindow) const
{
return Utils::indexOf(m_serialControlTabs, [outputWindow](const SerialControlTab &tab) {
return tab.window == outputWindow;
});
}
int SerialOutputPane::currentIndex() const
{
if (const QWidget *w = m_tabWidget->currentWidget())
return indexOf(w);
return -1;
}
SerialControl *SerialOutputPane::currentSerialControl() const
{
const int index = currentIndex();
if (index != -1)
return m_serialControlTabs.at(index).serialControl;
return nullptr;
}
bool SerialOutputPane::isCurrent(const SerialControl *rc) const
{
const int index = currentIndex();
return index >= 0 ? m_serialControlTabs.at(index).serialControl == rc : false;
}
int SerialOutputPane::findTabWithPort(const QString &portName) const
{
return Utils::indexOf(m_serialControlTabs, [&portName](const SerialControlTab &tab) {
return tab.serialControl->portName() == portName;
});
}
int SerialOutputPane::findRunningTabWithPort(const QString &portName) const
{
return Utils::indexOf(m_serialControlTabs, [&portName](const SerialControlTab &tab) {
return tab.serialControl->isRunning() && tab.serialControl->portName() == portName;
});
}
void SerialOutputPane::handleOldOutput(Core::OutputWindow *window) const
{
// TODO: cleanOldAppOutput setting? (window->clear();)
window->grayOutOldContent();
}
void SerialOutputPane::updateCloseActions()
{
const int tabCount = m_tabWidget->count();
m_closeCurrentTabAction->setEnabled(tabCount > 0);
m_closeAllTabsAction->setEnabled(tabCount > 0);
m_closeOtherTabsAction->setEnabled(tabCount > 1);
}
bool SerialOutputPane::closeTab(int tabIndex, CloseTabMode closeTabMode)
{
Q_UNUSED(closeTabMode)
int index = indexOf(m_tabWidget->widget(tabIndex));
QTC_ASSERT(index != -1, return true);
qCDebug(log) << "close tab " << tabIndex << m_serialControlTabs[index].serialControl
<< m_serialControlTabs[index].window;
// TODO: Prompt user to stop
if (m_serialControlTabs[index].serialControl->isRunning()) {
m_serialControlTabs[index].serialControl->stop(true); // Force stop
}
m_tabWidget->removeTab(tabIndex);
delete m_serialControlTabs[index].serialControl;
delete m_serialControlTabs[index].window;
m_serialControlTabs.removeAt(index);
updateCloseActions();
if (m_serialControlTabs.isEmpty())
hide();
return true;
}
void SerialOutputPane::contextMenuRequested(const QPoint &pos, int index)
{
QList<QAction *> actions { m_closeCurrentTabAction, m_closeAllTabsAction, m_closeOtherTabsAction };
QAction *action = QMenu::exec(actions, m_tabWidget->mapToGlobal(pos), nullptr, m_tabWidget);
const int currentIdx = index != -1 ? index : currentIndex();
if (action == m_closeCurrentTabAction) {
if (currentIdx >= 0)
closeTab(currentIdx);
} else if (action == m_closeAllTabsAction) {
closeTabs(SerialOutputPane::CloseTabWithPrompt);
} else if (action == m_closeOtherTabsAction) {
for (int t = m_tabWidget->count() - 1; t >= 0; t--)
if (t != currentIdx)
closeTab(t);
}
}
void SerialOutputPane::enableDefaultButtons()
{
const SerialControl *rc = currentSerialControl();
const bool isRunning = rc && rc->isRunning();
enableButtons(rc, isRunning);
}
void SerialOutputPane::enableButtons(const SerialControl *rc, bool isRunning)
{
// TODO: zoom buttons?
if (rc) {
m_connectButton->setEnabled(!isRunning);
m_disconnectButton->setEnabled(isRunning);
m_resetButton->setEnabled(isRunning);
m_portsSelection->setEnabled(!isRunning);
m_baudRateSelection->setEnabled(!isRunning);
} else {
m_connectButton->setEnabled(true);
m_disconnectButton->setEnabled(false);
m_portsSelection->setEnabled(true);
m_baudRateSelection->setEnabled(true);
}
}
void SerialOutputPane::tabChanged(int i)
{
if (m_prevTabIndex >= 0 && m_prevTabIndex < m_serialControlTabs.size()) {
m_serialControlTabs[m_prevTabIndex].inputText = m_inputLine->text();
m_serialControlTabs[m_prevTabIndex].inputCursorPosition = m_inputLine->cursorPosition();
}
const int index = indexOf(m_tabWidget->widget(i));
if (i != -1 && index != -1) {
SerialControlTab &tab = m_serialControlTabs[index];
const SerialControl *rc = tab.serialControl;
// Update combobox index
m_portsSelection->blockSignals(true);
m_baudRateSelection->blockSignals(true);
m_lineEndingsSelection->blockSignals(true);
m_portsSelection->setCurrentText(rc->portName());
m_baudRateSelection->setCurrentText(rc->baudRateText());
m_lineEndingsSelection->setCurrentIndex(tab.lineEndingIndex);
tab.lineEnd = m_lineEndingsSelection->currentData().toByteArray();
m_portsSelection->blockSignals(false);
m_baudRateSelection->blockSignals(false);
m_lineEndingsSelection->blockSignals(false);
m_inputLine->setText(tab.inputText);
m_inputLine->setCursorPosition(tab.inputCursorPosition);
qCDebug(log) << "Changed tab, is running:" << rc->isRunning();
// Update buttons
enableButtons(rc, rc->isRunning());
} else {
enableDefaultButtons();
}
m_prevTabIndex = index;
}
bool SerialOutputPane::isRunning() const
{
return Utils::anyOf(m_serialControlTabs, [](const SerialControlTab &rt) {
return rt.serialControl->isRunning();
});
}
void SerialOutputPane::activePortNameChanged(int index)
{
// When the model is updated, current index is reset -> set it back
if (index < 0) {
m_portsSelection->setCurrentText(m_currentPortName);
return;
}
const QString pn = m_devicesModel->portName(index);
if (SerialControl *current = currentSerialControl()) {
if (current->portName() == pn || pn.isEmpty())
return;
m_currentPortName = current->portName();
qCDebug(log) << "Set port to" << pn << "(" << index << ")";
current->setPortName(pn);
// Update tab text
const int tabIndex = indexOf(current);
if (tabIndex >= 0)
m_tabWidget->setTabText(tabIndex, pn);
}
// Update current port name
m_currentPortName = pn;
m_settings.setPortName(pn);
emit settingsChanged(m_settings);
}
void SerialOutputPane::activeBaudRateChanged(int index)
{
if (index < 0)
return;
SerialControl *current = currentSerialControl();
const qint32 br = m_devicesModel->baudRate(index);
qCDebug(log) << "Set baudrate to" << br << "(" << index << ")";
if (current)
current->setBaudRate(br);
m_settings.setBaudRate(br);
emit settingsChanged(m_settings);
}
void SerialOutputPane::defaultLineEndingChanged(int index)
{
if (index < 0)
return;
m_settings.setDefaultLineEndingIndex(index);
const int currentControlIndex = currentIndex();
if (currentControlIndex >= 0) {
m_serialControlTabs[currentControlIndex].lineEnd =
m_lineEndingsSelection->currentData().toByteArray();
}
qCDebug(log) << "Set default line ending to "
<< m_settings.defaultLineEndingText()
<< "(" << index << ")";
emit settingsChanged(m_settings);
}
void SerialOutputPane::connectControl()
{
const QString currentPortName = m_devicesModel->portName(m_portsSelection->currentIndex());
if (currentPortName.isEmpty())
return;
SerialControl *current = currentSerialControl();
const int index = currentIndex();
// MAYBE: use current->canReUseOutputPane(...)?
// Show tab if already opened and running
const int i = findRunningTabWithPort(currentPortName);
if (i >= 0) {
m_tabWidget->setCurrentIndex(i);
qCDebug(log) << "Port running in tab #" << i;
return;
}
if (current) {
current->setPortName(currentPortName);
current->setBaudRate(m_devicesModel->baudRate(m_baudRateSelection->currentIndex()));
// Gray out old and connect
if (index != -1) {
auto &tab = m_serialControlTabs[index];
handleOldOutput(tab.window);
tab.window->scrollToBottom();
}
qCDebug(log) << "Connect to" << current->portName();
} else {
// Create a new window
auto rc = new SerialControl(m_settings);
rc->setPortName(currentPortName);
createNewOutputWindow(rc);
qCDebug(log) << "Create and connect to" << rc->portName();
current = rc;
}
// Update tab text
if (index != -1)
m_tabWidget->setTabText(index, current->portName());
current->start();
}
void SerialOutputPane::disconnectControl()
{
SerialControl *current = currentSerialControl();
if (current) {
current->stop(true);
qCDebug(log) << "Disconnected.";
}
}
void SerialOutputPane::resetControl()
{
SerialControl *current = currentSerialControl();
if (current)
current->pulseDataTerminalReady();
}
void SerialOutputPane::openNewTerminalControl()
{
const QString currentPortName = m_devicesModel->portName(m_portsSelection->currentIndex());
if (currentPortName.isEmpty())
return;
auto rc = new SerialControl(m_settings);
rc->setPortName(currentPortName);
createNewOutputWindow(rc);
qCDebug(log) << "Created new terminal on" << rc->portName();
}
// Send lineedit input to serial port
void SerialOutputPane::sendInput()
{
SerialControl *current = currentSerialControl();
const int index = currentIndex();
if (current && current->isRunning() && index >= 0) {
qCDebug(log) << "Sending:" << m_inputLine->text().toUtf8();
current->writeData(m_inputLine->text().toUtf8() + m_serialControlTabs.at(index).lineEnd);
}
m_inputLine->selectAll();
// TODO: add a blink or something to visually see if the data was sent or not
}
} // namespace Internal
} // namespace SerialTerminal
#include "serialoutputpane.moc"