20 changed files with 1089 additions and 0 deletions
@ -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 |
||||||
@ -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 |
||||||
|
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(); |
||||||
|
}; |
||||||
Loading…
Reference in new issue