Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions panels/dock/taskmanager/rolecombinemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,23 @@ RoleCombineModel::RoleCombineModel(QAbstractItemModel* major, QAbstractItemModel
endRemoveColumns();
});

// forward modelReset from major source
connect(sourceModel(), &QAbstractItemModel::modelReset, this, [this, majorRoles, func]() {
beginResetModel();
m_indexMap.clear();
int rowCount = sourceModel()->rowCount();
int columnCount = sourceModel()->columnCount();
for (int i = 0; i < rowCount; i++) {
for (int j = 0; j < columnCount; j++) {
QModelIndex majorIndex = sourceModel()->index(i, j);
QModelIndex minorIndex = func(majorIndex.data(majorRoles), m_minor);
if (majorIndex.isValid() && minorIndex.isValid())
m_indexMap[qMakePair(i, j)] = qMakePair(minorIndex.row(), minorIndex.column());
}
}
endResetModel();
});

// connect changedSignal
connect(major, &QAbstractItemModel::dataChanged, this,
[this, majorRoles, func](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles){
Expand Down
1 change: 1 addition & 0 deletions panels/dock/taskmanager/rolegroupmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ void RoleGroupModel::setSourceModel(QAbstractItemModel *model)
m_rowMap.removeOne(sourceRows);
delete sourceRows;
endRemoveRows();
--i;
}
}
adjustMap(first, -((last - first) + 1));
Expand Down
8 changes: 8 additions & 0 deletions tests/panels/dock/taskmanager/combinemodela.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,11 @@ void TestModelA::removeData(DataA *data)
delete data;
endRemoveRows();
}

void TestModelA::resetModel()
{
beginResetModel();
qDeleteAll(m_list);
m_list.clear();
endResetModel();
}
1 change: 1 addition & 0 deletions tests/panels/dock/taskmanager/combinemodela.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class TestModelA : public QAbstractListModel

void addData(DataA *data);
void removeData(DataA *data);
void resetModel();

private:
QList<DataA*> m_list;
Expand Down
8 changes: 8 additions & 0 deletions tests/panels/dock/taskmanager/combinemodelb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,11 @@ void TestModelB::removeData(DataB *data)
delete data;
endRemoveRows();
}

void TestModelB::clear()
{
beginResetModel();
qDeleteAll(m_list);
m_list.clear();
endResetModel();
}
1 change: 1 addition & 0 deletions tests/panels/dock/taskmanager/combinemodelb.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class TestModelB : public QAbstractListModel

void addData(DataB *data);
void removeData(DataB *data);
void clear();


private:
Expand Down
94 changes: 94 additions & 0 deletions tests/panels/dock/taskmanager/rolecombinemodeltests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,97 @@ TEST(RoleCombineModel, ParentParameterHandlingFix)
EXPECT_EQ(model.rowCount(), 1);
EXPECT_TRUE(model.index(0, 0).isValid());
}

// ---- 验证 Bug: RoleCombineModel 不处理 source modelReset ----
// 当 major 源模型(如窗口 monitor)调用 beginResetModel/endResetModel 时,
// RoleCombineModel 应当重建 m_indexMap 并发出 modelReset 通知下游视图。
// 当前实现缺少 modelReset/layoutChanged 连接,导致 m_indexMap 残留旧映射、
// 下游 DockGlobalElementModel 不清理过期条目 -> 任务栏出现幽灵/重复图标。
TEST(RoleCombineModel, MajorModelResetForwarding)
{
TestModelA modelA;
TestModelB modelB;

auto combineFunc = [](QVariant data, QAbstractItemModel *model) -> QModelIndex {
auto matches = model->match(model->index(0, 0), TestModelB::idRole, data);
return matches.isEmpty() ? QModelIndex() : matches.first();
};

RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, combineFunc);

modelA.addData(new DataA(0, "a0", &modelA));
modelA.addData(new DataA(1, "a1", &modelA));
modelB.addData(new DataB(0, "b0", &modelB));
modelB.addData(new DataB(1, "b1", &modelB));

ASSERT_EQ(model.rowCount(), 2);

QSignalSpy resetSpy(&model, &QAbstractItemModel::modelReset);

