diff --git a/.gitignore b/.gitignore index 7f4826b..1aafb5a 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,20 @@ compile_commands.json *creator.user* *_qmlcache.qrc + +# CMake build directories +build/ +Build/ +cmake-build-*/ +CMakeCache.txt +CMakeFiles/ +cmake_install.cmake +install_manifest.txt + +# CodeQL build artifacts +_codeql_build_dir/ +_codeql_detected_source_root + +# Executables +SerialChat +SerialChat.exe diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..74f9998 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.5) + +project(SerialChat VERSION 1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Automatically handle Qt MOC, UIC, and RCC +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTORCC ON) + +# Find Qt5 packages (minimum version 5.12) +find_package(Qt5 5.12 REQUIRED COMPONENTS Core Gui Widgets SerialPort) + +# Source files +set(SOURCES + src/main.cpp + src/models/SerialPort.cpp + src/models/Message.cpp + src/models/Session.cpp + src/views/MainWindow.cpp + src/views/MessageBubbleWidget.cpp +) + +# Header files +set(HEADERS + src/models/SerialPort.h + src/models/Message.h + src/models/Session.h + src/views/MainWindow.h + src/views/MessageBubbleWidget.h +) + +# Create executable +add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) + +# Link Qt5 libraries +target_link_libraries(${PROJECT_NAME} + Qt5::Core + Qt5::Gui + Qt5::Widgets + Qt5::SerialPort +) + +# Include directories +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/src/models + ${CMAKE_CURRENT_SOURCE_DIR}/src/views +) + +# Platform-specific settings for Qt6 compatibility +if(WIN32) + set_target_properties(${PROJECT_NAME} PROPERTIES WIN32_EXECUTABLE TRUE) +endif() diff --git a/README.md b/README.md index 0972a44..d901e07 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,78 @@ # Serial-Chat -​​Serial Chat​​: Reimagining serial port debugging as an intuitive chat conversation. Visualize data flows between devices and applications in a familiar, collaborative interface. +**Serial Chat**: Reimagining serial port debugging as an intuitive chat conversation. Visualize data flows between devices and applications in a familiar, collaborative interface. + +## Features +- Qt5-based C++ application (compatible with Qt 5.12+) +- Clean MVC architecture with separation between models and views +- Real-time serial port communication +- Chat-like interface for visualizing incoming/outgoing data +- Support for multiple baud rates and serial port configurations + +## Architecture + +The project follows a clean Model-View separation: + +### Models (`src/models/`) +- **SerialPort**: Wrapper around QSerialPort for managing serial port connections + - Signals: `dataReceived()`, `errorOccurred()`, `connectionStatusChanged()` + - Methods: `openPort()`, `closePort()`, `writeData()`, `readData()` + +- **Message**: Represents a single message with content, direction (incoming/outgoing), and timestamp + - Properties: `content`, `direction`, `timestamp` + - Signals: `contentChanged()`, `directionChanged()` + +- **Session**: Manages a collection of messages + - Methods: `addMessage()`, `clearMessages()`, `messages()` + - Signals: `messageAdded()`, `messagesCleared()` + +### Views (`src/views/`) +- **MainWindow**: Main application window with sidebar and chat area + - Sidebar: Port selection, baud rate configuration, connection controls + - Chat Area: QScrollArea displaying message bubbles + - Input: Text field and send button for outgoing messages + +- **MessageBubbleWidget**: Visual representation of a message as a chat bubble + - Different styling for incoming (white) vs outgoing (green) messages + - Displays message content and timestamp + +## Building the Project + +### Prerequisites +- CMake 3.5 or higher +- Qt5 5.12 or higher (Core, Gui, Widgets, SerialPort modules) +- C++11 compatible compiler + +### Linux (Ubuntu/Debian) +```bash +# Install dependencies +sudo apt-get update +sudo apt-get install -y qtbase5-dev libqt5serialport5-dev cmake build-essential + +# Build +mkdir build +cd build +cmake .. +cmake --build . + +# Run +./SerialChat +``` + +### Future Qt6 Compatibility +The code is structured to be portable to Qt6 with minimal changes: +- Uses `find_package(Qt5 ...)` which can be changed to `find_package(Qt6 ...)` +- Uses Qt5 signal/slot syntax compatible with Qt6 +- No deprecated Qt5 APIs used +- Clean separation of concerns makes migration easier + +## Usage +1. Launch the application +2. Select a serial port from the dropdown (click "Refresh Ports" to update the list) +3. Choose the appropriate baud rate +4. Click "Connect" to establish the connection +5. Type messages in the input field and press "Send" or Enter +6. Incoming data appears as white bubbles on the left +7. Outgoing data appears as green bubbles on the right + +## License +See LICENSE file for details. diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..c7397ea --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,16 @@ +#include +#include "MainWindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + app.setApplicationName("Serial Chat"); + app.setApplicationVersion("1.0"); + app.setOrganizationName("SerialChat"); + + MainWindow mainWindow; + mainWindow.show(); + + return app.exec(); +} diff --git a/src/models/Message.cpp b/src/models/Message.cpp new file mode 100644 index 0000000..5a037c0 --- /dev/null +++ b/src/models/Message.cpp @@ -0,0 +1,52 @@ +#include "Message.h" + +Message::Message(QObject *parent) + : QObject(parent) + , m_direction(Incoming) + , m_timestamp(QDateTime::currentDateTime()) +{ +} + +Message::Message(const QString &content, Direction direction, QObject *parent) + : QObject(parent) + , m_content(content) + , m_direction(direction) + , m_timestamp(QDateTime::currentDateTime()) +{ +} + +QString Message::content() const +{ + return m_content; +} + +void Message::setContent(const QString &content) +{ + if (m_content != content) { + m_content = content; + emit contentChanged(); + } +} + +Message::Direction Message::direction() const +{ + return m_direction; +} + +void Message::setDirection(Direction direction) +{ + if (m_direction != direction) { + m_direction = direction; + emit directionChanged(); + } +} + +QDateTime Message::timestamp() const +{ + return m_timestamp; +} + +void Message::setTimestamp(const QDateTime ×tamp) +{ + m_timestamp = timestamp; +} diff --git a/src/models/Message.h b/src/models/Message.h new file mode 100644 index 0000000..0bc4142 --- /dev/null +++ b/src/models/Message.h @@ -0,0 +1,41 @@ +#ifndef MESSAGE_H +#define MESSAGE_H + +#include +#include +#include + +class Message : public QObject +{ + Q_OBJECT + +public: + enum Direction { + Incoming, + Outgoing + }; + + explicit Message(QObject *parent = nullptr); + Message(const QString &content, Direction direction, QObject *parent = nullptr); + ~Message() override = default; + + QString content() const; + void setContent(const QString &content); + + Direction direction() const; + void setDirection(Direction direction); + + QDateTime timestamp() const; + void setTimestamp(const QDateTime ×tamp); + +signals: + void contentChanged(); + void directionChanged(); + +private: + QString m_content; + Direction m_direction; + QDateTime m_timestamp; +}; + +#endif // MESSAGE_H diff --git a/src/models/SerialPort.cpp b/src/models/SerialPort.cpp new file mode 100644 index 0000000..99773af --- /dev/null +++ b/src/models/SerialPort.cpp @@ -0,0 +1,102 @@ +#include "SerialPort.h" + +SerialPort::SerialPort(QObject *parent) + : QObject(parent) + , m_serialPort(new QSerialPort(this)) + , m_baudRate(QSerialPort::Baud9600) +{ + connect(m_serialPort, &QSerialPort::readyRead, this, &SerialPort::handleReadyRead); + connect(m_serialPort, &QSerialPort::errorOccurred, this, &SerialPort::handleError); +} + +SerialPort::~SerialPort() +{ + closePort(); +} + +bool SerialPort::openPort(const QString &portName, qint32 baudRate) +{ + closePort(); + + m_portName = portName; + m_baudRate = baudRate; + + m_serialPort->setPortName(portName); + m_serialPort->setBaudRate(baudRate); + m_serialPort->setDataBits(QSerialPort::Data8); + m_serialPort->setParity(QSerialPort::NoParity); + m_serialPort->setStopBits(QSerialPort::OneStop); + m_serialPort->setFlowControl(QSerialPort::NoFlowControl); + + bool opened = m_serialPort->open(QIODevice::ReadWrite); + emit connectionStatusChanged(opened); + + return opened; +} + +void SerialPort::closePort() +{ + if (m_serialPort->isOpen()) { + m_serialPort->close(); + emit connectionStatusChanged(false); + } +} + +bool SerialPort::isOpen() const +{ + return m_serialPort->isOpen(); +} + +bool SerialPort::writeData(const QByteArray &data) +{ + if (!m_serialPort->isOpen()) { + return false; + } + + qint64 bytesWritten = m_serialPort->write(data); + if (bytesWritten == -1) { + return false; + } + + // Ensure all data is flushed + return m_serialPort->waitForBytesWritten(1000); +} + +QByteArray SerialPort::readData() +{ + if (!m_serialPort->isOpen()) { + return QByteArray(); + } + + return m_serialPort->readAll(); +} + +QString SerialPort::portName() const +{ + return m_portName; +} + +qint32 SerialPort::baudRate() const +{ + return m_baudRate; +} + +QList SerialPort::availablePorts() +{ + return QSerialPortInfo::availablePorts(); +} + +void SerialPort::handleReadyRead() +{ + QByteArray data = m_serialPort->readAll(); + if (!data.isEmpty()) { + emit dataReceived(data); + } +} + +void SerialPort::handleError(QSerialPort::SerialPortError error) +{ + if (error != QSerialPort::NoError && error != QSerialPort::TimeoutError) { + emit errorOccurred(m_serialPort->errorString()); + } +} diff --git a/src/models/SerialPort.h b/src/models/SerialPort.h new file mode 100644 index 0000000..d2b53e7 --- /dev/null +++ b/src/models/SerialPort.h @@ -0,0 +1,43 @@ +#ifndef SERIALPORT_H +#define SERIALPORT_H + +#include +#include +#include + +class SerialPort : public QObject +{ + Q_OBJECT + +public: + explicit SerialPort(QObject *parent = nullptr); + ~SerialPort() override; + + bool openPort(const QString &portName, qint32 baudRate = QSerialPort::Baud9600); + void closePort(); + bool isOpen() const; + + bool writeData(const QByteArray &data); + QByteArray readData(); + + QString portName() const; + qint32 baudRate() const; + + static QList availablePorts(); + +signals: + void dataReceived(const QByteArray &data); + void errorOccurred(const QString &error); + void connectionStatusChanged(bool connected); + +private slots: + void handleReadyRead(); + void handleError(QSerialPort::SerialPortError error); + +private: + QSerialPort *m_serialPort; + QString m_portName; + qint32 m_baudRate; +}; + +#endif // SERIALPORT_H diff --git a/src/models/Session.cpp b/src/models/Session.cpp new file mode 100644 index 0000000..fb34d32 --- /dev/null +++ b/src/models/Session.cpp @@ -0,0 +1,46 @@ +#include "Session.h" + +Session::Session(QObject *parent) + : QObject(parent) + , m_sessionName("Default Session") +{ +} + +void Session::addMessage(Message *message) +{ + if (message) { + message->setParent(this); + m_messages.append(message); + emit messageAdded(message); + } +} + +void Session::clearMessages() +{ + qDeleteAll(m_messages); + m_messages.clear(); + emit messagesCleared(); +} + +QList Session::messages() const +{ + return m_messages; +} + +int Session::messageCount() const +{ + return m_messages.count(); +} + +QString Session::sessionName() const +{ + return m_sessionName; +} + +void Session::setSessionName(const QString &name) +{ + if (m_sessionName != name) { + m_sessionName = name; + emit sessionNameChanged(); + } +} diff --git a/src/models/Session.h b/src/models/Session.h new file mode 100644 index 0000000..78b1751 --- /dev/null +++ b/src/models/Session.h @@ -0,0 +1,35 @@ +#ifndef SESSION_H +#define SESSION_H + +#include +#include +#include "Message.h" + +class Session : public QObject +{ + Q_OBJECT + +public: + explicit Session(QObject *parent = nullptr); + ~Session() override = default; + + void addMessage(Message *message); + void clearMessages(); + + QList messages() const; + int messageCount() const; + + QString sessionName() const; + void setSessionName(const QString &name); + +signals: + void messageAdded(Message *message); + void messagesCleared(); + void sessionNameChanged(); + +private: + QString m_sessionName; + QList m_messages; +}; + +#endif // SESSION_H diff --git a/src/views/MainWindow.cpp b/src/views/MainWindow.cpp new file mode 100644 index 0000000..46d9dd6 --- /dev/null +++ b/src/views/MainWindow.cpp @@ -0,0 +1,244 @@ +#include "MainWindow.h" +#include "MessageBubbleWidget.h" +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , m_serialPort(new SerialPort(this)) + , m_session(new Session(this)) +{ + setupUI(); + setupConnections(); + refreshPortList(); +} + +void MainWindow::setupUI() +{ + setWindowTitle("Serial Chat"); + resize(900, 600); + + // Create central widget with horizontal layout + m_centralWidget = new QWidget(this); + QHBoxLayout *mainLayout = new QHBoxLayout(m_centralWidget); + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->setSpacing(0); + + // Create splitter for resizable sidebar + QSplitter *splitter = new QSplitter(Qt::Horizontal, this); + + // === SIDEBAR === + m_sidebarWidget = new QWidget(this); + m_sidebarWidget->setMinimumWidth(200); + m_sidebarWidget->setMaximumWidth(300); + QVBoxLayout *sidebarLayout = new QVBoxLayout(m_sidebarWidget); + + // Connection settings group + QGroupBox *connectionGroup = new QGroupBox("Connection Settings", this); + QVBoxLayout *connectionLayout = new QVBoxLayout(connectionGroup); + + // Port selection + QLabel *portLabel = new QLabel("Serial Port:", this); + m_portComboBox = new QComboBox(this); + connectionLayout->addWidget(portLabel); + connectionLayout->addWidget(m_portComboBox); + + // Baud rate selection + QLabel *baudLabel = new QLabel("Baud Rate:", this); + m_baudRateComboBox = new QComboBox(this); + m_baudRateComboBox->addItems({"9600", "19200", "38400", "57600", "115200"}); + m_baudRateComboBox->setCurrentText("9600"); + connectionLayout->addWidget(baudLabel); + connectionLayout->addWidget(m_baudRateComboBox); + + // Buttons + m_refreshButton = new QPushButton("Refresh Ports", this); + m_connectButton = new QPushButton("Connect", this); + m_connectButton->setCheckable(true); + connectionLayout->addWidget(m_refreshButton); + connectionLayout->addWidget(m_connectButton); + + connectionGroup->setLayout(connectionLayout); + sidebarLayout->addWidget(connectionGroup); + + // Status label + m_statusLabel = new QLabel("Disconnected", this); + m_statusLabel->setStyleSheet("padding: 10px; background-color: #f0f0f0; border-radius: 5px;"); + m_statusLabel->setAlignment(Qt::AlignCenter); + sidebarLayout->addWidget(m_statusLabel); + + sidebarLayout->addStretch(); + m_sidebarWidget->setLayout(sidebarLayout); + + // === CHAT AREA === + m_chatWidget = new QWidget(this); + QVBoxLayout *chatMainLayout = new QVBoxLayout(m_chatWidget); + chatMainLayout->setContentsMargins(10, 10, 10, 10); + + // Chat scroll area + m_chatScrollArea = new QScrollArea(this); + m_chatScrollArea->setWidgetResizable(true); + m_chatScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_chatScrollArea->setStyleSheet("QScrollArea { border: 1px solid #ddd; background-color: #f5f5f5; }"); + + m_chatContentWidget = new QWidget(this); + m_chatLayout = new QVBoxLayout(m_chatContentWidget); + m_chatLayout->setAlignment(Qt::AlignTop); + m_chatLayout->setSpacing(10); + m_chatLayout->setContentsMargins(10, 10, 10, 10); + + m_chatContentWidget->setLayout(m_chatLayout); + m_chatScrollArea->setWidget(m_chatContentWidget); + + chatMainLayout->addWidget(m_chatScrollArea); + + // Message input area + QHBoxLayout *inputLayout = new QHBoxLayout(); + m_messageInput = new QLineEdit(this); + m_messageInput->setPlaceholderText("Type a message..."); + m_messageInput->setEnabled(false); + + m_sendButton = new QPushButton("Send", this); + m_sendButton->setEnabled(false); + + inputLayout->addWidget(m_messageInput); + inputLayout->addWidget(m_sendButton); + chatMainLayout->addLayout(inputLayout); + + m_chatWidget->setLayout(chatMainLayout); + + // Add widgets to splitter + splitter->addWidget(m_sidebarWidget); + splitter->addWidget(m_chatWidget); + splitter->setStretchFactor(0, 0); + splitter->setStretchFactor(1, 1); + + mainLayout->addWidget(splitter); + m_centralWidget->setLayout(mainLayout); + setCentralWidget(m_centralWidget); +} + +void MainWindow::setupConnections() +{ + connect(m_connectButton, &QPushButton::clicked, this, &MainWindow::onConnectButtonClicked); + connect(m_sendButton, &QPushButton::clicked, this, &MainWindow::onSendButtonClicked); + connect(m_refreshButton, &QPushButton::clicked, this, &MainWindow::refreshPortList); + connect(m_messageInput, &QLineEdit::returnPressed, this, &MainWindow::onSendButtonClicked); + + connect(m_serialPort, &SerialPort::dataReceived, this, &MainWindow::onDataReceived); + connect(m_session, &Session::messageAdded, this, &MainWindow::onMessageAdded); +} + +void MainWindow::refreshPortList() +{ + m_portComboBox->clear(); + QList ports = SerialPort::availablePorts(); + + for (const QSerialPortInfo &info : ports) { + m_portComboBox->addItem(info.portName()); + } + + if (m_portComboBox->count() == 0) { + m_portComboBox->addItem("No ports available"); + m_connectButton->setEnabled(false); + } else { + m_connectButton->setEnabled(true); + } +} + +void MainWindow::onConnectButtonClicked() +{ + if (m_connectButton->isChecked()) { + QString portName = m_portComboBox->currentText(); + + // Check if there are valid ports available + if (portName == "No ports available" || portName.isEmpty()) { + m_connectButton->setChecked(false); + m_statusLabel->setText("No valid port selected"); + m_statusLabel->setStyleSheet("padding: 10px; background-color: #f0c6c6; border-radius: 5px;"); + return; + } + + qint32 baudRate = m_baudRateComboBox->currentText().toInt(); + + if (m_serialPort->openPort(portName, baudRate)) { + m_connectButton->setText("Disconnect"); + m_statusLabel->setText("Connected to " + portName); + m_statusLabel->setStyleSheet("padding: 10px; background-color: #c8f0c6; border-radius: 5px;"); + m_messageInput->setEnabled(true); + m_sendButton->setEnabled(true); + m_portComboBox->setEnabled(false); + m_baudRateComboBox->setEnabled(false); + } else { + m_connectButton->setChecked(false); + m_statusLabel->setText("Failed to connect"); + m_statusLabel->setStyleSheet("padding: 10px; background-color: #f0c6c6; border-radius: 5px;"); + } + } else { + m_serialPort->closePort(); + m_connectButton->setText("Connect"); + m_statusLabel->setText("Disconnected"); + m_statusLabel->setStyleSheet("padding: 10px; background-color: #f0f0f0; border-radius: 5px;"); + m_messageInput->setEnabled(false); + m_sendButton->setEnabled(false); + m_portComboBox->setEnabled(true); + m_baudRateComboBox->setEnabled(true); + } +} + +void MainWindow::onSendButtonClicked() +{ + QString text = m_messageInput->text().trimmed(); + if (text.isEmpty() || !m_serialPort->isOpen()) { + return; + } + + QByteArray data = text.toUtf8(); + if (m_serialPort->writeData(data)) { + Message *message = new Message(text, Message::Outgoing); + m_session->addMessage(message); + m_messageInput->clear(); + } +} + +void MainWindow::onDataReceived(const QByteArray &data) +{ + QString text = QString::fromUtf8(data); + Message *message = new Message(text, Message::Incoming); + m_session->addMessage(message); +} + +void MainWindow::onMessageAdded(Message *message) +{ + addMessageToChat(message); +} + +void MainWindow::addMessageToChat(Message *message) +{ + MessageBubbleWidget *bubble = new MessageBubbleWidget(message, this); + + QHBoxLayout *bubbleLayout = new QHBoxLayout(); + bubbleLayout->setContentsMargins(0, 0, 0, 0); + + if (message->direction() == Message::Outgoing) { + bubbleLayout->addStretch(); + bubbleLayout->addWidget(bubble); + } else { + bubbleLayout->addWidget(bubble); + bubbleLayout->addStretch(); + } + + m_chatLayout->addLayout(bubbleLayout); + + // Scroll to bottom after layout update + QTimer::singleShot(0, this, &MainWindow::scrollToBottom); +} + +void MainWindow::scrollToBottom() +{ + QScrollBar *scrollBar = m_chatScrollArea->verticalScrollBar(); + scrollBar->setValue(scrollBar->maximum()); +} diff --git a/src/views/MainWindow.h b/src/views/MainWindow.h new file mode 100644 index 0000000..6cdd5da --- /dev/null +++ b/src/views/MainWindow.h @@ -0,0 +1,63 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "SerialPort.h" +#include "Session.h" +#include "Message.h" + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override = default; + +private slots: + void onConnectButtonClicked(); + void onSendButtonClicked(); + void onDataReceived(const QByteArray &data); + void onMessageAdded(Message *message); + void refreshPortList(); + void scrollToBottom(); + +private: + void setupUI(); + void setupConnections(); + void addMessageToChat(Message *message); + + // Models + SerialPort *m_serialPort; + Session *m_session; + + // UI Components - Sidebar + QWidget *m_sidebarWidget; + QComboBox *m_portComboBox; + QComboBox *m_baudRateComboBox; + QPushButton *m_connectButton; + QPushButton *m_refreshButton; + QLabel *m_statusLabel; + + // UI Components - Chat Area + QWidget *m_chatWidget; + QScrollArea *m_chatScrollArea; + QWidget *m_chatContentWidget; + QVBoxLayout *m_chatLayout; + QLineEdit *m_messageInput; + QPushButton *m_sendButton; + + // Central Widget + QWidget *m_centralWidget; +}; + +#endif // MAINWINDOW_H diff --git a/src/views/MessageBubbleWidget.cpp b/src/views/MessageBubbleWidget.cpp new file mode 100644 index 0000000..16b4e6f --- /dev/null +++ b/src/views/MessageBubbleWidget.cpp @@ -0,0 +1,79 @@ +#include "MessageBubbleWidget.h" +#include +#include +#include + +MessageBubbleWidget::MessageBubbleWidget(Message *message, QWidget *parent) + : QWidget(parent) + , m_message(message) +{ + m_layout = new QVBoxLayout(this); + m_layout->setContentsMargins(10, 8, 10, 8); + m_layout->setSpacing(4); + + m_contentLabel = new QLabel(this); + m_contentLabel->setWordWrap(true); + m_contentLabel->setTextFormat(Qt::PlainText); + + m_timestampLabel = new QLabel(this); + m_timestampLabel->setStyleSheet("color: gray; font-size: 10px;"); + + m_layout->addWidget(m_contentLabel); + m_layout->addWidget(m_timestampLabel); + + setLayout(m_layout); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); + setMaximumWidth(400); + + updateUI(); +} + +void MessageBubbleWidget::setMessage(Message *message) +{ + m_message = message; + updateUI(); +} + +Message *MessageBubbleWidget::message() const +{ + return m_message; +} + +void MessageBubbleWidget::paintEvent(QPaintEvent *event) +{ + Q_UNUSED(event); + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + + QPainterPath path; + path.addRoundedRect(rect(), 10, 10); + + if (m_message && m_message->direction() == Message::Outgoing) { + painter.fillPath(path, QColor(220, 248, 198)); + } else { + painter.fillPath(path, QColor(255, 255, 255)); + } + + painter.setPen(QColor(200, 200, 200)); + painter.drawPath(path); +} + +void MessageBubbleWidget::updateUI() +{ + if (m_message) { + m_contentLabel->setText(m_message->content()); + m_timestampLabel->setText(m_message->timestamp().toString("hh:mm:ss")); + + if (m_message->direction() == Message::Outgoing) { + m_contentLabel->setAlignment(Qt::AlignRight); + m_timestampLabel->setAlignment(Qt::AlignRight); + } else { + m_contentLabel->setAlignment(Qt::AlignLeft); + m_timestampLabel->setAlignment(Qt::AlignLeft); + } + } else { + m_contentLabel->setText(""); + m_timestampLabel->setText(""); + } +} diff --git a/src/views/MessageBubbleWidget.h b/src/views/MessageBubbleWidget.h new file mode 100644 index 0000000..6d33151 --- /dev/null +++ b/src/views/MessageBubbleWidget.h @@ -0,0 +1,32 @@ +#ifndef MESSAGEBUBBLEWIDGET_H +#define MESSAGEBUBBLEWIDGET_H + +#include +#include +#include +#include "Message.h" + +class MessageBubbleWidget : public QWidget +{ + Q_OBJECT + +public: + explicit MessageBubbleWidget(Message *message, QWidget *parent = nullptr); + ~MessageBubbleWidget() override = default; + + void setMessage(Message *message); + Message *message() const; + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + void updateUI(); + + Message *m_message; + QLabel *m_contentLabel; + QLabel *m_timestampLabel; + QVBoxLayout *m_layout; +}; + +#endif // MESSAGEBUBBLEWIDGET_H