This commit is contained in:
yanxiatao 2026-06-26 15:20:48 +03:00 committed by GitHub
commit 1ec56b7f0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 193 additions and 5 deletions

View file

@ -145,6 +145,30 @@ QStringList InstanceList::getLinkedInstancesById(const QString& id) const
return linkedInstances; return linkedInstances;
} }
QStringList InstanceList::groupOrder() const
{
return m_groupOrder;
}
void InstanceList::moveGroup(const QString& group, int toIndex)
{
int fromIndex = m_groupOrder.indexOf(group);
if (fromIndex < 0)
return;
int size = m_groupOrder.size();
toIndex = qBound(0, toIndex, size);
if (fromIndex == toIndex)
return;
QString name = m_groupOrder.takeAt(fromIndex);
// After takeAt, insert position shifts if target was after the removed position
int insertAt = (fromIndex < toIndex) ? (toIndex - 1) : toIndex;
m_groupOrder.insert(insertAt, name);
emit groupOrderChanged();
saveGroupList();
}
int InstanceList::rowCount(const QModelIndex& parent) const int InstanceList::rowCount(const QModelIndex& parent) const
{ {
Q_UNUSED(parent); Q_UNUSED(parent);
@ -293,6 +317,8 @@ void InstanceList::deleteGroup(const GroupId& name)
} }
if (removed) if (removed)
saveGroupList(); saveGroupList();
m_groupOrder.removeAll(name);
emit groupOrderChanged();
} }
void InstanceList::renameGroup(const QString& src, const QString& dst) void InstanceList::renameGroup(const QString& src, const QString& dst)
@ -318,6 +344,10 @@ void InstanceList::renameGroup(const QString& src, const QString& dst)
} }
if (modified) if (modified)
saveGroupList(); saveGroupList();
int idx = m_groupOrder.indexOf(src);
if (idx >= 0)
m_groupOrder[idx] = dst;
emit groupOrderChanged();
} }
bool InstanceList::isGroupCollapsed(const QString& group) bool InstanceList::isGroupCollapsed(const QString& group)
@ -692,7 +722,11 @@ void InstanceList::increaseGroupCount(const QString& group)
if (group.isEmpty()) if (group.isEmpty())
return; return;
bool wasNew = !m_groupNameCache.contains(group);
++m_groupNameCache[group]; ++m_groupNameCache[group];
if (wasNew && !m_groupOrder.contains(group)) {
m_groupOrder.append(group);
}
} }
void InstanceList::decreaseGroupCount(const QString& group) void InstanceList::decreaseGroupCount(const QString& group)
@ -703,6 +737,7 @@ void InstanceList::decreaseGroupCount(const QString& group)
if (--m_groupNameCache[group] < 1) { if (--m_groupNameCache[group] < 1) {
m_groupNameCache.remove(group); m_groupNameCache.remove(group);
m_collapsedGroups.remove(group); m_collapsedGroups.remove(group);
m_groupOrder.removeAll(group);
} }
} }
@ -757,6 +792,16 @@ void InstanceList::saveGroupList()
ungrouped.insert("hidden", QJsonValue(true)); ungrouped.insert("hidden", QJsonValue(true));
toplevel.insert("ungrouped", ungrouped); toplevel.insert("ungrouped", ungrouped);
} }
// Save group order (deduplicate to prevent silent corruption)
QJsonArray orderArr;
QStringList savedOrder;
for (const auto& name : m_groupOrder) {
if (!savedOrder.contains(name) && (name.isEmpty() || m_groupNameCache.contains(name))) {
savedOrder.append(name);
orderArr.append(name);
}
}
toplevel.insert("groupOrder", orderArr);
QJsonDocument doc(toplevel); QJsonDocument doc(toplevel);
try { try {
FS::write(groupFileName, doc.toJson()); FS::write(groupFileName, doc.toJson());
@ -772,9 +817,13 @@ void InstanceList::loadGroupList()
QString groupFileName = m_instDir + "/instgroups.json"; QString groupFileName = m_instDir + "/instgroups.json";
// if there's no group file, fail // if there's no group file, set up default state
if (!QFileInfo(groupFileName).exists()) if (!QFileInfo(groupFileName).exists()) {
m_groupOrder.clear();
m_groupOrder.append("");
m_groupsLoaded = true;
return; return;
}
QByteArray jsonData; QByteArray jsonData;
try { try {
@ -815,6 +864,16 @@ void InstanceList::loadGroupList()
m_instanceGroupIndex.clear(); m_instanceGroupIndex.clear();
m_groupNameCache.clear(); m_groupNameCache.clear();
m_groupOrder.clear();
// Load saved group order FIRST, so increaseGroupCount won't re-add groups out of order
if (rootObj.value("groupOrder").isArray()) {
QJsonArray orderArr = rootObj.value("groupOrder").toArray();
for (const auto& val : orderArr) {
QString name = val.toString();
m_groupOrder.append(name);
}
}
// Iterate through all the groups. // Iterate through all the groups.
QJsonObject groupMapping = rootObj.value("groups").toObject(); QJsonObject groupMapping = rootObj.value("groups").toObject();
@ -862,6 +921,23 @@ void InstanceList::loadGroupList()
// empty string represents ungrouped "group" // empty string represents ungrouped "group"
m_collapsedGroups.insert(""); m_collapsedGroups.insert("");
} }
// Deduplicate group order (defense against previously-corrupted data)
QStringList uniqueOrder;
for (const auto& name : m_groupOrder) {
if (!uniqueOrder.contains(name))
uniqueOrder.append(name);
}
m_groupOrder = uniqueOrder;
// Treat ungrouped group (empty string) as a regular group for ordering
if (m_groupOrder.isEmpty()) {
m_groupOrder = m_groupNameCache.keys();
m_groupOrder.sort();
}
if (!m_groupOrder.contains(""))
m_groupOrder.append("");
m_groupsLoaded = true; m_groupsLoaded = true;
qDebug() << "Group list loaded."; qDebug() << "Group list loaded.";
} }

