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 <noreply@anthropic.com>
This commit is contained in:
ispyisail
2026-10-01 18:33:50 +13:00
parent 7f799bb425
commit 66ad129c20
7 changed files with 267 additions and 1 deletions
@@ -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();
@@ -39,6 +39,7 @@ class FreeTerminalEditor : public QWidget
void reload();
void apply();
bool hasPendingEdits() const;
void setProject(QETProject *project);
+69 -1
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include <QComboBox>
#include <QMimeData>
#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<QSharedPointer<RealTerminal> > 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<QUuid> 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<QUuid> FreeTerminalModel::uuidsFromMimeData(const QMimeData *mime_data)
{
QVector<QUuid> 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<QUuid> &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
*/
@@ -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<QSharedPointer<RealTerminal>> realTerminalForIndex(const QModelIndexList &index_list) const;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
static QString freeTerminalMimeType();
static QVector<QUuid> uuidsFromMimeData(const QMimeData *mime_data);
static QMimeData *mimeDataForUuids(const QVector<QUuid> &uuids);
private:
void fillTerminalVector();
@@ -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);
@@ -27,7 +27,13 @@
#include "../../qetgraphicsitem/terminalelement.h"
#include "../terminalstrip.h"
#include "../../qetinformation.h"
#include "freeterminalmodel.h"
#include <QApplication>
#include <QDrag>
#include <QDropEvent>
#include <QMimeData>
#include <QMouseEvent>
#include <QUuid>
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<bool ()> 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<QMouseEvent *>(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<QMouseEvent *>(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<QDragEnterEvent *>(event);
if (!freeTerminals(de->mimeData()).isEmpty()) {
de->acceptProposedAction();
} else {
de->ignore();
}
return true;
}
case QEvent::DragMove:
{
auto de = static_cast<QDragMoveEvent *>(event);
if (dropAllowed(de->mimeData(), de->position().toPoint())) {
de->acceptProposedAction();
} else {
de->ignore();
}
return true;
}
case QEvent::Drop:
{
auto de = static_cast<QDropEvent *>(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<QSharedPointer<RealTerminal>> TerminalStripTreeDockWidget::freeTerminals(const QMimeData *mime_data) const
{
QVector<QSharedPointer<RealTerminal>> 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());
}
@@ -21,11 +21,15 @@
#include <QDockWidget>
#include <QPointer>
#include <QHash>
#include <QUuid>
#include <functional>
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<RealTerminal> currentRealTerminal() const;
void setDropCheck(std::function<bool()> 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<QSharedPointer<RealTerminal>> 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<QMetaObject::Connection> m_strip_changed_connection;
bool m_current_is_free_terminal{false};
QMetaObject::Connection m_project_destroy_connection;
std::function<bool()> m_drop_check;
QPoint m_drag_start_pos;
QUuid m_drag_uuid;
};
#endif // TERMINALSTRIPTREEDOCKWIDGET_H