diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 1339499c7..e366b1cf4 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -145,6 +145,30 @@ QStringList InstanceList::getLinkedInstancesById(const QString& id) const 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 { Q_UNUSED(parent); @@ -293,6 +317,8 @@ void InstanceList::deleteGroup(const GroupId& name) } if (removed) saveGroupList(); + m_groupOrder.removeAll(name); + emit groupOrderChanged(); } void InstanceList::renameGroup(const QString& src, const QString& dst) @@ -318,6 +344,10 @@ void InstanceList::renameGroup(const QString& src, const QString& dst) } if (modified) saveGroupList(); + int idx = m_groupOrder.indexOf(src); + if (idx >= 0) + m_groupOrder[idx] = dst; + emit groupOrderChanged(); } bool InstanceList::isGroupCollapsed(const QString& group) @@ -692,7 +722,11 @@ void InstanceList::increaseGroupCount(const QString& group) if (group.isEmpty()) return; + bool wasNew = !m_groupNameCache.contains(group); ++m_groupNameCache[group]; + if (wasNew && !m_groupOrder.contains(group)) { + m_groupOrder.append(group); + } } void InstanceList::decreaseGroupCount(const QString& group) @@ -703,6 +737,7 @@ void InstanceList::decreaseGroupCount(const QString& group) if (--m_groupNameCache[group] < 1) { m_groupNameCache.remove(group); m_collapsedGroups.remove(group); + m_groupOrder.removeAll(group); } } @@ -757,6 +792,16 @@ void InstanceList::saveGroupList() ungrouped.insert("hidden", QJsonValue(true)); 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); try { FS::write(groupFileName, doc.toJson()); @@ -772,9 +817,13 @@ void InstanceList::loadGroupList() QString groupFileName = m_instDir + "/instgroups.json"; - // if there's no group file, fail - if (!QFileInfo(groupFileName).exists()) + // if there's no group file, set up default state + if (!QFileInfo(groupFileName).exists()) { + m_groupOrder.clear(); + m_groupOrder.append(""); + m_groupsLoaded = true; return; + } QByteArray jsonData; try { @@ -815,6 +864,16 @@ void InstanceList::loadGroupList() m_instanceGroupIndex.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. QJsonObject groupMapping = rootObj.value("groups").toObject(); @@ -862,6 +921,23 @@ void InstanceList::loadGroupList() // empty string represents ungrouped "group" 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; qDebug() << "Group list loaded."; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index f0a92d273..8f3bdc06b 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -158,8 +158,11 @@ class InstanceList : public QAbstractListModel { QMimeData* mimeData(const QModelIndexList& indexes) const override; QStringList getLinkedInstancesById(const QString& id) const; + QStringList groupOrder() const; + void moveGroup(const QString& group, int toIndex); signals: + void groupOrderChanged(); void dataIsInvalid(); void instancesChanged(); void instanceSelectRequest(QString instanceId); @@ -195,6 +198,7 @@ class InstanceList : public QAbstractListModel { std::vector> m_instances; // id -> refs QMap m_groupNameCache; + QStringList m_groupOrder; SettingsObject* m_globalSettings; QString m_instDir; diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index 9a24b7990..d1bc15e7f 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -38,6 +38,7 @@ #include #include #include + #include #include #include @@ -46,6 +47,7 @@ #include #include #include +#include #include #include "VisualGroup.h" @@ -202,7 +204,23 @@ void InstanceView::updateGeometries() } 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 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(); viewport()->update(); } @@ -283,12 +301,20 @@ void InstanceView::mousePressEvent(QMouseEvent* event) m_pressedIndex = index; m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex); m_pressedPosition = geometryPos; + m_draggingGroup = false; + m_draggedGroupName.clear(); + m_groupDragTargetIndex = -1; if (event->button() == Qt::LeftButton) { VisualGroup::HitResults hitResult; m_pressedCategory = categoryAt(geometryPos, hitResult); - if (m_pressedCategory && hitResult & VisualGroup::CheckboxHit) { - setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState); + if (m_pressedCategory && hitResult & VisualGroup::HeaderHit) { + // 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(); return; } @@ -325,6 +351,39 @@ void InstanceView::mouseMoveEvent(QMouseEvent* event) QPoint visualPos = event->pos(); 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) { return; } @@ -368,6 +427,35 @@ void InstanceView::mouseReleaseEvent(QMouseEvent* event) { 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 geometryPos = event->pos() + offset(); QPersistentModelIndex index = indexAt(visualPos); @@ -524,6 +612,21 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) 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) { const QModelIndex index = model()->index(i, 0); if (isIndexHidden(index)) { diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index 5d9dbf729..b47b6c1fe 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -139,6 +139,11 @@ class InstanceView : public QAbstractItemView { QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag; 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 QString& cat) const; VisualGroup* categoryAt(const QPoint& pos, VisualGroup::HitResults& result) const;