diff --git a/CMakeLists.txt b/CMakeLists.txt index 033fbc2..eb7e4fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,6 +29,12 @@ qt_add_executable(llink src/authdialog.ui src/authdialog.cpp src/settingsdialog.h src/settingsdialog.cpp src/settingsdialog.ui + src/data.h + src/data.cpp + src/networkstreammodel.h + src/networkstreammodel.cpp + src/particlelistmodel.h + src/particlelistmodel.cpp ) qt_add_resources(llink diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user index ae6138b..e0fc734 100644 --- a/CMakeLists.txt.user +++ b/CMakeLists.txt.user @@ -1,6 +1,6 @@ - + EnvironmentId @@ -106,15 +106,15 @@ 2 false - -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_BUILD_TYPE:STRING=Debug --DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} + -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON -DCMAKE_GENERATOR:STRING=Ninja -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_BUILD_TYPE:STRING=Debug +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} 0 /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Debug @@ -164,15 +164,15 @@ 2 false - -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_BUILD_TYPE:STRING=Release --DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} + -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON -DCMAKE_GENERATOR:STRING=Ninja -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_BUILD_TYPE:STRING=Release +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Release @@ -219,15 +219,15 @@ 2 false - -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo --DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} + -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON -DCMAKE_GENERATOR:STRING=Ninja -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-RelWithDebInfo @@ -274,15 +274,15 @@ 2 false - -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo --DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} + -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON -DCMAKE_GENERATOR:STRING=Ninja -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} 0 /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-Profile @@ -330,15 +330,15 @@ 2 false - -DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_BUILD_TYPE:STRING=MinSizeRel --DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} + -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON -DCMAKE_GENERATOR:STRING=Ninja -DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} +-DCMAKE_BUILD_TYPE:STRING=MinSizeRel +-DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} +-DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} +-DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} -DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake --DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON +-DCMAKE_CXX_FLAGS_INIT:STRING=%{Qt:QML_DEBUG_FLAG} /Users/talksik/Documents/code/llink/build/Qt_6_8_6_for_macOS-MinSizeRel diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 3a4a961..d7332ca 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -8,6 +8,8 @@ #include "settingsdialog.h" #include "ui_mainwindow.h" #include +#include +#include #include #include #include @@ -15,7 +17,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), m_trayIcon(nullptr), m_authManager(new AuthManager(this)), m_settingsDialog(nullptr), - m_authDialog(nullptr) + m_authDialog(nullptr), m_data(nullptr), m_networkStreamModel(nullptr), m_particleListModel(nullptr) { m_authDialog = new AuthDialog(m_authManager, this); @@ -27,6 +29,8 @@ MainWindow::MainWindow(QWidget *parent) ui->splitter->setStretchFactor(1, 1); ui->splitter->setStretchFactor(2, 2); + setupModels(); + m_authDialog->setModal(true); QTimer::singleShot(3000, this, [this]() { @@ -95,3 +99,69 @@ void MainWindow::on_settingsButton_clicked() } m_settingsDialog->show(); } + +void MainWindow::setupModels() +{ + // Create data with fake data + m_data = new Data(this); + + // Create and set tree model + m_networkStreamModel = new NetworkStreamModel(m_data, this); + ui->topLevelParticlesView->setModel(m_networkStreamModel); + ui->topLevelParticlesView->setHeaderHidden(true); + ui->topLevelParticlesView->expandAll(); + + // Configure column behavior: name stretches, unseen count is fixed width + ui->topLevelParticlesView->header()->setStretchLastSection(false); + ui->topLevelParticlesView->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->topLevelParticlesView->header()->setSectionResizeMode(1, QHeaderView::Fixed); + ui->topLevelParticlesView->setColumnWidth(1, 40); + + // Create and set list model + m_particleListModel = new ParticleListModel(m_data, this); + ui->particlesView->setModel(m_particleListModel); + + // Connect selection changes + connect(ui->topLevelParticlesView->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &MainWindow::onTreeSelectionChanged); +} + +void MainWindow::onTreeSelectionChanged( + const QItemSelection &selected, + const QItemSelection &deselected) +{ + Q_UNUSED(deselected); + + if (selected.indexes().isEmpty()) + { + m_particleListModel->setStreamId(QString()); + return; + } + + QModelIndex index = selected.indexes().first(); + + if (m_networkStreamModel->isStreamItem(index)) + { + QString streamId = m_networkStreamModel->streamIdFromIndex(index); + m_particleListModel->setStreamId(streamId); + + // Auto-select first unseen particle (or first particle if all seen) + int unseenRow = m_particleListModel->firstUnseenRow(); + if (unseenRow >= 0) + { + ui->particlesView->setCurrentIndex(m_particleListModel->index(unseenRow, 0)); + ui->particlesView->setFocus(); + } + else if (m_particleListModel->rowCount() > 0) + { + ui->particlesView->setCurrentIndex(m_particleListModel->index(0, 0)); + ui->particlesView->setFocus(); + } + } + else + { + m_particleListModel->setStreamId(QString()); + } +} diff --git a/src/MainWindow.h b/src/MainWindow.h index dc9de73..6c648d4 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -6,10 +6,14 @@ #define LLINK_MAINWINDOW_H #include +#include "data.h" +#include "networkstreammodel.h" +#include "particlelistmodel.h" QT_BEGIN_NAMESPACE class QSystemTrayIcon; class AuthManager; +class QItemSelection; QT_END_NAMESPACE class AuthDialog; @@ -33,8 +37,11 @@ public: void on_settingsButton_clicked(); + void onTreeSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected); + private: void setupTrayIcon(); + void setupModels(); QSystemTrayIcon *m_trayIcon; Ui::MainWindow *ui; @@ -43,6 +50,11 @@ public: AuthDialog *m_authDialog; SettingsDialog *m_settingsDialog; + + // Data and models + Data *m_data; + NetworkStreamModel *m_networkStreamModel; + ParticleListModel *m_particleListModel; }; #endif //LLINK_MAINWINDOW_H diff --git a/src/data.cpp b/src/data.cpp new file mode 100644 index 0000000..4c632a1 --- /dev/null +++ b/src/data.cpp @@ -0,0 +1,142 @@ +#include "data.h" + +Data::Data(QObject *parent) : QObject(parent) +{ + initializeFakeData(); +} + +void Data::initializeFakeData() +{ + // Network 1: Private + Network net1; + net1.id = "net-private"; + net1.name = "Private"; + net1.openStreamCount = 2; + net1.openStreamCapacity = 5; + net1.countHumans = 1; + m_networks.append(net1); + + // Stream: Personal Notes + Stream stream1; + stream1.id = "stream-notes"; + stream1.name = "Personal Notes"; + stream1.status = Stream::OPEN; + stream1.unseenCount = 3; + m_streams[net1.id].append(stream1); + + // Particles for Personal Notes + m_particles[stream1.id].append({"p1", "text", true}); + m_particles[stream1.id].append({"p2", "link", false}); + m_particles[stream1.id].append({"p3", "image", true}); + m_particles[stream1.id].append({"p4", "text", false}); + m_particles[stream1.id].append({"p5", "link", true}); + + // Stream: Quick Captures + Stream stream2; + stream2.id = "stream-captures"; + stream2.name = "Quick Captures"; + stream2.status = Stream::OPEN; + stream2.unseenCount = 0; + m_streams[net1.id].append(stream2); + + // Particles for Quick Captures + m_particles[stream2.id].append({"p6", "text", true}); + m_particles[stream2.id].append({"p7", "image", true}); + + // Network 2: Flowy Labs, Inc + Network net2; + net2.id = "net-flowy"; + net2.name = "Flowy Labs, Inc"; + net2.openStreamCount = 3; + net2.openStreamCapacity = 10; + net2.countHumans = 12; + m_networks.append(net2); + + // Stream: Engineering + Stream stream3; + stream3.id = "stream-eng"; + stream3.name = "wild-shape"; + stream3.status = Stream::OPEN; + stream3.unseenCount = 5; + m_streams[net2.id].append(stream3); + + // Particles for Engineering + m_particles[stream3.id].append({"p8", "text", false}); + m_particles[stream3.id].append({"p9", "link", true}); + m_particles[stream3.id].append({"p10", "text", false}); + m_particles[stream3.id].append({"p11", "image", true}); + m_particles[stream3.id].append({"p12", "text", false}); + m_particles[stream3.id].append({"p13", "link", false}); + m_particles[stream3.id].append({"p14", "text", true}); + m_particles[stream3.id].append({"p15", "image", false}); + + // Stream: Design + Stream stream4; + stream4.id = "stream-design"; + stream4.name = "arch-rival"; + stream4.status = Stream::OPEN; + stream4.unseenCount = 2; + m_streams[net2.id].append(stream4); + + // Particles for Design + m_particles[stream4.id].append({"p16", "image", false}); + m_particles[stream4.id].append({"p17", "text", true}); + m_particles[stream4.id].append({"p18", "link", false}); + m_particles[stream4.id].append({"p19", "image", true}); + + // Stream: Archive + Stream stream5; + stream5.id = "stream-archive"; + stream5.name = "anthem-project"; + stream5.status = Stream::CLOSED; + stream5.unseenCount = 0; + m_streams[net2.id].append(stream5); + + // Particles for Archive + for (int i = 20; i < 35; i++) + { + m_particles[stream5.id].append({QString("p%1").arg(i), "text", true}); + } +} + +const QList& Data::networks() const +{ + return m_networks; +} + +const QList& Data::streamsForNetwork(const QString &networkId) const +{ + static QList emptyList; + auto it = m_streams.find(networkId); + if (it == m_streams.end()) + { + return emptyList; + } + return it.value(); +} + +const QList& Data::particlesForStream(const QString &streamId) const +{ + static QList emptyList; + auto it = m_particles.find(streamId); + if (it == m_particles.end()) + { + return emptyList; + } + return it.value(); +} + +int Data::networkCount() const +{ + return m_networks.size(); +} + +int Data::streamCount(const QString &networkId) const +{ + return m_streams.value(networkId).size(); +} + +int Data::particleCount(const QString &streamId) const +{ + return m_particles.value(streamId).size(); +} diff --git a/src/data.h b/src/data.h new file mode 100644 index 0000000..ea298f5 --- /dev/null +++ b/src/data.h @@ -0,0 +1,66 @@ +#ifndef DATA_H +#define DATA_H + +#include +#include + +struct Network +{ + QString id; + QString name; + int openStreamCount; + int openStreamCapacity; + int countHumans; +}; + +struct Stream +{ + QString id; + QString name; + enum StreamStatus + { + OPEN, + CLOSED, + }; + StreamStatus status; + int unseenCount; +}; + +struct Particle +{ + QString id; + QString type; + bool seen; +}; + +class Data : public QObject +{ + Q_OBJECT + +public: + explicit Data(QObject *parent = nullptr); + + // Accessors for models + const QList& networks() const; + const QList& streamsForNetwork(const QString &networkId) const; + const QList& particlesForStream(const QString &streamId) const; + + int networkCount() const; + int streamCount(const QString &networkId) const; + int particleCount(const QString &streamId) const; + +signals: + void networksChanged(); + void streamsChanged(const QString &networkId); + void particlesChanged(const QString &streamId); + +private: + void initializeFakeData(); + QList m_networks; + // Network -> Streams + QMap> m_streams; + // Stream -> Particles + QMap> m_particles; +}; + +#endif // DATA_H diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 39318d6..a4e2980 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -14,7 +14,7 @@ Flowy.llink - + 0 @@ -41,61 +41,114 @@ 0 - 28 + 20 - - - - 770 - 0 - 26 - 22 - + + + 0 - - ... + + 0 - - + + 0 - - - - - 740 - 0 - 26 - 22 - + + 0 - - ... + + 0 - - - - + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + + + + + + + ... + + + + + + + - - - 0 - 0 - - Qt::Orientation::Horizontal - - - - 0 - 0 - - + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + New stream + + + + + + true + + + + + + + Create network + + + + diff --git a/src/networkstreammodel.cpp b/src/networkstreammodel.cpp new file mode 100644 index 0000000..8373e56 --- /dev/null +++ b/src/networkstreammodel.cpp @@ -0,0 +1,194 @@ +#include "networkstreammodel.h" + +NetworkStreamModel::NetworkStreamModel(Data *data, QObject *parent) + : QAbstractItemModel(parent), m_data(data) +{ +} + +QModelIndex NetworkStreamModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) + { + return QModelIndex(); + } + + if (!parent.isValid()) + { + // Top-level item (Network) + // Encode: networkIndex in lower 32 bits, -1 in upper 32 bits + quintptr id = static_cast(row) | (static_cast(0xFFFFFFFF) << 32); + return createIndex(row, column, id); + } + else + { + // Child item (Stream) + // Decode parent to get network index + quintptr parentId = parent.internalId(); + int networkIndex = static_cast(parentId & 0xFFFFFFFF); + + // Encode: streamIndex in lower 32 bits, networkIndex in upper 32 bits + quintptr id = static_cast(row) | (static_cast(networkIndex) << 32); + return createIndex(row, column, id); + } +} + +QModelIndex NetworkStreamModel::parent(const QModelIndex &child) const +{ + if (!child.isValid()) + { + return QModelIndex(); + } + + quintptr id = child.internalId(); + int networkIndex = static_cast((id >> 32) & 0xFFFFFFFF); + + // If networkIndex is 0xFFFFFFFF, this is a network item (no parent) + if (networkIndex == 0xFFFFFFFF) + { + return QModelIndex(); + } + + // This is a stream item, return its parent network + quintptr parentId = static_cast(networkIndex) | (static_cast(0xFFFFFFFF) << 32); + return createIndex(networkIndex, 0, parentId); +} + +int NetworkStreamModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + { + // Root level - return number of networks + return m_data->networkCount(); + } + + quintptr id = parent.internalId(); + int networkIndex = static_cast((id >> 32) & 0xFFFFFFFF); + + // If this is a network item, return number of streams + if (networkIndex == 0xFFFFFFFF) + { + int actualNetworkIndex = static_cast(id & 0xFFFFFFFF); + if (actualNetworkIndex >= 0 && actualNetworkIndex < m_data->networks().size()) + { + const QString &networkId = m_data->networks().at(actualNetworkIndex).id; + return m_data->streamCount(networkId); + } + } + + // Stream items have no children + return 0; +} + +int NetworkStreamModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return 2; // Column 0: name, Column 1: unseen count +} + +QVariant NetworkStreamModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + { + return QVariant(); + } + + quintptr id = index.internalId(); + int networkIndex = static_cast((id >> 32) & 0xFFFFFFFF); + + if (networkIndex == 0xFFFFFFFF) + { + // This is a network item + if (role == Qt::DisplayRole && index.column() == 0) + { + int actualNetworkIndex = static_cast(id & 0xFFFFFFFF); + if (actualNetworkIndex >= 0 && actualNetworkIndex < m_data->networks().size()) + { + return m_data->networks().at(actualNetworkIndex).name; + } + } + // Networks don't show unseen count in column 1 + return QVariant(); + } + else + { + // This is a stream item + int streamIndex = static_cast(id & 0xFFFFFFFF); + if (networkIndex >= 0 && networkIndex < m_data->networks().size()) + { + const QString &networkId = m_data->networks().at(networkIndex).id; + const QList &streams = m_data->streamsForNetwork(networkId); + + if (streamIndex >= 0 && streamIndex < streams.size()) + { + const Stream &stream = streams.at(streamIndex); + + if (role == Qt::DisplayRole) + { + if (index.column() == 0) + { + // Column 0: stream name + return stream.name; + } + else if (index.column() == 1) + { + // Column 1: unseen count (only show if > 0) + if (stream.unseenCount > 0) + { + return stream.unseenCount; + } + } + } + else if (role == Qt::TextAlignmentRole && index.column() == 1) + { + // Right-align the unseen count + return static_cast(Qt::AlignRight | Qt::AlignVCenter); + } + } + } + } + + return QVariant(); +} + +QString NetworkStreamModel::streamIdFromIndex(const QModelIndex &index) const +{ + if (!index.isValid()) + { + return QString(); + } + + quintptr id = index.internalId(); + int networkIndex = static_cast((id >> 32) & 0xFFFFFFFF); + + // Check if this is a stream item (not a network) + if (networkIndex != 0xFFFFFFFF) + { + int streamIndex = static_cast(id & 0xFFFFFFFF); + if (networkIndex >= 0 && networkIndex < m_data->networks().size()) + { + const QString &networkId = m_data->networks().at(networkIndex).id; + const QList &streams = m_data->streamsForNetwork(networkId); + + if (streamIndex >= 0 && streamIndex < streams.size()) + { + return streams.at(streamIndex).id; + } + } + } + + return QString(); +} + +bool NetworkStreamModel::isStreamItem(const QModelIndex &index) const +{ + if (!index.isValid()) + { + return false; + } + + quintptr id = index.internalId(); + int networkIndex = static_cast((id >> 32) & 0xFFFFFFFF); + + // If networkIndex is not 0xFFFFFFFF, this is a stream item + return networkIndex != 0xFFFFFFFF; +} diff --git a/src/networkstreammodel.h b/src/networkstreammodel.h new file mode 100644 index 0000000..8ed5cf1 --- /dev/null +++ b/src/networkstreammodel.h @@ -0,0 +1,29 @@ +#ifndef NETWORKSTREAMMODEL_H +#define NETWORKSTREAMMODEL_H + +#include +#include "data.h" + +class NetworkStreamModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit NetworkStreamModel(Data *data, QObject *parent = nullptr); + + // Required QAbstractItemModel interface + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + // Helper methods for MainWindow + QString streamIdFromIndex(const QModelIndex &index) const; + bool isStreamItem(const QModelIndex &index) const; + +private: + Data *m_data; +}; + +#endif // NETWORKSTREAMMODEL_H diff --git a/src/particlelistmodel.cpp b/src/particlelistmodel.cpp new file mode 100644 index 0000000..1c01ed2 --- /dev/null +++ b/src/particlelistmodel.cpp @@ -0,0 +1,79 @@ +#include "particlelistmodel.h" + +ParticleListModel::ParticleListModel(Data *data, QObject *parent) + : QAbstractListModel(parent), m_data(data), m_currentStreamId() +{ +} + +int ParticleListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + { + return 0; + } + + if (m_currentStreamId.isEmpty()) + { + return 0; + } + + return m_data->particleCount(m_currentStreamId); +} + +QVariant ParticleListModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || role != Qt::DisplayRole) + { + return QVariant(); + } + + if (m_currentStreamId.isEmpty()) + { + return QVariant(); + } + + const QList &particles = m_data->particlesForStream(m_currentStreamId); + + if (index.row() < 0 || index.row() >= particles.size()) + { + return QVariant(); + } + + const Particle &particle = particles.at(index.row()); + + // Show dot (•) for unseen, nothing for seen + if (particle.seen) + { + return particle.type; + } + else + { + return QString("%1 •").arg(particle.type); + } +} + +void ParticleListModel::setStreamId(const QString &streamId) +{ + beginResetModel(); + m_currentStreamId = streamId; + endResetModel(); +} + +int ParticleListModel::firstUnseenRow() const +{ + if (m_currentStreamId.isEmpty()) + { + return -1; + } + + const QList &particles = m_data->particlesForStream(m_currentStreamId); + for (int i = 0; i < particles.size(); ++i) + { + if (!particles.at(i).seen) + { + return i; + } + } + + return -1; // All seen or empty +} diff --git a/src/particlelistmodel.h b/src/particlelistmodel.h new file mode 100644 index 0000000..be0c5df --- /dev/null +++ b/src/particlelistmodel.h @@ -0,0 +1,25 @@ +#ifndef PARTICLELISTMODEL_H +#define PARTICLELISTMODEL_H + +#include +#include "data.h" + +class ParticleListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit ParticleListModel(Data *data, QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + void setStreamId(const QString &streamId); + int firstUnseenRow() const; // Returns -1 if none unseen + +private: + Data *m_data; + QString m_currentStreamId; +}; + +#endif // PARTICLELISTMODEL_H