diff --git a/src/plugins/plugins.pro b/src/plugins/plugins.pro index 2eb9a6b2df5..94cb7b74d23 100644 --- a/src/plugins/plugins.pro +++ b/src/plugins/plugins.pro @@ -55,7 +55,8 @@ SUBDIRS = \ updateinfo \ scxmleditor \ welcome \ - silversearcher + silversearcher \ + serialterminal qtHaveModule(quick) { SUBDIRS += qmlprofiler diff --git a/src/plugins/serialterminal/SerialTerminal.json.in b/src/plugins/serialterminal/SerialTerminal.json.in new file mode 100644 index 00000000000..efff034b32a --- /dev/null +++ b/src/plugins/serialterminal/SerialTerminal.json.in @@ -0,0 +1,20 @@ +{ + \"Name\" : \"SerialTerminal\", + \"Version\" : \"$$QTCREATOR_VERSION\", + \"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\", + \"Vendor\" : \"Benjamin Balga\", + \"Experimental\" : true, + \"DisabledByDefault\" : true, + \"Copyright\" : \"(C) 2018 Benjamin Balga\", + \"License\" : [ \"Commercial Usage\", + \"\", + \"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt 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.\", + \"\", + \"GNU General Public License Usage\", + \"\", + \"Alternatively, this plugin 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 plugin. 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.\" + ], + \"Description\" : \"Serial Port Terminal\", + \"Url\" : \"http://www.qt.io\", + $$dependencyList +} diff --git a/src/plugins/serialterminal/consolelineedit.cpp b/src/plugins/serialterminal/consolelineedit.cpp new file mode 100644 index 00000000000..49c7cee3f60 --- /dev/null +++ b/src/plugins/serialterminal/consolelineedit.cpp @@ -0,0 +1,92 @@ +/**************************************************************************** +** +** 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 "consolelineedit.h" +#include "serialterminalconstants.h" + +#include + +namespace SerialTerminal { +namespace Internal { + +ConsoleLineEdit::ConsoleLineEdit(QWidget *parent) : + QLineEdit(parent), + m_maxEntries(Constants::DEFAULT_MAX_ENTRIES) +{ + connect(this, &QLineEdit::returnPressed, this, &ConsoleLineEdit::addHistoryEntry); +} + +// Add current text to history entries, if not empty and different from last entry. +// Called when return key is pressed. +void ConsoleLineEdit::addHistoryEntry() +{ + m_currentEntryIndex = 0; + const QString currentText = text(); + + if (currentText.isEmpty()) + return; + + if (!m_history.isEmpty() && m_history.first() == currentText) + return; + + m_history.prepend(currentText); + if (m_history.size() > m_maxEntries) + m_history.removeLast(); +} + +// Load a specific history entry: 0 = current, n = n-most last entry +void ConsoleLineEdit::loadHistoryEntry(int entryIndex) +{ + if (entryIndex < 0 || entryIndex > m_history.size()) + return; + + if (m_currentEntryIndex == 0) + m_editingEntry = text(); + + if (entryIndex <= 0 && m_currentEntryIndex > 0) { + m_currentEntryIndex = 0; + setText(m_editingEntry); + } else if (entryIndex > 0) { + m_currentEntryIndex = entryIndex; + setText(m_history.at(entryIndex-1)); + } +} + +void ConsoleLineEdit::keyPressEvent(QKeyEvent *event) +{ + // Navigate history with up/down keys + if (event->key() == Qt::Key_Up) { + loadHistoryEntry(m_currentEntryIndex+1); + event->accept(); + } else if (event->key() == Qt::Key_Down) { + loadHistoryEntry(m_currentEntryIndex-1); + event->accept(); + } else { + QLineEdit::keyPressEvent(event); + } +} + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/consolelineedit.h b/src/plugins/serialterminal/consolelineedit.h new file mode 100644 index 00000000000..b3167cdb52e --- /dev/null +++ b/src/plugins/serialterminal/consolelineedit.h @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include + +namespace SerialTerminal { +namespace Internal { + +class ConsoleLineEdit : public QLineEdit +{ +public: + explicit ConsoleLineEdit(QWidget *parent = nullptr); + + void addHistoryEntry(); + void loadHistoryEntry(int entryIndex); + +protected: + void keyPressEvent(QKeyEvent *event) override final; + +private: + QStringList m_history; + int m_maxEntries; // TODO: add to settings + int m_currentEntryIndex = 0; + QString m_editingEntry; +}; + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialcontrol.cpp b/src/plugins/serialterminal/serialcontrol.cpp new file mode 100644 index 00000000000..55e4a94a804 --- /dev/null +++ b/src/plugins/serialterminal/serialcontrol.cpp @@ -0,0 +1,248 @@ +/**************************************************************************** +** +** 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 "serialcontrol.h" +#include "serialterminalconstants.h" + +#include + +namespace SerialTerminal { +namespace Internal { + +SerialControl::SerialControl(const Settings &settings, QObject *parent) : + QObject(parent) +{ + m_serialPort.setBaudRate(settings.baudRate); + m_serialPort.setDataBits(settings.dataBits); + m_serialPort.setParity(settings.parity); + m_serialPort.setStopBits(settings.stopBits); + m_serialPort.setFlowControl(settings.flowControl); + + if (!settings.portName.isEmpty()) + m_serialPort.setPortName(settings.portName); + + m_initialDtrState = settings.initialDtrState; + m_initialRtsState = settings.initialRtsState; + m_clearInputOnSend = settings.clearInputOnSend; + + m_reconnectTimer.setInterval(Constants::RECONNECT_DELAY); + m_reconnectTimer.setSingleShot(true); + + connect(&m_serialPort, &QSerialPort::readyRead, + this, &SerialControl::handleReadyRead); + + connect(&m_serialPort, &QSerialPort::errorOccurred, + this, &SerialControl::handleError); + + connect(&m_reconnectTimer, &QTimer::timeout, + this, &SerialControl::reconnectTimeout); +} + +bool SerialControl::start() +{ + stop(); + + if (!m_serialPort.open(QIODevice::ReadWrite)) { + if (!m_retrying) + appendMessage(tr("Unable to open port %1.").arg(portName()) + "\n", Utils::ErrorMessageFormat); + return false; + } + + m_serialPort.setDataTerminalReady(m_initialDtrState); + m_serialPort.setRequestToSend(m_initialRtsState); + + if (m_retrying) + appendMessage(tr("Session resumed.") + QString("\n\n"), Utils::NormalMessageFormat); + else + appendMessage(tr("Starting new session on %1...").arg(portName()) + "\n", Utils::NormalMessageFormat); + + m_retrying = false; + + m_running = true; + emit started(); + emit runningChanged(true); + return true; +} + +void SerialControl::stop(bool force) +{ + if (force) { + // Stop retries + m_reconnectTimer.stop(); + m_retrying = false; + } + + // Close if opened + if (m_serialPort.isOpen()) + m_serialPort.close(); + + // Print paused or finished message + if (force || (m_running && !m_retrying)) { + appendMessage(QString("\n") + + tr("Session finished on %1.").arg(portName()) + + QString("\n\n"), + Utils::NormalMessageFormat); + + m_running = false; + emit finished(); + emit runningChanged(false); + } else if (m_running && m_retrying) { + appendMessage(QString("\n") + + tr("Session paused...") + + QString("\n"), + Utils::NormalMessageFormat); + m_running = false; + // MAYBE: send paused() signals? + } +} + +bool SerialControl::isRunning() const +{ + // Considered "running" if "paused" (i.e. trying to reconnect) + return m_running || m_retrying; +} + +QString SerialControl::displayName() const +{ + return portName().isEmpty() ? tr("No Port") : portName(); +} + +bool SerialControl::canReUseOutputPane(const SerialControl *other) const +{ + return other->portName() == portName(); +} + +Utils::OutputFormatter *SerialControl::outputFormatter() +{ + return new Utils::OutputFormatter(); // TODO: custom formatter? +} + +void SerialControl::appendMessage(const QString &msg, Utils::OutputFormat format) +{ + emit appendMessageRequested(this, msg, format); +} + +QString SerialControl::portName() const +{ + return m_serialPort.portName(); +} + +void SerialControl::setPortName(const QString &name) +{ + if (m_serialPort.portName() == name) + return; + m_serialPort.setPortName(name); +} + +qint32 SerialControl::baudRate() const +{ + return m_serialPort.baudRate(); +} + +void SerialControl::setBaudRate(qint32 baudRate) +{ + if (m_serialPort.baudRate() == baudRate) + return; + m_serialPort.setBaudRate(baudRate); +} + +QString SerialControl::baudRateText() const +{ + return QString::number(baudRate()); +} + +void SerialControl::pulseDataTerminalReady() +{ + m_serialPort.setDataTerminalReady(!m_initialDtrState); + QTimer::singleShot(Constants::RESET_DELAY, [&]() { + m_serialPort.setDataTerminalReady(m_initialDtrState); + }); +} + +qint64 SerialControl::writeData(const QByteArray& data) +{ + return m_serialPort.write(data); +} + +void SerialControl::handleReadyRead() +{ + const QByteArray ba = m_serialPort.readAll(); + // For now, UTF8 should be safe for most use cases + appendMessage(QString::fromUtf8(ba), Utils::StdOutFormat); + // TODO: add config for string format conversion +} + +void SerialControl::reconnectTimeout() +{ + // No port name set, stop reconnecting + if (portName().isEmpty()) { + m_retrying = false; + return; + } + + // Try to reconnect, restart timer if failed + if (start()) + m_retrying = false; + else + m_reconnectTimer.start(); +} + +void SerialControl::handleError(QSerialPort::SerialPortError error) +{ + if (!isRunning()) // No auto reconnect if not running + return; + + if (!m_retrying && error != QSerialPort::NoError) + appendMessage(QString("\n") + + tr("Serial port error: %1 (%2)").arg(m_serialPort.errorString()).arg(error) + + QString("\n"), + Utils::ErrorMessageFormat); + + // Activate auto-reconnect on some resource errors + // TODO: add auto-reconnect option to settings + switch (error) { + case QSerialPort::OpenError: + case QSerialPort::DeviceNotFoundError: + case QSerialPort::WriteError: + case QSerialPort::ReadError: + case QSerialPort::ResourceError: + case QSerialPort::UnsupportedOperationError: + case QSerialPort::UnknownError: + case QSerialPort::TimeoutError: + case QSerialPort::NotOpenError: + // Enable auto-reconnect if needed + if (!m_reconnectTimer.isActive() && !portName().isEmpty()) { + m_retrying = true; + m_reconnectTimer.start(); + } + break; + + default: + break; + } +} + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialcontrol.h b/src/plugins/serialterminal/serialcontrol.h new file mode 100644 index 00000000000..229bf169c67 --- /dev/null +++ b/src/plugins/serialterminal/serialcontrol.h @@ -0,0 +1,105 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "serialterminalsettings.h" + +#include + +#include +#include +#include + +namespace Utils { class OutputFormatter; } + +namespace SerialTerminal { +namespace Internal { + +// Handle serial port connect/disconnect/auto-reconnect, data read/write and info/error messages. +class SerialControl : public QObject +{ + Q_OBJECT +public: + enum StopResult { + StoppedSynchronously, // Stopped. + AsynchronousStop // Stop sequence has been started + }; + + explicit SerialControl(const Settings &settings, QObject *parent = nullptr); + + bool start(); + + void stop(bool force = false); + bool isRunning() const; + + QString displayName() const; + + bool canReUseOutputPane(const SerialControl *other) const; + + Utils::OutputFormatter *outputFormatter(); + + void appendMessage(const QString &msg, Utils::OutputFormat format); + + QString portName() const; + void setPortName(const QString &name); + + qint32 baudRate() const; + void setBaudRate(qint32 baudRate); + QString baudRateText() const; + + void pulseDataTerminalReady(); + + qint64 writeData(const QByteArray &data); + +signals: + void appendMessageRequested(SerialControl *serialControl, + const QString &msg, Utils::OutputFormat format); + void started(); + void finished(); + void runningChanged(bool running); + +private: + void handleReadyRead(); + void reconnectTimeout(); + void handleError(QSerialPort::SerialPortError error); + +protected: + void tryReconnect(); + +private: + QString m_portName; + QSerialPort m_serialPort; + QTimer m_reconnectTimer; + + bool m_initialDtrState = false; + bool m_initialRtsState = false; + bool m_clearInputOnSend = false; + bool m_retrying = false; + bool m_running = false; +}; + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialdevicemodel.cpp b/src/plugins/serialterminal/serialdevicemodel.cpp new file mode 100644 index 00000000000..8a6b10173fc --- /dev/null +++ b/src/plugins/serialterminal/serialdevicemodel.cpp @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** 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 "serialdevicemodel.h" + +#include + +namespace SerialTerminal { +namespace Internal { + +SerialDeviceModel::SerialDeviceModel(QObject *parent) : + QAbstractListModel(parent), + m_baudRates(QSerialPortInfo::standardBaudRates()) +{ +} + +QString SerialDeviceModel::portName(int index) const +{ + if (index < 0 || index >= m_ports.size()) + return QString(); + + return m_ports.at(index).portName(); +} + +QStringList SerialDeviceModel::baudRates() const +{ + return Utils::transform(m_baudRates, [](int b) { return QString::number(b); }); +} + +qint32 SerialDeviceModel::baudRate(int index) const +{ + if (index < 0 || index >= m_baudRates.size()) + return 0; + + return m_baudRates.at(index); +} + +int SerialDeviceModel::indexForBaudRate(qint32 baudRate) const +{ + return m_baudRates.indexOf(baudRate); +} + +void SerialDeviceModel::disablePort(const QString &portName) +{ + m_disabledPorts.insert(portName); + + const int i = Utils::indexOf(m_ports, [&portName](const QSerialPortInfo &info) { + return info.portName() == portName; + }); + + if (i >= 0) + emit dataChanged(index(i), index(i), {Qt::DisplayRole}); +} + +void SerialDeviceModel::enablePort(const QString &portName) +{ + m_disabledPorts.remove(portName); +} + +void SerialDeviceModel::update() +{ + // Called from the combobox before popup, thus updated only when needed and immediately + beginResetModel(); + m_ports.clear(); + const QList serialPortInfos = QSerialPortInfo::availablePorts(); + for (const QSerialPortInfo &serialPortInfo : serialPortInfos) { + const QString portName = serialPortInfo.portName(); + + // TODO: add filter + if (!portName.isEmpty()) + m_ports.append(serialPortInfo); + } + endResetModel(); +} + +Qt::ItemFlags SerialDeviceModel::flags(const QModelIndex &index) const +{ + auto f = QAbstractListModel::flags(index); + if (!index.isValid() || index.row() < 0 || index.row() >= m_ports.size()) + return f; + + if (m_disabledPorts.contains(m_ports.at(index.row()).portName())) + f &= ~Qt::ItemIsEnabled; + + return f; +} + +int SerialDeviceModel::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return m_ports.size(); +} + +QVariant SerialDeviceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (role != Qt::DisplayRole) + return QVariant(); + + return m_ports.at(index.row()).portName(); +} + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialdevicemodel.h b/src/plugins/serialterminal/serialdevicemodel.h new file mode 100644 index 00000000000..f459740cbd6 --- /dev/null +++ b/src/plugins/serialterminal/serialdevicemodel.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include +#include +#include + +namespace SerialTerminal { +namespace Internal { + +class SerialDeviceModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit SerialDeviceModel(QObject *parent = nullptr); + + QString portName(int index) const; + + QStringList baudRates() const; + qint32 baudRate(int index) const; + int indexForBaudRate(qint32 baudRate) const; + + void disablePort(const QString &portName); + void enablePort(const QString &portName); + + void update(); + + Qt::ItemFlags flags(const QModelIndex &index) const override final; + int rowCount(const QModelIndex &parent) const override final; + QVariant data(const QModelIndex &index, int role) const override final; + +private: + QList m_ports; + QSet m_disabledPorts; + QList m_baudRates; +}; + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialoutputpane.cpp b/src/plugins/serialterminal/serialoutputpane.cpp new file mode 100644 index 00000000000..2fc5cf48ebc --- /dev/null +++ b/src/plugins/serialterminal/serialoutputpane.cpp @@ -0,0 +1,745 @@ +/**************************************************************************** +** +** 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace SerialTerminal { +namespace Internal { + +static Q_LOGGING_CATEGORY(log, Constants::LOGGING_CATEGORY) + +// 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) override 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(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(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() override 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(&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 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(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(&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(&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 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 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" diff --git a/src/plugins/serialterminal/serialoutputpane.h b/src/plugins/serialterminal/serialoutputpane.h new file mode 100644 index 00000000000..d7859652e9a --- /dev/null +++ b/src/plugins/serialterminal/serialoutputpane.h @@ -0,0 +1,176 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "serialdevicemodel.h" +#include "serialterminalsettings.h" + +#include +#include + +#include + +#include + +QT_BEGIN_NAMESPACE +class QToolButton; +class QButtonGroup; +class QAbstractButton; +class QComboBox; +QT_END_NAMESPACE + +namespace Core { class OutputWindow; } + +namespace SerialTerminal { +namespace Internal { + +class SerialControl; +class TabWidget; +class ComboBox; +class ConsoleLineEdit; + +class SerialOutputPane : public Core::IOutputPane +{ + Q_OBJECT + +public: + enum CloseTabMode { + CloseTabNoPrompt, + CloseTabWithPrompt + }; + + enum BehaviorOnOutput { + Flash, + Popup + }; + + explicit SerialOutputPane(Settings &settings); + + // IOutputPane + QWidget *outputWidget(QWidget *parent) override final; + QList toolBarWidgets() const override final; + QString displayName() const override final; + + int priorityInStatusBar() const override final; + void clearContents() override final; + void visibilityChanged(bool) override final; + bool canFocus() const override final; + bool hasFocus() const override final; + void setFocus() override final; + + bool canNext() const override final; + bool canPrevious() const override final; + void goToNext() override final; + void goToPrev() override final; + bool canNavigate() const override final; + + void createNewOutputWindow(SerialControl *rc); + + bool closeTabs(CloseTabMode mode); + + void appendMessage(SerialControl *rc, const QString &out, Utils::OutputFormat format); + + void setSettings(const Settings &settings); + +signals: + void settingsChanged(const Settings &settings); + +private: + class SerialControlTab { + public: + explicit SerialControlTab(SerialControl *serialControl = nullptr, + Core::OutputWindow *window = nullptr); + SerialControl *serialControl = nullptr; + Core::OutputWindow *window = nullptr; + BehaviorOnOutput behaviorOnOutput = Flash; + int inputCursorPosition = 0; + QString inputText; + QByteArray lineEnd; + int lineEndingIndex = 0; + }; + + void createToolButtons(); + void updateLineEndingsComboBox(); + + void contextMenuRequested(const QPoint &pos, int index); + + void enableDefaultButtons(); + void enableButtons(const SerialControl *rc, bool isRunning); + void tabChanged(int i); + + bool isRunning() const; + + void activePortNameChanged(int index); + void activeBaudRateChanged(int index); + void defaultLineEndingChanged(int index); + + void connectControl(); + void disconnectControl(); + void resetControl(); + void openNewTerminalControl(); + void sendInput(); + + bool closeTab(int index, CloseTabMode cm = CloseTabWithPrompt); + int indexOf(const SerialControl *rc) const; + int indexOf(const QWidget *outputWindow) const; + int currentIndex() const; + SerialControl *currentSerialControl() const; + bool isCurrent(const SerialControl *rc) const; + int findTabWithPort(const QString &portName) const; + int findRunningTabWithPort(const QString &portName) const; + void handleOldOutput(Core::OutputWindow *window) const; + + void updateCloseActions(); + + + std::unique_ptr m_mainWidget; + ConsoleLineEdit *m_inputLine = nullptr; + QComboBox *m_lineEndingsSelection = nullptr; + TabWidget *m_tabWidget = nullptr; + Settings m_settings; + QVector m_serialControlTabs; + int m_prevTabIndex = -1; + + SerialDeviceModel *m_devicesModel = nullptr; + + QAction *m_closeCurrentTabAction = nullptr; + QAction *m_closeAllTabsAction = nullptr; + QAction *m_closeOtherTabsAction = nullptr; + + QAction *m_disconnectAction = nullptr; + QToolButton *m_connectButton = nullptr; + QToolButton *m_disconnectButton = nullptr; + QToolButton *m_resetButton = nullptr; + QToolButton *m_newButton = nullptr; + ComboBox *m_portsSelection = nullptr; + ComboBox *m_baudRateSelection = nullptr; + + QString m_currentPortName; + float m_zoom = 1.0; +}; + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialterminal.pro b/src/plugins/serialterminal/serialterminal.pro new file mode 100644 index 00000000000..655f7a7759c --- /dev/null +++ b/src/plugins/serialterminal/serialterminal.pro @@ -0,0 +1,22 @@ +include(../../qtcreatorplugin.pri) + +QT += serialport + +# SerialTerminal files + +SOURCES += \ + consolelineedit.cpp \ + serialdevicemodel.cpp \ + serialoutputpane.cpp \ + serialterminalplugin.cpp \ + serialterminalsettings.cpp \ + serialcontrol.cpp + +HEADERS += \ + consolelineedit.h \ + serialdevicemodel.h \ + serialoutputpane.h \ + serialterminalconstants.h \ + serialterminalplugin.h \ + serialterminalsettings.h \ + serialcontrol.h diff --git a/src/plugins/serialterminal/serialterminal_dependencies.pri b/src/plugins/serialterminal/serialterminal_dependencies.pri new file mode 100644 index 00000000000..6bc09c0cfd0 --- /dev/null +++ b/src/plugins/serialterminal/serialterminal_dependencies.pri @@ -0,0 +1,6 @@ +QTC_PLUGIN_NAME = SerialTerminal +QTC_LIB_DEPENDS += \ + extensionsystem \ + utils +QTC_PLUGIN_DEPENDS += \ + coreplugin diff --git a/src/plugins/serialterminal/serialterminalconstants.h b/src/plugins/serialterminal/serialterminalconstants.h new file mode 100644 index 00000000000..f498f35db41 --- /dev/null +++ b/src/plugins/serialterminal/serialterminalconstants.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +namespace SerialTerminal { +namespace Constants { + +const char OUTPUT_PANE_TITLE[] = QT_TRANSLATE_NOOP("SerialTerminal::Internal::SerialTerminalOutputPane", "Serial Terminal"); + +const char LOGGING_CATEGORY[] = "qtc.serialterminal.outputpane"; + +// Settings entries +const char SETTINGS_GROUP[] = "SerialTerminalPlugin"; +const char SETTINGS_BAUDRATE[] = "BaudRate"; +const char SETTINGS_DATABITS[] = "DataBits"; +const char SETTINGS_PARITY[] = "Parity"; +const char SETTINGS_STOPBITS[] = "StopBits"; +const char SETTINGS_FLOWCONTROL[] = "FlowControl"; +const char SETTINGS_PORTNAME[] = "PortName"; +const char SETTINGS_INITIAL_DTR_STATE[] = "InitialDtr"; +const char SETTINGS_INITIAL_RTS_STATE[] = "InitialRts"; +const char SETTINGS_LINE_ENDINGS[] = "LineEndings"; +const char SETTINGS_LINE_ENDING_NAME[] = "LineEndingName"; +const char SETTINGS_LINE_ENDING_VALUE[] = "LineEndingValue"; +const char SETTINGS_DEFAULT_LINE_ENDING_INDEX[] = "DefaultLineEndingIndex"; +const char SETTINGS_CLEAR_INPUT_ON_SEND[] = "ClearInputOnSend"; + + +const int RECONNECT_DELAY = 1500; // milliseconds +const int RESET_DELAY = 100; // milliseconds +const int DEFAULT_MAX_ENTRIES = 20; // Max entries in the console line edit + +// Context +const char C_SERIAL_OUTPUT[] = "SerialTerminal.SerialOutput"; + +} // namespace SerialTerminal +} // namespace Constants diff --git a/src/plugins/serialterminal/serialterminalplugin.cpp b/src/plugins/serialterminal/serialterminalplugin.cpp new file mode 100644 index 00000000000..6eba0e81ff6 --- /dev/null +++ b/src/plugins/serialterminal/serialterminalplugin.cpp @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** 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 "serialterminalplugin.h" + +#include "serialcontrol.h" + +#include + +namespace SerialTerminal { +namespace Internal { + +bool SerialTerminalPlugin::initialize(const QStringList &arguments, QString *errorString) +{ + Q_UNUSED(arguments) + Q_UNUSED(errorString) + + m_settings.load(Core::ICore::settings()); + + // Create serial output pane + m_serialOutputPane = std::make_unique(m_settings); + connect(m_serialOutputPane.get(), &SerialOutputPane::settingsChanged, + this, &SerialTerminalPlugin::settingsChanged); + + connect(Core::ICore::instance(), &Core::ICore::saveSettingsRequested, + this, [this] { m_settings.save(Core::ICore::settings()); }); + + return true; +} + +void SerialTerminalPlugin::extensionsInitialized() +{ +} + +ExtensionSystem::IPlugin::ShutdownFlag SerialTerminalPlugin::aboutToShutdown() +{ + m_serialOutputPane->closeTabs(SerialOutputPane::CloseTabNoPrompt); + + return SynchronousShutdown; +} + +void SerialTerminalPlugin::settingsChanged(const Settings &settings) +{ + m_settings = settings; + m_settings.save(Core::ICore::settings()); + + m_serialOutputPane->setSettings(m_settings); +} + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialterminalplugin.h b/src/plugins/serialterminal/serialterminalplugin.h new file mode 100644 index 00000000000..7bf4b3ea41c --- /dev/null +++ b/src/plugins/serialterminal/serialterminalplugin.h @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include "serialoutputpane.h" +#include "serialterminalsettings.h" + +#include + +#include + +namespace SerialTerminal { +namespace Internal { + +class SerialTerminalPlugin : public ExtensionSystem::IPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "SerialTerminal.json") + +public: + explicit SerialTerminalPlugin() = default; + + bool initialize(const QStringList &arguments, QString *errorString) override final; + void extensionsInitialized() override final; + ShutdownFlag aboutToShutdown() override final; + +private: + void settingsChanged(const Settings &settings); + + Settings m_settings; + std::unique_ptr m_serialOutputPane; +}; + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialterminalsettings.cpp b/src/plugins/serialterminal/serialterminalsettings.cpp new file mode 100644 index 00000000000..fdb5c9aabe8 --- /dev/null +++ b/src/plugins/serialterminal/serialterminalsettings.cpp @@ -0,0 +1,178 @@ +/**************************************************************************** +** +** 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 "serialterminalsettings.h" +#include "serialterminalconstants.h" + +#include + +#include +#include + +namespace SerialTerminal { +namespace Internal { + +static Q_LOGGING_CATEGORY(log, Constants::LOGGING_CATEGORY) + +// Set 'value' only if the key exists in the settings +template +void readSetting(const QSettings &settings, T &value, const QString &key) { + if (settings.contains(key)) + value = settings.value(key).value(); +} + +Settings::Settings() +{ + lineEndings = { + {QObject::tr("None"), ""}, + {QObject::tr("LF"), "\n"}, + {QObject::tr("CR"), "\r"}, + {QObject::tr("CRLF"), "\r\n"} + }; + defaultLineEndingIndex = 1; +} + +// Save settings to a QSettings +void Settings::save(QSettings *settings) +{ + if (!settings || !edited) + return; + + settings->beginGroup(Constants::SETTINGS_GROUP); + + settings->setValue(Constants::SETTINGS_BAUDRATE, baudRate); + settings->setValue(Constants::SETTINGS_DATABITS, dataBits); + settings->setValue(Constants::SETTINGS_PARITY, parity); + settings->setValue(Constants::SETTINGS_STOPBITS, stopBits); + settings->setValue(Constants::SETTINGS_FLOWCONTROL, flowControl); + settings->setValue(Constants::SETTINGS_PORTNAME, portName); + settings->setValue(Constants::SETTINGS_INITIAL_DTR_STATE, initialDtrState); + settings->setValue(Constants::SETTINGS_INITIAL_RTS_STATE, initialRtsState); + settings->setValue(Constants::SETTINGS_DEFAULT_LINE_ENDING_INDEX, defaultLineEndingIndex); + settings->setValue(Constants::SETTINGS_CLEAR_INPUT_ON_SEND, clearInputOnSend); + + saveLineEndings(*settings); + + settings->endGroup(); + settings->sync(); + + edited = false; + + qCDebug(log) << "Settings saved."; +} + +// Load available settings from a QSettings +void Settings::load(QSettings *settings) +{ + if (!settings) + return; + + settings->beginGroup(Constants::SETTINGS_GROUP); + + readSetting(*settings, baudRate, Constants::SETTINGS_BAUDRATE); + readSetting(*settings, dataBits, Constants::SETTINGS_DATABITS); + readSetting(*settings, parity, Constants::SETTINGS_PARITY); + readSetting(*settings, stopBits, Constants::SETTINGS_STOPBITS); + readSetting(*settings, flowControl, Constants::SETTINGS_FLOWCONTROL); + + readSetting(*settings, portName, Constants::SETTINGS_PORTNAME); + readSetting(*settings, initialDtrState, Constants::SETTINGS_INITIAL_DTR_STATE); + readSetting(*settings, initialRtsState, Constants::SETTINGS_INITIAL_RTS_STATE); + readSetting(*settings, defaultLineEndingIndex, Constants::SETTINGS_DEFAULT_LINE_ENDING_INDEX); + + readSetting(*settings, clearInputOnSend, Constants::SETTINGS_CLEAR_INPUT_ON_SEND); + + loadLineEndings(*settings); + + settings->endGroup(); + + edited = false; + + qCDebug(log) << "Settings loaded."; +} + +void Settings::setBaudRate(qint32 br) +{ + if (br <= 0) + return; + + baudRate = br; + edited = true; +} + +QByteArray Settings::defaultLineEnding() const +{ + return defaultLineEndingIndex >= (unsigned int)lineEndings.size() + ? QByteArray() + : lineEndings.at(defaultLineEndingIndex).second; +} + +QString Settings::defaultLineEndingText() const +{ + return defaultLineEndingIndex >= (unsigned int)lineEndings.size() + ? QString() + : lineEndings.at(defaultLineEndingIndex).first; +} + +void Settings::setDefaultLineEndingIndex(unsigned int index) +{ + if (index >= (unsigned int)lineEndings.size()) + return; + + defaultLineEndingIndex = index; + edited = true; +} + + +void Settings::saveLineEndings(QSettings &settings) +{ + settings.beginWriteArray(Constants::SETTINGS_LINE_ENDINGS, lineEndings.size()); + int i = 0; + for (const QPair& value : Utils::asConst(lineEndings)) { + settings.setArrayIndex(i++); + settings.setValue(Constants::SETTINGS_LINE_ENDING_NAME, value.first); + settings.setValue(Constants::SETTINGS_LINE_ENDING_VALUE, value.second); + } + settings.endArray(); +} + +void Settings::loadLineEndings(QSettings &settings) +{ + const int size = settings.beginReadArray(Constants::SETTINGS_LINE_ENDINGS); + if (size > 0) // If null, keep default line endings + lineEndings.clear(); + + for (int i = 0; i < size; ++i) { + settings.setArrayIndex(i); + lineEndings.append({ + settings.value(Constants::SETTINGS_LINE_ENDING_NAME).toString(), + settings.value(Constants::SETTINGS_LINE_ENDING_VALUE).toByteArray() + }); + } + settings.endArray(); +} + +} // namespace Internal +} // namespace SerialTerminal diff --git a/src/plugins/serialterminal/serialterminalsettings.h b/src/plugins/serialterminal/serialterminalsettings.h new file mode 100644 index 00000000000..250eec48b71 --- /dev/null +++ b/src/plugins/serialterminal/serialterminalsettings.h @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** 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. +** +****************************************************************************/ + +#pragma once + +#include +#include +#include +#include +#include + +class QSettings; + +namespace SerialTerminal { +namespace Internal { + +class Settings { +public: + explicit Settings(); + + bool edited = false; + qint32 baudRate = 9600; + QSerialPort::DataBits dataBits = QSerialPort::Data8; + QSerialPort::Parity parity = QSerialPort::NoParity; + QSerialPort::StopBits stopBits = QSerialPort::OneStop; + QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl; + + QString portName; + + bool initialDtrState = false; + bool initialRtsState = false; + unsigned int defaultLineEndingIndex; + QVector> lineEndings; + + bool clearInputOnSend = false; + + void save(QSettings *settings); + void load(QSettings *settings); + + void setBaudRate(qint32 br); + + QByteArray defaultLineEnding() const; + QString defaultLineEndingText() const; + void setDefaultLineEndingIndex(unsigned int index); + +private: + void saveLineEndings(QSettings &settings); + void loadLineEndings(QSettings &settings); +}; + +} // namespace Internal +} // namespace SerialTerminal