From 66ad129c20d87b9216cdd13d11c102d9261d9ddf Mon Sep 17 00:00:00 2001 From: ispyisail Date: Thu, 1 Oct 2026 18:33:50 +1300 Subject: [PATCH] Add drag and drop of free terminals onto a terminal strip (#409) In the terminal strip manager, free terminals could only be moved into a strip with the "move to" list and its button. Dragging one onto a strip, which is what users try first (#409), did nothing. Free terminals can now be dragged onto a strip in the tree on the left, from either place they are listed: - the free terminal table: every selected row is moved; - the tree itself: the dragged free terminal is moved. Dropping on a strip, or on a terminal already in that strip, moves them with the same undo command as the button, so one undo puts them back. The strip is then selected, showing the terminals just added. A drop anywhere else is refused. A drop is also refused while the free terminal table has edits that are not applied yet: moving reloads that table and would lose them. The button is disabled in the same case. The drag carries the uuids of the terminal elements under its own mime type, and the tree only moves terminals that are still free in the project when the drop happens. Co-Authored-By: Claude Opus 5.5 --- .../TerminalStrip/ui/freeterminaleditor.cpp | 11 ++ sources/TerminalStrip/ui/freeterminaleditor.h | 1 + .../TerminalStrip/ui/freeterminalmodel.cpp | 70 +++++++- sources/TerminalStrip/ui/freeterminalmodel.h | 7 + .../ui/terminalstripeditorwindow.cpp | 4 + .../ui/terminalstriptreedockwidget.cpp | 161 ++++++++++++++++++ .../ui/terminalstriptreedockwidget.h | 14 ++ 7 files changed, 267 insertions(+), 1 deletion(-) diff --git a/sources/TerminalStrip/ui/freeterminaleditor.cpp b/sources/TerminalStrip/ui/freeterminaleditor.cpp index e9863f448..b0beda5d3 100644 --- a/sources/TerminalStrip/ui/freeterminaleditor.cpp +++ b/sources/TerminalStrip/ui/freeterminaleditor.cpp @@ -41,6 +41,9 @@ FreeTerminalEditor::FreeTerminalEditor(QETProject *project, QWidget *parent) : m_model = new FreeTerminalModel(m_project, this); ui->m_table_view->setModel(m_model); ui->m_table_view->setCurrentIndex(m_model->index(0,0)); + //Selected rows can be dragged onto a strip of the terminal strip tree + ui->m_table_view->setDragEnabled(true); + ui->m_table_view->setDragDropMode(QAbstractItemView::DragOnly); if (m_project) { connect(m_project, &QObject::destroyed, this, &FreeTerminalEditor::reload); @@ -274,6 +277,14 @@ void FreeTerminalEditor::on_m_move_pb_clicked() reload(); } +/** + * @brief FreeTerminalEditor::hasPendingEdits + * @return true if cells were edited and not yet applied + */ +bool FreeTerminalEditor::hasPendingEdits() const { + return !m_model->modifiedModelRealTerminalData().isEmpty(); +} + void FreeTerminalEditor::selectionChanged() { const bool has_selection = !ui->m_table_view->selectionModel()->selectedIndexes().isEmpty(); diff --git a/sources/TerminalStrip/ui/freeterminaleditor.h b/sources/TerminalStrip/ui/freeterminaleditor.h index dc75f06c3..2320a60ba 100644 --- a/sources/TerminalStrip/ui/freeterminaleditor.h +++ b/sources/TerminalStrip/ui/freeterminaleditor.h @@ -39,6 +39,7 @@ class FreeTerminalEditor : public QWidget void reload(); void apply(); + bool hasPendingEdits() const; void setProject(QETProject *project); diff --git a/sources/TerminalStrip/ui/freeterminalmodel.cpp b/sources/TerminalStrip/ui/freeterminalmodel.cpp index e11fb6ac7..7bfe62674 100644 --- a/sources/TerminalStrip/ui/freeterminalmodel.cpp +++ b/sources/TerminalStrip/ui/freeterminalmodel.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include +#include #include "freeterminalmodel.h" #include "../../elementprovider.h" @@ -239,7 +240,7 @@ QVariant FreeTerminalModel::headerData(int section, Qt::Orientation orientation, Qt::ItemFlags FreeTerminalModel::flags(const QModelIndex &index) const { - Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled; auto c = index.column(); if (c == LABEL_CELL || c == TYPE_CELL || c == FUNCTION_CELL) @@ -320,6 +321,73 @@ QVector > FreeTerminalModel::realTerminalForIndex(c return vector_; } +QStringList FreeTerminalModel::mimeTypes() const { + return QStringList{freeTerminalMimeType()}; +} + +/** + * @brief FreeTerminalModel::mimeData + * @param indexes + * @return the uuids of the terminal elements of the rows in @a indexes, + * to be dropped on a terminal strip of the terminal strip tree. + */ +QMimeData *FreeTerminalModel::mimeData(const QModelIndexList &indexes) const +{ + QVector uuids; + for (const auto &real_t : realTerminalForIndex(indexes)) { + uuids.append(real_t->elementUuid()); + } + return mimeDataForUuids(uuids); +} + +/** + * @brief FreeTerminalModel::freeTerminalMimeType + * @return the mime type of a drag of free terminals + */ +QString FreeTerminalModel::freeTerminalMimeType() { + return QStringLiteral("application/x-qet-free-terminal-uuids"); +} + +/** + * @brief FreeTerminalModel::uuidsFromMimeData + * @param mime_data + * @return the uuids of the terminal elements carried by @a mime_data, + * empty if it is not a drag of free terminals. + */ +QVector FreeTerminalModel::uuidsFromMimeData(const QMimeData *mime_data) +{ + QVector uuids; + if (!mime_data || !mime_data->hasFormat(freeTerminalMimeType())) { + return uuids; + } + + const auto lines = QString::fromLatin1(mime_data->data(freeTerminalMimeType())).split(QLatin1Char('\n'), Qt::SkipEmptyParts); + for (const auto &line : lines) { + const QUuid uuid(line); + if (!uuid.isNull()) { + uuids.append(uuid); + } + } + return uuids; +} + +/** + * @brief FreeTerminalModel::mimeDataForUuids + * @param uuids + * @return a new QMimeData carrying the uuids of terminal elements in @a uuids + */ +QMimeData *FreeTerminalModel::mimeDataForUuids(const QVector &uuids) +{ + QStringList lines; + for (const auto &uuid : uuids) { + lines.append(uuid.toString()); + } + + auto mime_data = new QMimeData(); + mime_data->setData(freeTerminalMimeType(), lines.join(QLatin1Char('\n')).toLatin1()); + return mime_data; +} + /** * @brief FreeTerminalModel::fillTerminalVector */ diff --git a/sources/TerminalStrip/ui/freeterminalmodel.h b/sources/TerminalStrip/ui/freeterminalmodel.h index d455832fa..7fb12568a 100644 --- a/sources/TerminalStrip/ui/freeterminalmodel.h +++ b/sources/TerminalStrip/ui/freeterminalmodel.h @@ -26,6 +26,7 @@ #include "../../qetproject.h" class RealTerminal; +class QMimeData; /** * @brief The FreeTerminalModel class @@ -61,6 +62,12 @@ class FreeTerminalModel : public QAbstractTableModel modelRealTerminalData dataAtRow(int row) const; QVector> realTerminalForIndex(const QModelIndexList &index_list) const; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + static QString freeTerminalMimeType(); + static QVector uuidsFromMimeData(const QMimeData *mime_data); + static QMimeData *mimeDataForUuids(const QVector &uuids); + private: void fillTerminalVector(); diff --git a/sources/TerminalStrip/ui/terminalstripeditorwindow.cpp b/sources/TerminalStrip/ui/terminalstripeditorwindow.cpp index 32beac65a..c47660bd9 100644 --- a/sources/TerminalStrip/ui/terminalstripeditorwindow.cpp +++ b/sources/TerminalStrip/ui/terminalstripeditorwindow.cpp @@ -67,6 +67,10 @@ TerminalStripEditorWindow::TerminalStripEditorWindow(QETProject *project, QWidge m_terminal_strip_editor = new TerminalStripEditor{m_project, this}; connect(m_tree_dock, &TerminalStripTreeDockWidget::currentStripChanged, this, &TerminalStripEditorWindow::currentStripChanged); + //A move reloads the free terminal table, which would lose its unapplied edits + m_tree_dock->setDropCheck([this]() { + return !m_free_terminal_editor->hasPendingEdits(); + }); ui->m_stacked_widget->insertWidget(EMPTY_PAGE, new QWidget(ui->m_stacked_widget)); ui->m_stacked_widget->insertWidget(FREE_TERMINAL_PAGE, m_free_terminal_editor); diff --git a/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp b/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp index fd81bd61e..2611ee5b8 100644 --- a/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp +++ b/sources/TerminalStrip/ui/terminalstriptreedockwidget.cpp @@ -27,7 +27,13 @@ #include "../../qetgraphicsitem/terminalelement.h" #include "../terminalstrip.h" #include "../../qetinformation.h" +#include "freeterminalmodel.h" +#include +#include +#include +#include +#include #include TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QWidget *parent) : @@ -38,6 +44,12 @@ TerminalStripTreeDockWidget::TerminalStripTreeDockWidget(QETProject *project, QW setProject(project); ui->m_tree_view->expandRecursively(ui->m_tree_view->rootIndex()); + + //Free terminals are dragged from this tree or from the free terminal + //table and dropped on a strip. The tree items themselves never move, + //the tree is rebuilt from the project after the drop. + ui->m_tree_view->viewport()->setAcceptDrops(true); + ui->m_tree_view->viewport()->installEventFilter(this); } TerminalStripTreeDockWidget::~TerminalStripTreeDockWidget() @@ -365,3 +377,152 @@ void TerminalStripTreeDockWidget::setCurrentStrip(TerminalStrip *strip) m_current_strip = strip; emit currentStripChanged(strip); } + +/** + * @brief TerminalStripTreeDockWidget::setDropCheck + * @param check : called before accepting a drop of free terminals, + * a drop is refused when it returns false. + */ +void TerminalStripTreeDockWidget::setDropCheck(std::function check) { + m_drop_check = check; +} + +/** + * @brief TerminalStripTreeDockWidget::eventFilter + * Drag a free terminal of the tree, and drop free terminals + * (from the tree or from the free terminal table) on a strip. + */ +bool TerminalStripTreeDockWidget::eventFilter(QObject *watched, QEvent *event) +{ + if (watched != ui->m_tree_view->viewport()) { + return QDockWidget::eventFilter(watched, event); + } + + switch (event->type()) + { + case QEvent::MouseButtonPress: + { + auto me = static_cast(event); + m_drag_uuid = QUuid(); + auto item = ui->m_tree_view->itemAt(me->pos()); + if (me->button() == Qt::LeftButton + && item + && item->type() == Terminal + && item->parent() + && item->parent()->type() == FreeTerminal) + { + m_drag_start_pos = me->pos(); + m_drag_uuid = item->data(0, UUID_USER_ROLE).toUuid(); + } + break; + } + case QEvent::MouseMove: + { + auto me = static_cast(event); + if (!m_drag_uuid.isNull() + && (me->buttons() & Qt::LeftButton) + && (me->pos() - m_drag_start_pos).manhattanLength() >= QApplication::startDragDistance()) + { + auto drag = new QDrag(ui->m_tree_view); + drag->setMimeData(FreeTerminalModel::mimeDataForUuids({m_drag_uuid})); + drag->setPixmap(QET::Icons::ElementTerminal.pixmap(16, 16)); + m_drag_uuid = QUuid(); + drag->exec(Qt::CopyAction); + return true; + } + break; + } + case QEvent::DragEnter: + { + //Accepted wherever it enters, or no DragMove follows to reach a strip + auto de = static_cast(event); + if (!freeTerminals(de->mimeData()).isEmpty()) { + de->acceptProposedAction(); + } else { + de->ignore(); + } + return true; + } + case QEvent::DragMove: + { + auto de = static_cast(event); + if (dropAllowed(de->mimeData(), de->position().toPoint())) { + de->acceptProposedAction(); + } else { + de->ignore(); + } + return true; + } + case QEvent::Drop: + { + auto de = static_cast(event); + const auto pos = de->position().toPoint(); + if (!dropAllowed(de->mimeData(), pos)) { + de->ignore(); + return true; + } + auto strip = stripAt(pos); + const auto terminals = freeTerminals(de->mimeData()); + de->acceptProposedAction(); + + //The command rebuilds the tree (TerminalStrip::orderChanged) + m_project->undoStack()->push(new AddTerminalToStripCommand(terminals, strip)); + setSelectedStrip(strip); + return true; + } + default: + break; + } + + return QDockWidget::eventFilter(watched, event); +} + +/** + * @brief TerminalStripTreeDockWidget::stripAt + * @param pos : position in the viewport of the tree + * @return the strip of the item at @a pos, the item being the strip + * or one of its terminals, or nullptr. + */ +TerminalStrip *TerminalStripTreeDockWidget::stripAt(const QPoint &pos) const +{ + auto item = ui->m_tree_view->itemAt(pos); + if (item && item->type() == Terminal) { + item = item->parent(); + } + if (item && item->type() == Strip) { + return m_item_strip_H.value(item); + } + return nullptr; +} + +/** + * @brief TerminalStripTreeDockWidget::freeTerminals + * @param mime_data + * @return the free terminals carried by @a mime_data + * which are still free in the project. + */ +QVector> TerminalStripTreeDockWidget::freeTerminals(const QMimeData *mime_data) const +{ + QVector> terminals; + for (const auto &uuid : FreeTerminalModel::uuidsFromMimeData(mime_data)) + { + const auto real_t = m_uuid_terminal_H.value(uuid); + if (real_t && !real_t->parentStrip() && !terminals.contains(real_t)) { + terminals.append(real_t); + } + } + return terminals; +} + +/** + * @brief TerminalStripTreeDockWidget::dropAllowed + * @return true if @a mime_data carries free terminals + * and there is a strip at @a pos to drop them in. + */ +bool TerminalStripTreeDockWidget::dropAllowed(const QMimeData *mime_data, const QPoint &pos) const +{ + return m_project + && stripAt(pos) + && !freeTerminals(mime_data).isEmpty() + && (!m_drop_check || m_drop_check()); +} diff --git a/sources/TerminalStrip/ui/terminalstriptreedockwidget.h b/sources/TerminalStrip/ui/terminalstriptreedockwidget.h index 142d72f97..0600218e4 100644 --- a/sources/TerminalStrip/ui/terminalstriptreedockwidget.h +++ b/sources/TerminalStrip/ui/terminalstriptreedockwidget.h @@ -21,11 +21,15 @@ #include #include #include +#include + +#include class QETProject; class QTreeWidgetItem; class TerminalStrip; class RealTerminal; +class QMimeData; namespace Ui { class TerminalStripTreeDockWidget; @@ -58,6 +62,10 @@ class TerminalStripTreeDockWidget : public QDockWidget QString currentLocation() const; void setSelectedStrip(TerminalStrip *strip); QSharedPointer currentRealTerminal() const; + void setDropCheck(std::function check); + + protected: + bool eventFilter(QObject *watched, QEvent *event) override; signals: void currentStripChanged(TerminalStrip *strip); @@ -70,6 +78,9 @@ class TerminalStripTreeDockWidget : public QDockWidget QTreeWidgetItem* addTerminalStrip(TerminalStrip *terminal_strip); void addFreeTerminal(); void setCurrentStrip(TerminalStrip *strip); + TerminalStrip *stripAt(const QPoint &pos) const; + QVector> freeTerminals(const QMimeData *mime_data) const; + bool dropAllowed(const QMimeData *mime_data, const QPoint &pos) const; private: Ui::TerminalStripTreeDockWidget *ui; @@ -82,6 +93,9 @@ class TerminalStripTreeDockWidget : public QDockWidget QVector m_strip_changed_connection; bool m_current_is_free_terminal{false}; QMetaObject::Connection m_project_destroy_connection; + std::function m_drop_check; + QPoint m_drag_start_pos; + QUuid m_drag_uuid; }; #endif // TERMINALSTRIPTREEDOCKWIDGET_H