View file

@ -158,8 +158,11 @@ class InstanceList : public QAbstractListModel {
QMimeData* mimeData(const QModelIndexList& indexes) const override; QMimeData* mimeData(const QModelIndexList& indexes) const override;
QStringList getLinkedInstancesById(const QString& id) const; QStringList getLinkedInstancesById(const QString& id) const;
QStringList groupOrder() const;
void moveGroup(const QString& group, int toIndex);
signals: signals:
void groupOrderChanged();
void dataIsInvalid(); void dataIsInvalid();
void instancesChanged(); void instancesChanged();
void instanceSelectRequest(QString instanceId); void instanceSelectRequest(QString instanceId);
@ -195,6 +198,7 @@ class InstanceList : public QAbstractListModel {
std::vector<std::unique_ptr<BaseInstance>> m_instances; std::vector<std::unique_ptr<BaseInstance>> m_instances;
// id -> refs // id -> refs
QMap<QString, int> m_groupNameCache; QMap<QString, int> m_groupNameCache;
QStringList m_groupOrder;
SettingsObject* m_globalSettings; SettingsObject* m_globalSettings;
QString m_instDir; QString m_instDir;

View file

@ -38,6 +38,7 @@
#include <QAccessible> #include <QAccessible>
#include <QApplication> #include <QApplication>
#include <QCache> #include <QCache>
#include <QDrag> #include <QDrag>
#include <QFont> #include <QFont>
#include <QListView> #include <QListView>
@ -46,6 +47,7 @@
#include <QPainter> #include <QPainter>
#include <QPersistentModelIndex> #include <QPersistentModelIndex>
#include <QScrollBar> #include <QScrollBar>
#include <QSet>
#include <QtMath> #include <QtMath>
#include "VisualGroup.h" #include "VisualGroup.h"
@ -202,7 +204,23 @@ void InstanceView::updateGeometries()
} }
qDeleteAll(m_groups); qDeleteAll(m_groups);
m_groups = cats.values(); m_groups.clear();
// Build ordered group list from custom group order
auto instanceList = APPLICATION->instances();
auto groupOrder = instanceList->groupOrder();
QSet<QString> placed;
for (const auto& groupName : groupOrder) {
if (cats.contains(groupName)) {
m_groups.append(cats.take(groupName));
placed.insert(groupName);
}
}
// Append remaining groups (not in order) alphabetically
auto remaining = cats.values();
std::sort(remaining.begin(), remaining.end(), [](VisualGroup* a, VisualGroup* b) { return a->text.localeAwareCompare(b->text) < 0; });
m_groups.append(remaining);
updateScrollbar(); updateScrollbar();
viewport()->update(); viewport()->update();
} }
@ -283,12 +301,20 @@ void InstanceView::mousePressEvent(QMouseEvent* event)
m_pressedIndex = index; m_pressedIndex = index;
m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex); m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex);
m_pressedPosition = geometryPos; m_pressedPosition = geometryPos;
m_draggingGroup = false;
m_draggedGroupName.clear();
m_groupDragTargetIndex = -1;
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {
VisualGroup::HitResults hitResult; VisualGroup::HitResults hitResult;
m_pressedCategory = categoryAt(geometryPos, hitResult); m_pressedCategory = categoryAt(geometryPos, hitResult);
if (m_pressedCategory && hitResult & VisualGroup::CheckboxHit) { if (m_pressedCategory && hitResult & VisualGroup::HeaderHit) {
setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState); // Always allow drag from header, collapse also possible on click
m_draggingGroup = true;
m_draggedGroupName = m_pressedCategory->text;
if (hitResult & VisualGroup::CheckboxHit) {
setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState);
}
event->accept(); event->accept();
return; return;
} }
@ -325,6 +351,39 @@ void InstanceView::mouseMoveEvent(QMouseEvent* event)
QPoint visualPos = event->pos(); QPoint visualPos = event->pos();
QPoint geometryPos = event->pos() + offset(); QPoint geometryPos = event->pos() + offset();
if (m_draggingGroup) {
topLeft = m_pressedPosition - offset();
qreal dist = (topLeft - event->pos()).manhattanLength();
qreal threshold = QApplication::startDragDistance();
if (dist > threshold) {
// Drag preview: compute visual target index and show indicator
setState(NoState);
QPoint cursorPos = geometryPos;
m_groupDragTargetIndex = -1;
for (int i = 0; i < m_groups.size(); i++) {
if (m_groups[i]->text == m_draggedGroupName)
continue;
int gTop = m_groups[i]->verticalPosition();
int gBot = gTop + m_groups[i]->totalHeight();
int hMid = gTop + VisualGroup::headerHeight() / 2;
if (cursorPos.y() < hMid) {
m_groupDragTargetIndex = i;
break;
} else if (cursorPos.y() < gBot) {
m_groupDragTargetIndex = i + 1;
break;
}
}
if (m_groupDragTargetIndex < 0)
m_groupDragTargetIndex = m_groups.size();
if (m_groupDragTargetIndex > m_groups.size())
m_groupDragTargetIndex = m_groups.size();
viewport()->update();
return;
}
return;
}
if (state() == ExpandingState || state() == CollapsingState) { if (state() == ExpandingState || state() == CollapsingState) {
return; return;
} }
@ -368,6 +427,35 @@ void InstanceView::mouseReleaseEvent(QMouseEvent* event)
{ {
executeDelayedItemsLayout(); executeDelayedItemsLayout();
// Commit group drag reorder on release
if (m_draggingGroup && m_groupDragTargetIndex >= 0) {
auto groupOrder = APPLICATION->instances()->groupOrder();
int currentOrderIdx = groupOrder.indexOf(m_draggedGroupName);
if (currentOrderIdx >= 0) {
// Convert visual target index to groupOrder index
int orderTarget = 0;
for (int i = 0; i < m_groupDragTargetIndex && i < m_groups.size(); i++) {
if (m_groups[i]->text == m_draggedGroupName)
continue;
int orderPos = groupOrder.indexOf(m_groups[i]->text);
if (orderPos >= 0 && orderPos >= orderTarget)
orderTarget = orderPos + 1;
}
// Clamp to valid range
if (orderTarget > groupOrder.size())
orderTarget = groupOrder.size();
if (orderTarget != currentOrderIdx) {
APPLICATION->instances()->moveGroup(m_draggedGroupName, orderTarget);
updateGeometries();
}
}
viewport()->update();
}
m_draggingGroup = false;
m_draggedGroupName.clear();
m_groupDragTargetIndex = -1;
QPoint visualPos = event->pos(); QPoint visualPos = event->pos();
QPoint geometryPos = event->pos() + offset(); QPoint geometryPos = event->pos() + offset();
QPersistentModelIndex index = indexAt(visualPos); QPersistentModelIndex index = indexAt(visualPos);
@ -524,6 +612,21 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event)
option.rect = backup; option.rect = backup;
} }
// Draw group drag drop indicator
if (m_groupDragTargetIndex >= 0) {
int indicatorY = 0;
if (m_groupDragTargetIndex < m_groups.size()) {
indicatorY = m_groups[m_groupDragTargetIndex]->verticalPosition() - verticalOffset();
} else if (!m_groups.isEmpty()) {
auto* last = m_groups.last();
indicatorY = last->verticalPosition() + last->totalHeight() - verticalOffset();
}
painter.save();
painter.setPen(QPen(Qt::white, 1));
painter.drawLine(m_leftMargin, indicatorY, wpWidth - m_rightMargin, indicatorY);
painter.restore();
}
for (int i = 0; i < model()->rowCount(); ++i) { for (int i = 0; i < model()->rowCount(); ++i) {
const QModelIndex index = model()->index(i, 0); const QModelIndex index = model()->index(i, 0);
if (isIndexHidden(index)) { if (isIndexHidden(index)) {

View file

@ -139,6 +139,11 @@ class InstanceView : public QAbstractItemView {
QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag; QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag;
QPoint m_lastDragPosition; QPoint m_lastDragPosition;
// group drag-reorder state
bool m_draggingGroup = false;
QString m_draggedGroupName;
int m_groupDragTargetIndex = -1;
VisualGroup* category(const QModelIndex& index) const; VisualGroup* category(const QModelIndex& index) const;
VisualGroup* category(const QString& cat) const; VisualGroup* category(const QString& cat) const;
VisualGroup* categoryAt(const QPoint& pos, VisualGroup::HitResults& result) const; VisualGroup* categoryAt(const QPoint& pos, VisualGroup::HitResults& result) const;