// 模拟窗口 monitor 的 clear() 操作:beginResetModel + 清空 + endResetModel
modelA.resetModel();

// 源已清空,rowCount 应归零
EXPECT_EQ(model.rowCount(), 0);

// 必须发出 modelReset 信号,否则下游视图永远不知道行已消失
// BUG: 当前实现不会发出此信号,此断言会 FAIL
EXPECT_EQ(resetSpy.count(), 1) << "FAIL: RoleCombineModel did not forward modelReset from major source";

// 重新添加数据后,新行应正确映射,不应残留旧行
modelA.addData(new DataA(2, "a2", &modelA));
modelB.addData(new DataB(2, "b2", &modelB));

EXPECT_EQ(model.rowCount(), 1);
auto roleNames = model.roleNames();
auto roleNamesB = modelB.roleNames();
QHash<QByteArray, int> names2Role;
for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) {
names2Role.insert(it.value(), it.key());
}
int bDataRole = names2Role.value(roleNamesB.value(TestModelB::dataRole));
EXPECT_EQ(model.index(0, 0).data(bDataRole).toString(), "b2");
}

// ---- 验证 Bug: RoleCombineModel 不处理 minor modelReset ----
// 当 minor 源模型(apps 模型)复位时,RoleCombineModel 同样应重建映射。
TEST(RoleCombineModel, MinorModelResetForwarding)
{
TestModelA modelA;
TestModelB modelB;

auto combineFunc = [](QVariant data, QAbstractItemModel *model) -> QModelIndex {
auto matches = model->match(model->index(0, 0), TestModelB::idRole, data);
return matches.isEmpty() ? QModelIndex() : matches.first();
};

RoleCombineModel model(&modelA, &modelB, TestModelA::idRole, combineFunc);

modelA.addData(new DataA(0, "a0", &modelA));
modelA.addData(new DataA(1, "a1", &modelA));
modelB.addData(new DataB(0, "b0", &modelB));
modelB.addData(new DataB(1, "b1", &modelB));

ASSERT_EQ(model.rowCount(), 2);

// 模拟 apps 模型复位:清除所有数据并重新添加
// 当前实现没有连接 minor 的 modelReset,m_indexMap 中的映射会残留
// 这里只验证不会崩溃,以及重新添加后映射仍能正确工作
modelB.clear();
modelB.addData(new DataB(0, "b0_new", &modelB));
modelB.addData(new DataB(1, "b1_new", &modelB));
modelB.addData(new DataB(2, "b2_new", &modelB));

auto roleNames = model.roleNames();
auto roleNamesB = modelB.roleNames();
QHash<QByteArray, int> names2Role;
for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) {
names2Role.insert(it.value(), it.key());
}
int bDataRole = names2Role.value(roleNamesB.value(TestModelB::dataRole));

// 重新添加后,映射应能正确找到新数据
// BUG: m_indexMap 中的旧映射没有重建,数据和索引可能不匹配
EXPECT_EQ(model.index(0, 0).data(bDataRole).toString(), "b0_new");
EXPECT_EQ(model.index(1, 0).data(bDataRole).toString(), "b1_new");
}
102 changes: 102 additions & 0 deletions tests/panels/dock/taskmanager/rolegroupmodeltests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,105 @@ TEST(RoleGroupModel, ScrollingBoundaryTest)
EXPECT_FALSE(negativeChild.isValid());
}
}

