// // Created by arjun-flowylabs on 1/10/2026. // #include "MainWindow.h" #include "dialogs/authdialog.h" #include "authmanager.h" #include "dialogs/createstreamdialog.h" #include "dialogs/memberpickerdialog.h" #include "widgets/networkdetailwidget.h" #include "networkmanager.h" #include "dialogs/settingsdialog.h" #include "dialogs/textinputdialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), m_trayIcon(nullptr), m_authManager(new AuthManager(this)), m_settingsDialog(nullptr), m_authDialog(nullptr), m_store(nullptr), m_networkStreamModel(nullptr), m_particleListModel(nullptr), m_selectedParticleRow(-1), m_isDragging(false), m_textInputDialog(nullptr), m_createStreamDialog(nullptr), m_networkDetailWidget(nullptr) { m_authDialog = new AuthDialog(this); m_textInputDialog = new TextInputDialog(this); connect(m_textInputDialog, &TextInputDialog::textSubmitted, this, &MainWindow::onTextMessageSubmitted); buildUi(); m_topBar->setCursor(Qt::OpenHandCursor); // Enable frameless window for edge-to-edge experience setWindowFlags(Qt::FramelessWindowHint | Qt::Window); setAttribute(Qt::WA_TranslucentBackground, false); // Style for grey status bar text QString statusBarLabelStyle = "QLabel { color: #808080; }"; QString pingLabelStyle = "QLabel { color: #00AA00; }"; // Green for ping m_currentPathLabel = new QLabel(this); m_currentPathLabel->setMargin(5); m_currentPathLabel->setStyleSheet(statusBarLabelStyle); QFont font = QFont(); font.setStyle(QFont::StyleItalic); m_currentPathLabel->setFont(font); m_statusBar->addWidget(m_currentPathLabel); // Add spacer to push right-aligned items to the right m_statusBar->addPermanentWidget(new QLabel(""), 1); // Ping indicator (green text) m_pingLabel = new QLabel(this); m_pingLabel->setMargin(5); m_pingLabel->setMinimumWidth(100); m_pingLabel->setAlignment(Qt::AlignCenter); m_pingLabel->setStyleSheet(pingLabelStyle); m_statusBar->addPermanentWidget(m_pingLabel); // Position indicator (vim-style) m_positionLabel = new QLabel(this); m_positionLabel->setMargin(5); m_positionLabel->setMinimumWidth(50); m_positionLabel->setAlignment(Qt::AlignCenter); m_positionLabel->setStyleSheet(statusBarLabelStyle); m_statusBar->addPermanentWidget(m_positionLabel); // Storage indicator m_storageLabel = new QLabel(this); m_storageLabel->setMargin(5); m_storageLabel->setMinimumWidth(60); m_storageLabel->setAlignment(Qt::AlignCenter); m_storageLabel->setStyleSheet(statusBarLabelStyle); m_statusBar->addPermanentWidget(m_storageLabel); // Setup ping timer for periodic server connectivity check m_pingTimer = new QTimer(this); connect(m_pingTimer, &QTimer::timeout, this, []() { NetworkManager::instance().ping(); }); connect(&NetworkManager::instance(), &NetworkManager::pingResult, this, [this](int ms) { m_pingLabel->setText("connected " + QString::number(ms) + "ms"); m_pingLabel->setStyleSheet("QLabel { color: #00AA00; }"); }); connect(&NetworkManager::instance(), &NetworkManager::pingFailed, this, [this]() { m_pingLabel->setText("disconnected"); m_pingLabel->setStyleSheet("QLabel { color: #AA0000; }"); }); m_pingTimer->start(10000); NetworkManager::instance().ping(); m_splitter->setStretchFactor(0, 1); m_splitter->setStretchFactor(1, 4); m_splitter->setStretchFactor(2, 1); setupModels(); m_authDialog->setModal(true); connect(m_authManager, &AuthManager::codeSentToEmail, m_authDialog, &AuthDialog::onCodeSentToEmail); connect(m_authManager, &AuthManager::errorOccurred, m_authDialog, &AuthDialog::showError); connect(m_authManager, &AuthManager::signedIn, m_authDialog, &AuthDialog::hide); connect(m_authDialog, &AuthDialog::codeRequested, m_authManager, &AuthManager::requestSignInCode); connect(m_authDialog, &AuthDialog::signInRequested, m_authManager, &AuthManager::signIn); connect(m_authManager, &AuthManager::signedIn, m_authDialog, &AuthDialog::reset); connect(m_authManager, &AuthManager::signedOut, m_authDialog, &AuthDialog::reset); connect(m_authManager, &AuthManager::signedOut, m_authDialog, &AuthDialog::show); connect(m_authManager, &AuthManager::signedIn, this, &MainWindow::onSignedIn); connect(m_store, &Store::startupDataLoaded, m_treeView, &QTreeView::expandAll); connect(m_store, &Store::particleUpdated, this, &MainWindow::onParticleUpdated); // DocumentSend toolbar button opens text input dialog connect(m_sendTextButton, &QToolButton::clicked, this, &MainWindow::showTextInputDialog); // Explicit connects for buttons (no auto-connect without .ui) connect(m_settingsButton, &QToolButton::clicked, this, &MainWindow::on_settingsButton_clicked); connect(m_exitButton, &QToolButton::clicked, this, &MainWindow::on_exitButton_clicked); // "New stream" button m_createStreamDialog = new CreateStreamDialog(this); connect(m_newStreamButton, &QPushButton::clicked, this, &MainWindow::onNewStreamClicked); connect(m_createStreamDialog, &CreateStreamDialog::streamRequested, this, &MainWindow::onCreateStreamRequested); // "Add members" toolbar button (ListAdd icon in right sidebar) connect(m_addMembersButton, &QToolButton::clicked, this, &MainWindow::onAddMembersClicked); m_authManager->tryRestoreSession(); setupTrayIcon(); } void MainWindow::buildUi() { resize(650, 500); setWindowTitle("Flowy.llink"); auto *centralWidget = new QWidget(this); setCentralWidget(centralWidget); auto *mainLayout = new QVBoxLayout(centralWidget); mainLayout->setContentsMargins(0, 0, 0, 0); m_splitter = new QSplitter(Qt::Horizontal); mainLayout->addWidget(m_splitter); // ── Left sidebar ── m_leftSidebar = new QWidget(); m_leftSidebar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); auto *leftLayout = new QVBoxLayout(m_leftSidebar); leftLayout->setSpacing(5); leftLayout->setContentsMargins(2, 2, 2, 2); // Top bar (drag area) m_topBar = new QWidget(); m_topBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); m_topBar->setMinimumHeight(20); auto *topBarLayout = new QHBoxLayout(m_topBar); topBarLayout->setSpacing(0); topBarLayout->setContentsMargins(0, 0, 0, 0); m_settingsButton = new QToolButton(); m_settingsButton->setText("..."); m_settingsButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::HelpAbout)); topBarLayout->addWidget(m_settingsButton); topBarLayout->addStretch(); m_exitButton = new QToolButton(); m_exitButton->setText("..."); m_exitButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ApplicationExit)); topBarLayout->addWidget(m_exitButton); leftLayout->addWidget(m_topBar); // Tree view m_treeView = new QTreeView(); m_treeView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); leftLayout->addWidget(m_treeView); // New stream button m_newStreamButton = new QPushButton("New stream"); m_newStreamButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ListAdd)); leftLayout->addWidget(m_newStreamButton); m_splitter->addWidget(m_leftSidebar); // ── Content view ── m_contentView = new QWidget(); m_contentView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_contentView->setMinimumWidth(350); auto *contentLayout = new QVBoxLayout(m_contentView); contentLayout->setSpacing(2); contentLayout->setContentsMargins(0, 0, 0, 2); // Content container with spacers and labels auto *contentContainer = new QWidget(); contentContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); auto *contentContainerLayout = new QVBoxLayout(contentContainer); contentContainerLayout->setSpacing(2); contentContainerLayout->setContentsMargins(0, 0, 0, 0); contentContainerLayout->addStretch(); m_particleContentLabel = new QLabel(); QFont contentFont; contentFont.setPointSize(36); m_particleContentLabel->setFont(contentFont); m_particleContentLabel->setMaximumSize(500, 300); m_particleContentLabel->setAlignment(Qt::AlignCenter); m_particleContentLabel->setWordWrap(true); m_particleContentLabel->setScaledContents(true); contentContainerLayout->addWidget(m_particleContentLabel); contentContainerLayout->addStretch(); m_creatorLabel = new QLabel(); QFont creatorFont; creatorFont.setPointSize(15); m_creatorLabel->setFont(creatorFont); m_creatorLabel->setAlignment(Qt::AlignCenter); contentContainerLayout->addWidget(m_creatorLabel); m_ackLabel = new QLabel(); m_ackLabel->setAlignment(Qt::AlignCenter); contentContainerLayout->addWidget(m_ackLabel); contentLayout->addWidget(contentContainer); // Content toolbar (empty, matches widget_4 from .ui) auto *contentToolbar = new QWidget(); auto *contentToolbarLayout = new QHBoxLayout(contentToolbar); contentToolbarLayout->setSpacing(2); contentToolbarLayout->setContentsMargins(8, 0, 0, 0); contentLayout->addWidget(contentToolbar); m_splitter->addWidget(m_contentView); // ── Right sidebar ── m_rightSidebar = new QWidget(); m_rightSidebar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); auto *rightLayout = new QVBoxLayout(m_rightSidebar); rightLayout->setSpacing(2); rightLayout->setContentsMargins(2, 2, 2, 2); // Particle list view m_particlesView = new QListView(); m_particlesView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); auto sp1 = m_particlesView->sizePolicy(); sp1.setVerticalStretch(2); m_particlesView->setSizePolicy(sp1); rightLayout->addWidget(m_particlesView); // Members list m_membersList = new QListWidget(); m_membersList->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); auto sp2 = m_membersList->sizePolicy(); sp2.setVerticalStretch(1); m_membersList->setSizePolicy(sp2); rightLayout->addWidget(m_membersList); // "Hold space to speak" label auto *holdSpaceLabel = new QLabel("Hold space to speak"); holdSpaceLabel->setAlignment(Qt::AlignCenter); QPalette holdPalette = holdSpaceLabel->palette(); QColor holdColor(153, 156, 145, 217); holdPalette.setColor(QPalette::WindowText, holdColor); holdSpaceLabel->setPalette(holdPalette); rightLayout->addWidget(holdSpaceLabel); // Empty toolbar row (widget_5) auto *emptyToolbar = new QWidget(); auto *emptyToolbarLayout = new QHBoxLayout(emptyToolbar); emptyToolbarLayout->setSpacing(2); emptyToolbarLayout->setContentsMargins(0, 0, 0, 0); rightLayout->addWidget(emptyToolbar); // RTL toolbar row (widget_3) auto *rtlToolbar = new QWidget(); rtlToolbar->setLayoutDirection(Qt::RightToLeft); auto *rtlToolbarLayout = new QHBoxLayout(rtlToolbar); rtlToolbarLayout->setSpacing(2); rtlToolbarLayout->setContentsMargins(0, 0, 0, 0); auto *editClearButton = new QToolButton(); editClearButton->setText("..."); editClearButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::EditClear)); editClearButton->setToolTip(QStringLiteral("Close stream")); rtlToolbarLayout->addWidget(editClearButton); m_addMembersButton = new QToolButton(); m_addMembersButton->setText("..."); m_addMembersButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ListAdd)); m_addMembersButton->setToolTip(QStringLiteral("Add members to stream")); rtlToolbarLayout->addWidget(m_addMembersButton); auto *insertImageButton = new QToolButton(); insertImageButton->setText("..."); insertImageButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::InsertImage)); insertImageButton->setToolTip(QStringLiteral("Attach files")); rtlToolbarLayout->addWidget(insertImageButton); m_sendTextButton = new QToolButton(); m_sendTextButton->setText("..."); m_sendTextButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::DocumentSend)); m_sendTextButton->setToolTip(QStringLiteral("Send text message")); rtlToolbarLayout->addWidget(m_sendTextButton); auto *cameraVideoButton = new QToolButton(); cameraVideoButton->setText("..."); cameraVideoButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::CameraVideo)); cameraVideoButton->setToolTip(QStringLiteral("Toggle camera mode")); rtlToolbarLayout->addWidget(cameraVideoButton); rightLayout->addWidget(rtlToolbar); m_splitter->addWidget(m_rightSidebar); QFrame* hSeparator = new QFrame(); hSeparator->setFrameShape(QFrame::HLine); hSeparator->setFrameShadow(QFrame::Plain); hSeparator->setStyleSheet("color: rgba(128, 128, 128, 0.3);"); hSeparator->setFixedHeight(1); mainLayout->addWidget(hSeparator); // ── Network detail widget (hidden by default) ── m_statusBar = new QStatusBar(); setStatusBar(m_statusBar); } void MainWindow::setupTrayIcon() { m_trayIcon = new QSystemTrayIcon(this); m_trayIcon->setIcon(QIcon(":/assets/logo.png")); m_trayIcon->setToolTip("llink"); QMenu *trayMenu = new QMenu(this); trayMenu->addAction("Show", this, &MainWindow::show); trayMenu->addAction("Quit", qApp, &QApplication::quit); m_trayIcon->setContextMenu(trayMenu); connect(m_trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger) { isVisible() ? hide() : show(); } }); m_trayIcon->show(); } MainWindow::~MainWindow() { } void MainWindow::on_exitButton_clicked() { qApp->exit(0); } void MainWindow::on_settingsButton_clicked() { if (!m_settingsDialog) { m_settingsDialog = new SettingsDialog(this); connect(m_settingsDialog, &SettingsDialog::signOutRequested, m_authManager, &AuthManager::signOut); connect(m_settingsDialog, &SettingsDialog::createNetworkRequested, this, [this](const QString &name) { m_store->createNetwork(name); }); } if (m_authManager->sessionData()) { m_settingsDialog->setAuthLabel("Hello, " + m_authManager->sessionData()->emailPrefix); } m_settingsDialog->show(); } void MainWindow::setupModels() { // Create store (data loaded after sign-in via loadStartupData) m_store = new Store(this); m_networkDetailWidget = new NetworkDetailWidget(this); m_splitter->addWidget(m_networkDetailWidget); m_splitter->setStretchFactor(3, 4); m_networkDetailWidget->hide(); connect(m_store, &Store::networksChanged, this, &MainWindow::onNetworksChanged); connect(m_networkDetailWidget, &NetworkDetailWidget::addMemberRequested, this, &MainWindow::onAddNetworkMemberRequested); // Create and set tree model m_networkStreamModel = new NetworkStreamModel(m_store, this); m_treeView->setModel(m_networkStreamModel); m_treeView->setIndentation(5); m_treeView->setHeaderHidden(true); m_treeView->expandAll(); // Configure column behavior: name stretches, unseen count is fixed width m_treeView->header()->setStretchLastSection(false); m_treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch); m_treeView->header()->setSectionResizeMode(1, QHeaderView::Fixed); m_treeView->setColumnWidth(1, 40); // Create and set list model m_particleListModel = new ParticleListModel(m_store, this); m_particlesView->setModel(m_particleListModel); // Configure particle list view to use ellipses for long text m_particlesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_particlesView->setTextElideMode(Qt::ElideRight); // Connect selection changes connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::onTreeSelectionChanged); // Connect particle selection changes connect(m_particlesView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::onParticleSelectionChanged); // Install event filter to capture key presses from child views m_treeView->installEventFilter(this); m_particlesView->installEventFilter(this); } void MainWindow::onSignedIn() { m_store->setCurrentUserEmail(m_authManager->sessionData()->email); m_store->loadStartupData(); } void MainWindow::onParticleUpdated(const QString &streamId, const QString &particleId) { Q_UNUSED(streamId); if (particleId != m_selectedParticleId) return; const Particle *particle = m_store->particleById(particleId); if (particle && !particle->ackedByEmails.empty()) { QStringList acked; for (const auto &email : particle->ackedByEmails) acked.append(email); m_ackLabel->setText("Acked by: " + acked.join(", ")); } else { m_ackLabel->setText(""); } } void MainWindow::onNewStreamClicked() { if (m_selectedNetworkId.isEmpty()) { return; } const Network *network = m_store->networkById(m_selectedNetworkId); if (network) { m_createStreamDialog->setNetworkMembers(network->members); } m_createStreamDialog->show(); m_createStreamDialog->raise(); m_createStreamDialog->activateWindow(); } void MainWindow::onCreateStreamRequested(const QString &name, const QString &visibility, const QStringList &members) { m_store->createStream(m_selectedNetworkId, name, "", visibility, members); } void MainWindow::onAddMembersClicked() { if (m_selectedStreamId.isEmpty() || m_selectedNetworkId.isEmpty()) { return; } const Network *network = m_store->networkById(m_selectedNetworkId); const Stream *stream = m_store->streamById(m_selectedStreamId); if (!network || !stream) { return; } MemberPickerDialog picker("Add Members", this); picker.setMembers(network->members, stream->memberEmails); if (picker.exec() == QDialog::Accepted) { QStringList emails = picker.selectedEmails(); if (!emails.isEmpty()) { auto *op = m_store->addStreamMembers(m_selectedStreamId, emails); connect(op, &Operation::success, this, [this](const QJsonDocument &) { auto *fetchOp = m_store->fetchStream(m_selectedStreamId); connect(fetchOp, &Operation::success, this, [this](const QJsonDocument &) { m_membersList->clear(); const Stream *s = m_store->streamById(m_selectedStreamId); if (s) { for (const QString &email : s->memberEmails) { m_membersList->addItem(email.split("@")[0]); } } }); }); } } } void MainWindow::onNetworksChanged() { if (!m_selectedNetworkId.isEmpty() && m_networkDetailWidget->isVisible()) { const Network *network = m_store->networkById(m_selectedNetworkId); if (network) { m_networkDetailWidget->setNetwork(*network, m_authManager->sessionData() ? m_authManager->sessionData()->email : QString()); } else { m_networkDetailWidget->clear(); } } } void MainWindow::onAddNetworkMemberRequested(const QString &networkId, const QString &email) { auto *op = m_store->addNetworkMembers(networkId, {email}); connect(op, &Operation::success, this, [this, networkId](const QJsonDocument &) { m_store->fetchNetwork(networkId); }); } void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected); if (selected.indexes().isEmpty()) { m_particleListModel->setStreamId(QString()); m_membersList->clear(); m_networkDetailWidget->hide(); m_contentView->show(); m_rightSidebar->show(); return; } QModelIndex index = selected.indexes().first(); if (m_networkStreamModel->isStreamItem(index)) { // Show normal 3-pane view m_networkDetailWidget->hide(); m_contentView->show(); m_rightSidebar->show(); QString streamId = m_networkStreamModel->streamIdFromIndex(index); m_particleListModel->setStreamId(streamId); m_selectedStreamId = streamId; // Also track the parent network QModelIndex parentIndex = m_networkStreamModel->parent(index); QString networkId = m_networkStreamModel->networkIdFromIndex(parentIndex); m_selectedNetworkId = networkId; // Show stream members m_membersList->clear(); const Stream *stream = m_store->streamById(streamId); if (stream) { for (const QString &email : stream->memberEmails) { m_membersList->addItem(email.split("@")[0]); } } // Auto-select first unseen particle (or first particle if all seen) int unseenRow = m_particleListModel->firstUnseenRow(); if (unseenRow >= 0) { m_particlesView->setCurrentIndex(m_particleListModel->index(unseenRow, 0)); m_particlesView->setFocus(); } else if (m_particleListModel->rowCount() > 0) { m_particlesView->setCurrentIndex(m_particleListModel->index(m_particleListModel->rowCount() - 1, 0)); m_particlesView->setFocus(); } } else // network selected { m_particleListModel->setStreamId(QString()); m_selectedStreamId.clear(); m_selectedParticleRow = -1; m_membersList->clear(); QString networkId = m_networkStreamModel->networkIdFromIndex(index); m_selectedNetworkId = networkId; // Show network detail widget const Network *network = m_store->networkById(networkId); if (network) { m_networkDetailWidget->setNetwork(*network, m_authManager->sessionData() ? m_authManager->sessionData()->email : QString()); } else { m_networkDetailWidget->clear(); } m_contentView->hide(); m_rightSidebar->hide(); m_networkDetailWidget->show(); } updateStatusBar(); } void MainWindow::onParticleSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected); if (selected.indexes().isEmpty()) { m_particleContentLabel->setText(""); m_creatorLabel->setText(""); m_ackLabel->setText(""); m_selectedParticleId.clear(); m_selectedParticleRow = -1; updateStatusBar(); return; } QModelIndex index = selected.indexes().first(); m_selectedParticleRow = index.row(); const Particle *particle = m_particleListModel->particleAtRow(index.row()); if (particle) { m_creatorLabel->setText(particle->createdByEmail); m_selectedParticleId = particle->id; if (particle->type == "text") { m_particleContentLabel->setText(particle->data.value("content").toString()); } else { m_particleContentLabel->setText(""); } // Show ack info if (!particle->ackedByEmails.empty()) { QStringList acked; for (const auto &email : particle->ackedByEmails) { acked.append(email); } m_ackLabel->setText("Acked by: " + acked.join(", ")); } else { m_ackLabel->setText(""); } // Mark seen on selection if (!particle->seen) { m_store->markParticleSeen(particle->id); } } updateStatusBar(); } void MainWindow::updateStatusBar() { QStringList pathComponents; // Add network if selected if (!m_selectedNetworkId.isEmpty()) { const Network *network = m_store->networkById(m_selectedNetworkId); if (network) { pathComponents << network->name; } } // Add stream if selected if (!m_selectedStreamId.isEmpty()) { const Stream *stream = m_store->streamById(m_selectedStreamId); if (stream) { pathComponents << stream->name; } } // Add particle if selected if (!m_selectedParticleId.isEmpty()) { const Particle *particle = m_store->particleById(m_selectedParticleId); if (particle) { pathComponents << "p_" + particle->id.right(5); } } m_currentPathLabel->setText(pathComponents.join(" > ")); // Update position indicator (vim-style) if (!m_selectedStreamId.isEmpty()) { int totalCount = m_particleListModel->rowCount(); if (totalCount == 0) { m_positionLabel->setText(""); } else if (totalCount == 1) { m_positionLabel->setText("All"); } else if (m_selectedParticleRow == 0) { m_positionLabel->setText("Top"); } else if (m_selectedParticleRow == totalCount - 1) { m_positionLabel->setText("Bot"); } else if (m_selectedParticleRow >= 0) { int percent = (m_selectedParticleRow * 100) / (totalCount - 1); m_positionLabel->setText(QString::number(percent) + "%"); } else { m_positionLabel->setText(""); } // Update data size indicator for selected particle if (!m_selectedParticleId.isEmpty()) { const Particle *particle = m_store->particleById(m_selectedParticleId); if (particle) { QJsonObject dataObj; for (auto it = particle->data.constBegin(); it != particle->data.constEnd(); ++it) { dataObj.insert(it.key(), QJsonValue::fromVariant(it.value())); } int bytes = QJsonDocument(dataObj).toJson(QJsonDocument::Compact).size(); if (bytes < 1024) m_storageLabel->setText(QString::number(bytes) + " B"); else m_storageLabel->setText(QString::number(bytes / 1024) + " kB"); } else { m_storageLabel->setText(""); } } else { m_storageLabel->setText(""); } } else { m_positionLabel->setText(""); m_storageLabel->setText(""); } } void MainWindow::showTextInputDialog() { if (m_selectedStreamId.isEmpty()) { return; } m_textInputDialog->show(); m_textInputDialog->raise(); m_textInputDialog->activateWindow(); } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { auto *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_T && keyEvent->modifiers() == Qt::NoModifier && !m_selectedStreamId.isEmpty()) { showTextInputDialog(); return true; } if (keyEvent->key() == Qt::Key_A && keyEvent->modifiers() == Qt::NoModifier && !m_selectedParticleId.isEmpty()) { m_store->ackParticle(m_selectedParticleId); return true; } } return QMainWindow::eventFilter(obj, event); } void MainWindow::onTextMessageSubmitted(const QString &text) { if (m_selectedStreamId.isEmpty()) { return; } auto *op = m_store->createParticle(m_selectedStreamId, "text", {{"content", text}}); connect(op, &Operation::success, this, [this](const QJsonDocument &) { int lastRow = m_particleListModel->rowCount() - 1; if (lastRow >= 0) { m_particlesView->setCurrentIndex(m_particleListModel->index(lastRow, 0)); m_particlesView->setFocus(); } }); } void MainWindow::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { // Check if the click is within the top bar widget area if (m_topBar && m_topBar->geometry().contains(event->pos())) { m_isDragging = true; setCursor(Qt::ClosedHandCursor); m_dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft(); event->accept(); return; } } QMainWindow::mousePressEvent(event); } void MainWindow::mouseMoveEvent(QMouseEvent *event) { if (m_isDragging && (event->buttons() & Qt::LeftButton)) { move(event->globalPosition().toPoint() - m_dragPosition); event->accept(); return; } QMainWindow::mouseMoveEvent(event); } void MainWindow::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_isDragging = false; unsetCursor(); } QMainWindow::mouseReleaseEvent(event); }