tools/chatgui: Qt5 GUI chat app stub with tray, channel list, message bubbles
This commit is contained in:
1 parent
6717c863e7
commit
b667d6f352
20 files changed
+1089
No files matched your search
@@ -0,0 +1,27 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(chatgui VERSION 0.1.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
|
||||
find_package(Qt5 COMPONENTS Widgets QUIET)
|
||||
if(NOT Qt5_FOUND)
|
||||
find_package(Qt6 COMPONENTS Widgets REQUIRED)
|
||||
set(QT_LIBS Qt6::Widgets)
|
||||
else()
|
||||
set(QT_LIBS Qt5::Widgets)
|
||||
endif()
|
||||
|
||||
add_executable(chatgui
|
||||
src/main.cpp
|
||||
src/mainwindow.cpp
|
||||
src/channellist.cpp
|
||||
src/channeldelegate.cpp
|
||||
src/chatview.cpp
|
||||
src/messagedelegate.cpp
|
||||
src/messagelist.cpp
|
||||
src/accountlist.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(chatgui PRIVATE ${QT_LIBS})
|
||||
@@ -0,0 +1,42 @@
|
||||
@echo off
|
||||
pushd "%~dp0\..\.."
|
||||
|
||||
echo ChatGUI Build Script for MSYS2 UCRT64
|
||||
echo ===========================================
|
||||
echo.
|
||||
|
||||
set MSYS2_PATH=C:\msys64
|
||||
if not "%MSYS2_ROOT%"=="" set MSYS2_PATH=%MSYS2_ROOT%
|
||||
if not "%MSYS2%"=="" set MSYS2_PATH=%MSYS2%
|
||||
|
||||
if not exist "%MSYS2_PATH%\msys2_shell.cmd" (
|
||||
echo [ERROR] MSYS2 not found at %MSYS2_PATH%
|
||||
echo Please install MSYS2 from https://www.msys2.org/
|
||||
echo or set MSYS2_ROOT environment variable.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [INFO] MSYS2 found at: %MSYS2_PATH%
|
||||
echo [INFO] Starting build...
|
||||
echo.
|
||||
|
||||
set "CURRENT_DIR=%CD%"
|
||||
|
||||
call "%MSYS2_PATH%\msys2_shell.cmd" -ucrt64 -here -defterm -no-start -c "cd \"%CURRENT_DIR%/tools/chatgui\" && bash build.sh 2>&1 | tee build_win.log"
|
||||
|
||||
set BUILD_RESULT=%ERRORLEVEL%
|
||||
|
||||
echo.
|
||||
echo ===========================================
|
||||
if %BUILD_RESULT% == 0 (
|
||||
echo [SUCCESS] Build completed!
|
||||
echo [INFO] Executable: tools\chatgui\build\chatgui.exe
|
||||
) else (
|
||||
echo [FAILED] Build failed with error code: %BUILD_RESULT%
|
||||
echo [INFO] Check tools\chatgui\build_win.log for details
|
||||
)
|
||||
echo ===========================================
|
||||
echo.
|
||||
|
||||
pause
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
set -eo pipefail
|
||||
|
||||
LOG_FILE="build_win.log"
|
||||
> "$LOG_FILE"
|
||||
|
||||
log() { echo "$1"; echo "$1" >> "$LOG_FILE"; }
|
||||
|
||||
log "================================"
|
||||
log "ChatGUI Build Script (MSYS2 UCRT64)"
|
||||
log "================================"
|
||||
log ""
|
||||
|
||||
SRC_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
BUILD_DIR="$SRC_DIR/build"
|
||||
|
||||
log "Source: $SRC_DIR"
|
||||
log "Build: $BUILD_DIR"
|
||||
log ""
|
||||
|
||||
log "[1/3] Configuring CMake..."
|
||||
cmake -S "$SRC_DIR" -B "$BUILD_DIR" -G "MSYS Makefiles" 2>&1 | tee -a "$LOG_FILE"
|
||||
test ${PIPESTATUS[0]} -eq 0 || { log "CMake configure FAILED"; exit 1; }
|
||||
|
||||
log ""
|
||||
log "[2/3] Building..."
|
||||
cmake --build "$BUILD_DIR" 2>&1 | tee -a "$LOG_FILE"
|
||||
test ${PIPESTATUS[0]} -eq 0 || { log "Build FAILED"; exit 1; }
|
||||
|
||||
log ""
|
||||
log "[3/3] Verifying..."
|
||||
EXE="$BUILD_DIR/chatgui.exe"
|
||||
if [ -f "$EXE" ]; then
|
||||
SIZE=$(du -h "$EXE" | cut -f1)
|
||||
log " OK: chatgui.exe ($SIZE)"
|
||||
log ""
|
||||
log "================================"
|
||||
log "Build SUCCESS"
|
||||
log "================================"
|
||||
log "Executable: $EXE"
|
||||
else
|
||||
log " FAIL: chatgui.exe not found"
|
||||
exit 1
|
||||
fi
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@@ -0,0 +1,69 @@
|
||||
#include "accountlist.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
static QPixmap makeAvatar(const QColor &color, const QString &letter) {
|
||||
QPixmap pix(24, 24);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setBrush(color);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(0, 0, 24, 24);
|
||||
p.setPen(Qt::white);
|
||||
QFont f = p.font();
|
||||
f.setBold(true);
|
||||
f.setPointSize(10);
|
||||
p.setFont(f);
|
||||
p.drawText(QRect(0, 0, 24, 24), Qt::AlignCenter, letter);
|
||||
p.end();
|
||||
return pix;
|
||||
}
|
||||
|
||||
AccountList::AccountList(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_listView(new QListView(this))
|
||||
, m_model(new QStandardItemModel(this))
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
auto *header = new QLabel("Accounts", this);
|
||||
header->setAlignment(Qt::AlignCenter);
|
||||
header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);");
|
||||
layout->addWidget(header);
|
||||
|
||||
m_listView->setModel(m_model);
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_listView->setIconSize(QSize(24, 24));
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
layout->addWidget(m_listView);
|
||||
|
||||
populateStubData();
|
||||
}
|
||||
|
||||
void AccountList::populateStubData() {
|
||||
struct {
|
||||
const char *name;
|
||||
QColor color;
|
||||
const char *letter;
|
||||
} accounts[] = {
|
||||
{"Alice", QColor(74, 144, 226), "A"},
|
||||
{"Bob", QColor(126, 211, 33), "B"},
|
||||
{"Carol", QColor(245, 166, 35), "C"},
|
||||
{"Dave", QColor(144, 19, 254), "D"},
|
||||
{"Eve", QColor(0, 184, 148), "E"},
|
||||
};
|
||||
|
||||
for (const auto &a : accounts) {
|
||||
auto *item = new QStandardItem();
|
||||
item->setText(a.name);
|
||||
item->setIcon(QIcon(makeAvatar(a.color, a.letter)));
|
||||
m_model->appendRow(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QListView>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
class AccountList : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AccountList(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
QListView *m_listView;
|
||||
QStandardItemModel *m_model;
|
||||
void populateStubData();
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
#include "channeldelegate.h"
|
||||
#include <QPainter>
|
||||
#include <QApplication>
|
||||
|
||||
ChannelDelegate::ChannelDelegate(QObject *parent)
|
||||
: QStyledItemDelegate(parent) {}
|
||||
|
||||
void ChannelDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const {
|
||||
painter->save();
|
||||
|
||||
if (option.state & QStyle::State_Selected) {
|
||||
painter->fillRect(option.rect, option.palette.highlight());
|
||||
} else if (option.state & QStyle::State_MouseOver) {
|
||||
painter->fillRect(option.rect, option.palette.light());
|
||||
}
|
||||
|
||||
const int margin = 8;
|
||||
const int iconSize = 32;
|
||||
QRect iconRect(option.rect.left() + margin,
|
||||
option.rect.top() + (option.rect.height() - iconSize) / 2,
|
||||
iconSize, iconSize);
|
||||
|
||||
QIcon icon = qvariant_cast<QIcon>(index.data(ChannelIconRole));
|
||||
if (!icon.isNull()) {
|
||||
icon.paint(painter, iconRect);
|
||||
}
|
||||
|
||||
QRect textRect(iconRect.right() + margin, option.rect.top() + margin,
|
||||
option.rect.right() - iconRect.right() - 2 * margin,
|
||||
option.rect.height() - 2 * margin);
|
||||
|
||||
QString name = index.data(ChannelNameRole).toString();
|
||||
QFont nameFont = option.font;
|
||||
nameFont.setBold(true);
|
||||
nameFont.setPointSize(nameFont.pointSize() + 1);
|
||||
painter->setFont(nameFont);
|
||||
|
||||
QColor textColor = (option.state & QStyle::State_Selected)
|
||||
? option.palette.highlightedText().color()
|
||||
: option.palette.text().color();
|
||||
painter->setPen(textColor);
|
||||
|
||||
QRect nameRect = textRect;
|
||||
nameRect.setHeight(QFontMetrics(nameFont).height());
|
||||
painter->drawText(nameRect, Qt::AlignLeft | Qt::AlignTop,
|
||||
painter->fontMetrics().elidedText(name, Qt::ElideRight, nameRect.width()));
|
||||
|
||||
QString lastMsg = index.data(ChannelLastMessageRole).toString();
|
||||
if (!lastMsg.isEmpty()) {
|
||||
QFont msgFont = option.font;
|
||||
msgFont.setPointSize(msgFont.pointSize() - 1);
|
||||
painter->setFont(msgFont);
|
||||
|
||||
QColor msgColor = (option.state & QStyle::State_Selected)
|
||||
? option.palette.highlightedText().color().darker(120)
|
||||
: QColor(128, 128, 128);
|
||||
painter->setPen(msgColor);
|
||||
|
||||
QRect msgRect(textRect.left(), nameRect.bottom() + 4,
|
||||
textRect.width(), option.rect.bottom() - nameRect.bottom() - margin);
|
||||
QString elidedMsg = painter->fontMetrics().elidedText(lastMsg, Qt::ElideRight, msgRect.width());
|
||||
painter->drawText(msgRect, Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, elidedMsg);
|
||||
}
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
QSize ChannelDelegate::sizeHint(const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const {
|
||||
Q_UNUSED(option);
|
||||
Q_UNUSED(index);
|
||||
return QSize(200, 56);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
enum ChannelDataRole {
|
||||
ChannelNameRole = Qt::DisplayRole,
|
||||
ChannelIconRole = Qt::DecorationRole,
|
||||
ChannelLastMessageRole = Qt::UserRole + 1
|
||||
};
|
||||
|
||||
class ChannelDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ChannelDelegate(QObject *parent = nullptr);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const override;
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "channellist.h"
|
||||
#include "channeldelegate.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
static QIcon makeChannelIcon(const QColor &color, const QString &letter) {
|
||||
QPixmap pix(32, 32);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setBrush(color);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawRoundedRect(0, 0, 32, 32, 8, 8);
|
||||
p.setPen(Qt::white);
|
||||
QFont f = p.font();
|
||||
f.setBold(true);
|
||||
f.setPointSize(14);
|
||||
p.setFont(f);
|
||||
p.drawText(QRect(0, 0, 32, 32), Qt::AlignCenter, letter);
|
||||
p.end();
|
||||
return QIcon(pix);
|
||||
}
|
||||
|
||||
ChannelList::ChannelList(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_listView(new QListView(this))
|
||||
, m_model(new QStandardItemModel(this))
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
auto *header = new QLabel("Channels", this);
|
||||
header->setAlignment(Qt::AlignCenter);
|
||||
header->setStyleSheet("font-weight: bold; padding: 8px; background: palette(window);");
|
||||
layout->addWidget(header);
|
||||
|
||||
m_listView->setModel(m_model);
|
||||
m_listView->setItemDelegate(new ChannelDelegate(this));
|
||||
m_listView->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_listView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_listView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
layout->addWidget(m_listView);
|
||||
|
||||
connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged,
|
||||
this, [this](const QModelIndex ¤t, const QModelIndex &) {
|
||||
if (current.isValid()) {
|
||||
emit channelSelected(current.row());
|
||||
}
|
||||
});
|
||||
|
||||
populateStubData();
|
||||
}
|
||||
|
||||
void ChannelList::populateStubData() {
|
||||
struct {
|
||||
const char *name;
|
||||
const char *lastMsg;
|
||||
QColor color;
|
||||
const char *letter;
|
||||
} channels[] = {
|
||||
{"#general", "Alice: Sounds good, let's deploy tomorrow morning.", QColor(74, 144, 226), "G"},
|
||||
{"#random", "Bob: Anyone seen the latest episode? It was amazing!", QColor(126, 211, 33), "R"},
|
||||
{"#dev-team", "Carol: I've pushed the fix for ETCP-42 to staging.", QColor(245, 166, 35), "D"},
|
||||
{"#announcements","Admin: Maintenance scheduled for Sunday 02:00 UTC.", QColor(208, 2, 27), "A"},
|
||||
{"#support", "Dave: Can someone help me configure the routing table? The BGP session keeps dropping after the handshake completes. I've checked the logs.", QColor(144, 19, 254), "S"},
|
||||
{"#offtopic", "Eve: Check out this new keyboard — Cherry MX Blue, 60%.", QColor(0, 184, 148), "O"},
|
||||
};
|
||||
|
||||
for (const auto &ch : channels) {
|
||||
auto *item = new QStandardItem();
|
||||
item->setData(ch.name, ChannelNameRole);
|
||||
item->setData(QString::fromUtf8(ch.lastMsg), ChannelLastMessageRole);
|
||||
item->setData(makeChannelIcon(ch.color, ch.letter), ChannelIconRole);
|
||||
m_model->appendRow(item);
|
||||
}
|
||||
|
||||
m_listView->setCurrentIndex(m_model->index(0, 0));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QListView>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
class ChannelList : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ChannelList(QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void channelSelected(int index);
|
||||
|
||||
private:
|
||||
QListView *m_listView;
|
||||
QStandardItemModel *m_model;
|
||||
void populateStubData();
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "chatview.h"
|
||||
#include <QPainter>
|
||||
#include <QDebug>
|
||||
|
||||
ChatView::ChatView(QWidget *parent)
|
||||
: QListView(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ChatView::setBackgroundImage(const QString &path) {
|
||||
if (!m_background.load(path)) {
|
||||
qWarning() << "ChatView: failed to load background" << path;
|
||||
return;
|
||||
}
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void ChatView::resizeEvent(QResizeEvent *event) {
|
||||
QListView::resizeEvent(event);
|
||||
scheduleDelayedItemsLayout();
|
||||
}
|
||||
|
||||
void ChatView::scrollContentsBy(int dx, int dy) {
|
||||
QListView::scrollContentsBy(dx, dy);
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void ChatView::paintEvent(QPaintEvent *event) {
|
||||
if (!m_background.isNull()) {
|
||||
QPainter p(viewport());
|
||||
p.drawTiledPixmap(viewport()->rect(), m_background);
|
||||
}
|
||||
QListView::paintEvent(event);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include <QListView>
|
||||
|
||||
class ChatView : public QListView {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ChatView(QWidget *parent = nullptr);
|
||||
|
||||
void setBackgroundImage(const QString &path);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void scrollContentsBy(int dx, int dy) override;
|
||||
|
||||
private:
|
||||
QPixmap m_background;
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
#include <QApplication>
|
||||
#include "mainwindow.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName("Chat");
|
||||
app.setQuitOnLastWindowClosed(false);
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "mainwindow.h"
|
||||
#include "channellist.h"
|
||||
#include "messagelist.h"
|
||||
#include "accountlist.h"
|
||||
#include <QSplitter>
|
||||
#include <QCloseEvent>
|
||||
#include <QApplication>
|
||||
#include <QStyle>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, m_channelList(nullptr)
|
||||
, m_messageList(nullptr)
|
||||
, m_accountList(nullptr)
|
||||
, m_trayIcon(nullptr)
|
||||
, m_trayMenu(nullptr)
|
||||
{
|
||||
setWindowTitle("Chat");
|
||||
resize(900, 600);
|
||||
|
||||
setupUi();
|
||||
setupTray();
|
||||
}
|
||||
|
||||
void MainWindow::setupUi() {
|
||||
auto *splitter = new QSplitter(Qt::Horizontal, this);
|
||||
|
||||
m_channelList = new ChannelList(splitter);
|
||||
m_messageList = new MessageList(splitter);
|
||||
m_accountList = new AccountList(splitter);
|
||||
|
||||
splitter->addWidget(m_channelList);
|
||||
splitter->addWidget(m_messageList);
|
||||
splitter->addWidget(m_accountList);
|
||||
|
||||
splitter->setStretchFactor(0, 2);
|
||||
splitter->setStretchFactor(1, 5);
|
||||
splitter->setStretchFactor(2, 1);
|
||||
|
||||
splitter->setSizes({220, 480, 180});
|
||||
|
||||
setCentralWidget(splitter);
|
||||
}
|
||||
|
||||
void MainWindow::setupTray() {
|
||||
m_trayIcon = new QSystemTrayIcon(this);
|
||||
m_trayIcon->setIcon(style()->standardIcon(QStyle::SP_ComputerIcon));
|
||||
m_trayIcon->setToolTip("Chat");
|
||||
|
||||
m_trayMenu = new QMenu(this);
|
||||
QAction *showAction = m_trayMenu->addAction("Show");
|
||||
connect(showAction, &QAction::triggered, this, [this]() {
|
||||
show();
|
||||
raise();
|
||||
activateWindow();
|
||||
});
|
||||
|
||||
QAction *quitAction = m_trayMenu->addAction("Quit");
|
||||
connect(quitAction, &QAction::triggered, qApp, &QApplication::quit);
|
||||
|
||||
m_trayIcon->setContextMenu(m_trayMenu);
|
||||
|
||||
connect(m_trayIcon, &QSystemTrayIcon::activated,
|
||||
this, &MainWindow::onTrayActivated);
|
||||
|
||||
m_trayIcon->show();
|
||||
}
|
||||
|
||||
void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
hide();
|
||||
m_trayIcon->showMessage("Chat", "Application minimized to tray.",
|
||||
QSystemTrayIcon::Information, 2000);
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) {
|
||||
if (reason == QSystemTrayIcon::Trigger ||
|
||||
reason == QSystemTrayIcon::DoubleClick) {
|
||||
show();
|
||||
raise();
|
||||
activateWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QMenu>
|
||||
|
||||
class ChannelList;
|
||||
class MessageList;
|
||||
class AccountList;
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow() override = default;
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void onTrayActivated(QSystemTrayIcon::ActivationReason reason);
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void setupTray();
|
||||
|
||||
ChannelList *m_channelList;
|
||||
MessageList *m_messageList;
|
||||
AccountList *m_accountList;
|
||||
QSystemTrayIcon *m_trayIcon;
|
||||
QMenu *m_trayMenu;
|
||||
};
|
||||
@@ -0,0 +1,251 @@
|
||||
#include "messagedelegate.h"
|
||||
#include <QPainter>
|
||||
#include <QPainterPath>
|
||||
#include <QApplication>
|
||||
#include <QAbstractItemView>
|
||||
#include <QFontMetrics>
|
||||
|
||||
MessageDelegate::MessageDelegate(QObject *parent)
|
||||
: QStyledItemDelegate(parent) {}
|
||||
|
||||
QSize MessageDelegate::sizeHint(const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const {
|
||||
Layout L = calcLayout(option, index);
|
||||
return QSize(L.viewWidth, L.totalHeight);
|
||||
}
|
||||
|
||||
static int measureTextHeight(const QFont &font, const QString &text, int width) {
|
||||
if (text.isEmpty() || width <= 0) return 0;
|
||||
QRect r = QFontMetrics(font).boundingRect(QRect(0, 0, width, 100000),
|
||||
Qt::AlignLeft | Qt::TextWordWrap, text);
|
||||
return r.height();
|
||||
}
|
||||
|
||||
MessageDelegate::Layout MessageDelegate::calcLayout(
|
||||
const QStyleOptionViewItem &option, const QModelIndex &index) const
|
||||
{
|
||||
Layout L{};
|
||||
L.viewWidth = 300;
|
||||
if (option.widget) {
|
||||
auto *v = qobject_cast<const QAbstractItemView *>(option.widget);
|
||||
if (v) L.viewWidth = v->viewport()->width();
|
||||
}
|
||||
if (L.viewWidth <= 0) L.viewWidth = 300;
|
||||
|
||||
bool hasQuote = index.data(MsgHasQuoteRole).toBool();
|
||||
QString text = index.data(MsgTextRole).toString();
|
||||
QString qText = hasQuote ? index.data(MsgQuoteTextRole).toString() : QString();
|
||||
|
||||
int bubbleW = L.viewWidth - kMarginH - kAvatar - kGap - kMarginH;
|
||||
if (bubbleW < 100) bubbleW = 100;
|
||||
L.bubbleWidth = bubbleW;
|
||||
|
||||
int textW = bubbleW - 2 * kPadH;
|
||||
|
||||
int qH = 0;
|
||||
if (hasQuote) {
|
||||
int qTextW = bubbleW - 2 * kPadH - kQuoteBarW - kQuotePadH;
|
||||
int qTextH = measureTextHeight(option.font, qText, qTextW);
|
||||
int qOneLineH = QFontMetrics(option.font).height();
|
||||
qH = kQuotePadV + qOneLineH + (qText.isEmpty() ? 0 : 2 + qTextH) + kQuotePadV;
|
||||
}
|
||||
|
||||
int textH = measureTextHeight(option.font, text, textW);
|
||||
int bubbleH = kPadTop + (hasQuote ? qH + kQuoteGap : 0) + textH + kStatusH + kPadBot;
|
||||
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap;
|
||||
|
||||
int contentLeft = kMarginH + kAvatar + kGap;
|
||||
|
||||
L.bubbleRect = QRect(contentLeft, 0, bubbleW, bubbleH);
|
||||
|
||||
int contentTop = kPadTop;
|
||||
|
||||
if (hasQuote) {
|
||||
L.quoteRect = QRect(contentLeft + kPadH, contentTop, bubbleW - 2 * kPadH, qH);
|
||||
L.quoteBarRect = QRect(L.quoteRect.left(), L.quoteRect.top(),
|
||||
kQuoteBarW, qH);
|
||||
int tx = L.quoteBarRect.right() + kQuotePadH;
|
||||
L.quoteAuthorRect = QRect(tx, L.quoteRect.top() + kQuotePadV,
|
||||
L.quoteRect.width() - kQuoteBarW - kQuotePadH,
|
||||
QFontMetrics(option.font).height());
|
||||
L.quoteTextRect = QRect(tx,
|
||||
L.quoteAuthorRect.bottom() + 2,
|
||||
L.quoteAuthorRect.width(),
|
||||
qH - kQuotePadV - QFontMetrics(option.font).height() - 2 - kQuotePadV);
|
||||
contentTop += qH + kQuoteGap;
|
||||
}
|
||||
|
||||
L.textRect = QRect(contentLeft + kPadH, contentTop,
|
||||
textW, textH);
|
||||
L.statusRect = QRect(contentLeft + kPadH,
|
||||
contentTop + textH + 2,
|
||||
textW, kStatusH);
|
||||
|
||||
int avatarY = L.bubbleRect.bottom() - kAvatar;
|
||||
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar);
|
||||
|
||||
return L;
|
||||
}
|
||||
|
||||
void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const {
|
||||
painter->save();
|
||||
painter->translate(option.rect.topLeft());
|
||||
painter->setClipRect(QRect(QPoint(0, 0), option.rect.size()));
|
||||
|
||||
Layout L = calcLayout(option, index);
|
||||
|
||||
drawBubble(painter, L);
|
||||
|
||||
if (index.data(MsgHasQuoteRole).toBool()) {
|
||||
drawQuote(painter, L, index, option);
|
||||
}
|
||||
|
||||
drawAvatar(painter, L, index);
|
||||
|
||||
painter->setPen(option.palette.text().color());
|
||||
QFont textFont = option.font;
|
||||
textFont.setPointSize(textFont.pointSize());
|
||||
painter->setFont(textFont);
|
||||
painter->drawText(L.textRect, Qt::AlignLeft | Qt::TextWordWrap,
|
||||
index.data(MsgTextRole).toString());
|
||||
|
||||
drawStatusBar(painter, L, index, option);
|
||||
|
||||
painter->restore();
|
||||
}
|
||||
|
||||
void MessageDelegate::drawQuote(QPainter *p, const Layout &L,
|
||||
const QModelIndex &index,
|
||||
const QStyleOptionViewItem &opt) const {
|
||||
p->save();
|
||||
|
||||
p->setPen(Qt::NoPen);
|
||||
p->setBrush(QColor(0xFF, 0xEE, 0xEE));
|
||||
p->drawRoundedRect(L.quoteRect, 5, 5);
|
||||
|
||||
p->setPen(Qt::NoPen);
|
||||
p->setBrush(QColor(0xE0, 0x40, 0x40));
|
||||
p->drawRect(L.quoteBarRect);
|
||||
|
||||
QFont boldFont = opt.font;
|
||||
boldFont.setBold(true);
|
||||
p->setFont(boldFont);
|
||||
p->setPen(QColor(0xE0, 0x40, 0x40));
|
||||
p->drawText(L.quoteAuthorRect, Qt::AlignLeft | Qt::AlignVCenter,
|
||||
index.data(MsgQuoteAuthorRole).toString());
|
||||
|
||||
p->setFont(opt.font);
|
||||
p->setPen(QColor(0x99, 0x55, 0x55));
|
||||
p->drawText(L.quoteTextRect, Qt::AlignLeft | Qt::TextWordWrap,
|
||||
index.data(MsgQuoteTextRole).toString());
|
||||
|
||||
p->restore();
|
||||
}
|
||||
|
||||
void MessageDelegate::drawBubble(QPainter *p, const Layout &L) const {
|
||||
p->save();
|
||||
p->setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
QRectF b = L.bubbleRect;
|
||||
qreal r = kRadius;
|
||||
qreal t = kTail;
|
||||
|
||||
QPainterPath body;
|
||||
body.addRoundedRect(b, r, r);
|
||||
|
||||
QPainterPath tail;
|
||||
qreal tx = b.left();
|
||||
qreal ty = b.bottom();
|
||||
tail.moveTo(tx, ty - r);
|
||||
tail.lineTo(tx - t, ty - t / 2.0);
|
||||
tail.lineTo(tx + r, ty);
|
||||
tail.closeSubpath();
|
||||
|
||||
QPainterPath combined = body.united(tail);
|
||||
|
||||
p->setPen(QPen(QColor(0xE0, 0xE0, 0xE0), 0.5));
|
||||
p->setBrush(QColor(255, 255, 255));
|
||||
p->drawPath(combined);
|
||||
|
||||
p->restore();
|
||||
}
|
||||
|
||||
void MessageDelegate::drawStatusBar(QPainter *p, const Layout &L,
|
||||
const QModelIndex &index,
|
||||
const QStyleOptionViewItem &opt) const {
|
||||
p->save();
|
||||
|
||||
QRect r = L.statusRect;
|
||||
|
||||
QVariant reactionsVar = index.data(MsgReactionsRole);
|
||||
QVariantList reactions = reactionsVar.toList();
|
||||
|
||||
int rx = r.left();
|
||||
if (!reactions.isEmpty()) {
|
||||
for (const QVariant &rv : reactions) {
|
||||
QVariantMap rm = rv.toMap();
|
||||
QString emoji = rm["emoji"].toString();
|
||||
int count = rm["count"].toInt();
|
||||
QVariant colorsVar = rm["colors"];
|
||||
QList<QColor> colors = colorsVar.value<QList<QColor>>();
|
||||
|
||||
if (count < 4 && !colors.isEmpty()) {
|
||||
int iconSize = 16;
|
||||
for (int i = colors.size() - 1; i >= 0; --i) {
|
||||
int cx = rx + i * 11;
|
||||
p->setPen(Qt::NoPen);
|
||||
p->setBrush(colors[i]);
|
||||
p->drawEllipse(cx, r.top() + 1, iconSize, iconSize);
|
||||
}
|
||||
rx += colors.size() * 11 + 8;
|
||||
} else {
|
||||
QFont emojiFont = opt.font;
|
||||
emojiFont.setPointSize(emojiFont.pointSize() - 1);
|
||||
p->setFont(emojiFont);
|
||||
p->setPen(opt.palette.text().color());
|
||||
QRect emojiRect(rx, r.top(), 20, r.height());
|
||||
p->drawText(emojiRect, Qt::AlignLeft | Qt::AlignVCenter, emoji);
|
||||
rx += 20;
|
||||
|
||||
QFont countFont = opt.font;
|
||||
countFont.setPointSize(countFont.pointSize() - 2);
|
||||
p->setFont(countFont);
|
||||
p->setPen(QColor(0x80, 0x80, 0x80));
|
||||
QRect cntRect(rx, r.top(), 24, r.height());
|
||||
p->drawText(cntRect, Qt::AlignLeft | Qt::AlignVCenter,
|
||||
QString::number(count));
|
||||
rx = cntRect.right() + 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QFont smallFont = opt.font;
|
||||
smallFont.setPointSize(smallFont.pointSize() - 2);
|
||||
p->setFont(smallFont);
|
||||
p->setPen(QColor(0x80, 0x80, 0x80));
|
||||
|
||||
QString statusText;
|
||||
if (index.data(MsgEditedRole).toBool()) {
|
||||
statusText = QString::fromUtf8("изменено ");
|
||||
}
|
||||
statusText += index.data(MsgTimeRole).toString();
|
||||
p->drawText(QRect(r.left(), r.top(), r.width(), r.height()),
|
||||
Qt::AlignRight | Qt::AlignVCenter, statusText);
|
||||
|
||||
p->restore();
|
||||
}
|
||||
|
||||
void MessageDelegate::drawAvatar(QPainter *p, const Layout &L,
|
||||
const QModelIndex &index) const {
|
||||
QPixmap av = index.data(MsgAvatarRole).value<QPixmap>();
|
||||
if (av.isNull()) return;
|
||||
|
||||
p->save();
|
||||
p->setRenderHint(QPainter::Antialiasing);
|
||||
QPainterPath clip;
|
||||
clip.addEllipse(L.avatarRect);
|
||||
p->setClipPath(clip);
|
||||
p->drawPixmap(L.avatarRect, av);
|
||||
p->restore();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
|
||||
enum MessageDataRole {
|
||||
MsgAuthorRole = Qt::UserRole + 1,
|
||||
MsgTextRole = Qt::UserRole + 2,
|
||||
MsgTimeRole = Qt::UserRole + 3,
|
||||
MsgEditedRole = Qt::UserRole + 4,
|
||||
MsgAvatarRole = Qt::UserRole + 5,
|
||||
MsgHasQuoteRole = Qt::UserRole + 6,
|
||||
MsgQuoteAuthorRole = Qt::UserRole + 7,
|
||||
MsgQuoteTextRole = Qt::UserRole + 8,
|
||||
MsgReactionsRole = Qt::UserRole + 9,
|
||||
};
|
||||
|
||||
class MessageDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MessageDelegate(QObject *parent = nullptr);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const override;
|
||||
|
||||
private:
|
||||
struct Layout {
|
||||
int viewWidth;
|
||||
int bubbleWidth;
|
||||
QRect avatarRect;
|
||||
QRect bubbleRect;
|
||||
QRect quoteRect;
|
||||
QRect quoteBarRect;
|
||||
QRect quoteAuthorRect;
|
||||
QRect quoteTextRect;
|
||||
QRect textRect;
|
||||
QRect statusRect;
|
||||
int totalHeight;
|
||||
};
|
||||
|
||||
Layout calcLayout(const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const;
|
||||
void drawQuote(QPainter *p, const Layout &L,
|
||||
const QModelIndex &index, const QStyleOptionViewItem &opt) const;
|
||||
void drawBubble(QPainter *p, const Layout &L) const;
|
||||
void drawStatusBar(QPainter *p, const Layout &L,
|
||||
const QModelIndex &index, const QStyleOptionViewItem &opt) const;
|
||||
void drawAvatar(QPainter *p, const Layout &L,
|
||||
const QModelIndex &index) const;
|
||||
|
||||
static constexpr int kAvatar = 36;
|
||||
static constexpr int kGap = 8;
|
||||
static constexpr int kRadius = 10;
|
||||
static constexpr int kPadH = 12;
|
||||
static constexpr int kPadTop = 8;
|
||||
static constexpr int kPadBot = 6;
|
||||
static constexpr int kTail = 6;
|
||||
static constexpr int kMarginH = 8;
|
||||
static constexpr int kItemGap = 3;
|
||||
static constexpr int kQuoteGap = 4;
|
||||
static constexpr int kQuotePadH = 6;
|
||||
static constexpr int kQuotePadV = 6;
|
||||
static constexpr int kQuoteBarW = 2;
|
||||
static constexpr int kQuoteAv = 16;
|
||||
static constexpr int kStatusH = 18;
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "messagelist.h"
|
||||
#include "chatview.h"
|
||||
#include "messagedelegate.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
static QPixmap makeAvatar(const QColor &color, const QString &letter, int size = 36) {
|
||||
QPixmap pix(size, size);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
p.setRenderHint(QPainter::Antialiasing);
|
||||
p.setBrush(color);
|
||||
p.setPen(Qt::NoPen);
|
||||
p.drawEllipse(0, 0, size, size);
|
||||
p.setPen(Qt::white);
|
||||
QFont f = p.font();
|
||||
f.setBold(true);
|
||||
f.setPointSize(size / 3);
|
||||
p.setFont(f);
|
||||
p.drawText(QRect(0, 0, size, size), Qt::AlignCenter, letter);
|
||||
p.end();
|
||||
return pix;
|
||||
}
|
||||
|
||||
static QVariant makeReaction(const QString &emoji, int count,
|
||||
const QList<QColor> &colors = {}) {
|
||||
QVariantMap m;
|
||||
m["emoji"] = emoji;
|
||||
m["count"] = count;
|
||||
if (!colors.isEmpty())
|
||||
m["colors"] = QVariant::fromValue(colors);
|
||||
return m;
|
||||
}
|
||||
|
||||
MessageList::MessageList(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_view(new ChatView(this))
|
||||
, m_model(new QStandardItemModel(this))
|
||||
{
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
m_view->setModel(m_model);
|
||||
m_view->setItemDelegate(new MessageDelegate(this));
|
||||
m_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_view->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
|
||||
m_view->setFrameShape(QFrame::NoFrame);
|
||||
m_view->setBackgroundImage("../resources/bg.jpg");
|
||||
layout->addWidget(m_view);
|
||||
|
||||
populateStubData();
|
||||
}
|
||||
|
||||
static void setMsg(QStandardItem *item,
|
||||
const QString &author, const QString &text, const QString &time,
|
||||
const QPixmap &avatar, bool edited,
|
||||
bool hasQuote, const QString &qAuthor, const QString &qText,
|
||||
const QVariantList &reactions) {
|
||||
item->setData(author, MsgAuthorRole);
|
||||
item->setData(text, MsgTextRole);
|
||||
item->setData(time, MsgTimeRole);
|
||||
item->setData(edited, MsgEditedRole);
|
||||
item->setData(QVariant::fromValue(avatar), MsgAvatarRole);
|
||||
item->setData(hasQuote, MsgHasQuoteRole);
|
||||
if (hasQuote) {
|
||||
item->setData(qAuthor, MsgQuoteAuthorRole);
|
||||
item->setData(qText, MsgQuoteTextRole);
|
||||
}
|
||||
if (!reactions.isEmpty())
|
||||
item->setData(reactions, MsgReactionsRole);
|
||||
}
|
||||
|
||||
void MessageList::populateStubData() {
|
||||
QColor cAlice(74, 144, 226), cBob(126, 211, 33), cCarol(245, 166, 35);
|
||||
QColor cDave(144, 19, 254), cEve(0, 184, 148);
|
||||
auto avAlice = makeAvatar(cAlice, "A");
|
||||
auto avBob = makeAvatar(cBob, "B");
|
||||
auto avCarol = makeAvatar(cCarol, "C");
|
||||
auto avDave = makeAvatar(cDave, "D");
|
||||
|
||||
auto *m0 = new QStandardItem();
|
||||
setMsg(m0,
|
||||
QString::fromUtf8("Alice"),
|
||||
QString::fromUtf8("Hey team! How's the deployment going?"),
|
||||
"10:15", avAlice, false,
|
||||
false, QString(), QString(),
|
||||
{});
|
||||
m_model->appendRow(m0);
|
||||
|
||||
auto *m1 = new QStandardItem();
|
||||
setMsg(m1,
|
||||
QString::fromUtf8("Bob"),
|
||||
QString::fromUtf8("All green on staging. Running the final integration tests now."),
|
||||
"10:17", avBob, false,
|
||||
false, QString(), QString(),
|
||||
QVariantList{makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 2,
|
||||
{cAlice, cCarol})});
|
||||
m_model->appendRow(m1);
|
||||
|
||||
auto *m2 = new QStandardItem();
|
||||
setMsg(m2,
|
||||
QString::fromUtf8("Carol"),
|
||||
QString::fromUtf8("I've pushed the ETCP-42 fix to staging. BGP handshake should be stable now. Please review the patch when you get a chance."),
|
||||
"10:22", avCarol, true,
|
||||
false, QString(), QString(),
|
||||
QVariantList{
|
||||
makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 3,
|
||||
{cAlice, cBob, cDave}),
|
||||
makeReaction(QString::fromUtf8("\xE2\x9D\xA4\xEF\xB8\x8F"), 1,
|
||||
{cAlice})
|
||||
});
|
||||
m_model->appendRow(m2);
|
||||
|
||||
auto *m3 = new QStandardItem();
|
||||
setMsg(m3,
|
||||
QString::fromUtf8("Dave"),
|
||||
QString::fromUtf8("Wait — the BGP session is still flapping after the handshake. I'm seeing the keepalive timer fire but no route updates come through. Logs attached."),
|
||||
"10:25", avDave, false,
|
||||
true,
|
||||
QString::fromUtf8("Carol"),
|
||||
QString::fromUtf8("BGP handshake should be stable now"),
|
||||
QVariantList{
|
||||
makeReaction(QString::fromUtf8("\xF0\x9F\x98\xAE"), 1,
|
||||
{cBob})
|
||||
});
|
||||
m_model->appendRow(m3);
|
||||
|
||||
auto *m4 = new QStandardItem();
|
||||
setMsg(m4,
|
||||
QString::fromUtf8("Alice"),
|
||||
QString::fromUtf8("Reviewed the patch. LGTM. Let's merge and deploy to production tomorrow morning."),
|
||||
"10:28", avAlice, false,
|
||||
false, QString(), QString(),
|
||||
QVariantList{
|
||||
makeReaction(QString::fromUtf8("\xF0\x9F\x91\x8D"), 5),
|
||||
makeReaction(QString::fromUtf8("\xF0\x9F\x8E\x89"), 2,
|
||||
{cBob, cEve})
|
||||
});
|
||||
m_model->appendRow(m4);
|
||||
|
||||
auto *m5 = new QStandardItem();
|
||||
setMsg(m5,
|
||||
QString::fromUtf8("Bob"),
|
||||
QString::fromUtf8("Can you share the full logs for the BGP session? I'll dig into the keepalive timer logic."),
|
||||
"10:30", avBob, false,
|
||||
true,
|
||||
QString::fromUtf8("Dave"),
|
||||
QString::fromUtf8("keepalive timer fire but no route updates"),
|
||||
{});
|
||||
m_model->appendRow(m5);
|
||||
|
||||
auto *m6 = new QStandardItem();
|
||||
setMsg(m6,
|
||||
QString::fromUtf8("Carol"),
|
||||
QString::fromUtf8("Sounds good. Let's coordinate in #dev-team. I'll prepare the release notes."),
|
||||
"10:33", avCarol, false,
|
||||
false, QString(), QString(),
|
||||
{});
|
||||
m_model->appendRow(m6);
|
||||
|
||||
auto *m7 = new QStandardItem();
|
||||
setMsg(m7,
|
||||
QString::fromUtf8("Alice"),
|
||||
QString::fromUtf8("Deploy scheduled for tomorrow 10:00 UTC. All features are frozen — no more merges to main."),
|
||||
"10:35", avAlice, false,
|
||||
false, QString(), QString(),
|
||||
QVariantList{
|
||||
makeReaction(QString::fromUtf8("\xF0\x9F\x9A\x80"), 3,
|
||||
{cBob, cCarol, cDave}),
|
||||
makeReaction(QString::fromUtf8("\xE2\x9D\xA4\xEF\xB8\x8F"), 7)
|
||||
});
|
||||
m_model->appendRow(m7);
|
||||
|
||||
m_view->scrollToBottom();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
class ChatView;
|
||||
|
||||
class MessageList : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MessageList(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
ChatView *m_view;
|
||||
QStandardItemModel *m_model;
|
||||
void populateStubData();
|
||||
};
|
||||
Reference in new issue
Block a user