// ---- 验证 Bug: RoleGroupModel::rowsRemoved 批量删除时跳过后续分组 ----
// 当一次 rowsRemoved 覆盖多行、且中间某个分组被删空时,该分组从 m_rowMap 移除后
// 循环下标前移,导致后续分组被跳过、其成员行未被移除,
// 随后 adjustMap 甚至可能把残留的源行号修正为负数。
TEST(RoleGroupModel, RowsRemovedRangeSkip)
{
QStandardItemModel model;
auto role = Qt::UserRole + 1;
RoleGroupModel groupModel(&model, role);

// 构造 3 个分组:g0=[0], g1=[2], g2=[3,4](行 1 为空数据, 不分组)
QStandardItem *g0a = new QStandardItem;
g0a->setData(QString("g0"), role);
model.appendRow(g0a); // source row 0

model.appendRow(new QStandardItem); // source row 1, empty -> not grouped

QStandardItem *g1a = new QStandardItem;
g1a->setData(QString("g1"), role);
model.appendRow(g1a); // source row 2

QStandardItem *g2a = new QStandardItem;
g2a->setData(QString("g2"), role);
model.appendRow(g2a); // source row 3

QStandardItem *g2b = new QStandardItem;
g2b->setData(QString("g2"), role);
model.appendRow(g2b); // source row 4

ASSERT_EQ(groupModel.rowCount(), 3); // g0, g1, g2
ASSERT_EQ(groupModel.rowCount(groupModel.index(0, 0)), 1); // g0: 1 child
ASSERT_EQ(groupModel.rowCount(groupModel.index(1, 0)), 1); // g1: 1 child
ASSERT_EQ(groupModel.rowCount(groupModel.index(2, 0)), 2); // g2: 2 children

// 删除 source rows 0..2:g0 的唯一子项被删(g0 变空),g1 的唯一子项被删
// BUG: g0 变空后从 m_rowMap 移除,循环下标前移,g2 中的 row 3 不会被处理
// 而且 g2 中的 row 2 残留,adjustMap 后变成负数。
model.removeRows(0, 3);

// g2 应保留,有 2 个子项(原 row 3,4 调整后变为 row 0,1)
EXPECT_EQ(groupModel.rowCount(), 1) << "FAIL: expected 1 group (g2), got " << groupModel.rowCount();

if (groupModel.rowCount() > 0) {
auto g2Idx = groupModel.index(0, 0);
int childCount = groupModel.rowCount(g2Idx);
EXPECT_EQ(childCount, 2) << "FAIL: g2 should have 2 children, got " << childCount;

// 验证 mapToSource 返回的索引都在有效范围内
for (int i = 0; i < childCount; ++i) {
auto child = groupModel.index(i, 0, g2Idx);
auto src = groupModel.mapToSource(child);
EXPECT_TRUE(src.isValid()) << "FAIL: child " << i << " maps to invalid source row";
EXPECT_LT(src.row(), model.rowCount()) << "FAIL: child " << i << " source row out of range";
}
}
}

// ---- 验证 Bug: RoleGroupModel 发出带有效 parent 的子级 dataChanged ----
// 当源模型中分组内某行的非去重角色数据改变时,RoleGroupModel 发出的 dataChanged
// 是一个子级索引(parent 有效)。下游的 DockItemModel 在分组模式下直接取
// topLeft.row() 作为顶层行号,导致刷新到错误的行或越界。
TEST(RoleGroupModel, ChildDataChangedHasValidParent)
{
QStandardItemModel model;
auto role = Qt::UserRole + 1;
RoleGroupModel groupModel(&model, role);

// 构造一个分组 "app" 包含 2 个子项
QStandardItem *a = new QStandardItem;
a->setData(QString("app"), role);
model.appendRow(a);

QStandardItem *b = new QStandardItem;
b->setData(QString("app"), role);
model.appendRow(b);

ASSERT_EQ(groupModel.rowCount(), 1);
auto groupIdx = groupModel.index(0, 0);
ASSERT_EQ(groupModel.rowCount(groupIdx), 2);

// 监听 dataChanged 信号,检查 child 索引的 parent 是否有效
QSignalSpy spy(&groupModel, &QAbstractItemModel::dataChanged);

// 改变非去重角色(Qt::DisplayRole),不触发去重重建
model.setData(model.index(1, 0), QVariant("new-title"), Qt::DisplayRole);

bool sawChildWithValidParent = false;
for (const auto &args : spy) {
if (args.size() >= 1) {
QModelIndex tl = args[0].value<QModelIndex>();
if (tl.parent().isValid()) {
sawChildWithValidParent = true;
break;
}
}
}

// BUG: 子级 dataChanged 会被下游 DockItemModel 错误地转发为顶层行号
EXPECT_TRUE(sawChildWithValidParent)
<< "FAIL: RoleGroupModel should emit child dataChanged with valid parent";
}
Loading