Files
qt-creator/src/plugins/serialterminal/serialoutputpane.cpp

746 lines
22 KiB
C++
Raw Normal View History

/****************************************************************************
**
** 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 <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->setMargin(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->setMargin(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, static_cast<void (QComboBox::*)(int)>(&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 uint counter = 0;
Core::Id contextId = Core::Id(Constants::C_SERIAL_OUTPUT).withSuffix(counter++);
Core::Context context(contextId);
Core::OutputWindow *ow = new Core::OutputWindow(context, m_tabWidget);
ow->setWindowTitle(tr("Serial Terminal Window"));
ow->setFormatter(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->setAutoRaise(true);
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->setAutoRaise(true);
m_disconnectButton->setEnabled(false);
connect(m_disconnectButton, &QToolButton::clicked,
this, &SerialOutputPane::disconnectControl);
// Reset button
m_resetButton = new QToolButton;
m_resetButton->setIcon(Utils::Icons::RELOAD.icon());
m_resetButton->setToolTip(tr("Reset Board"));
m_resetButton->setAutoRaise(true);
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->setAutoRaise(true);
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);
connect(m_portsSelection, &ComboBox::opened, m_devicesModel, &SerialDeviceModel::update);
connect(m_portsSelection, static_cast<void (ComboBox::*)(int)>(&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, static_cast<void (ComboBox::*)(int)>(&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 (QPair<QString,QByteArray> value : m_settings.lineEndings)
m_lineEndingsSelection->addItem(value.first, value.second);
m_lineEndingsSelection->setCurrentIndex(m_settings.defaultLineEndingIndex);
}
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), 0, 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;
}
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);
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);
}
// TODO: add a blink or something to visually see if the data was sent or not
}
} // namespace Internal
} // namespace SerialTerminal
#include "serialoutputpane.moc"