Merge pull request #1199 from ispyisail/feature/terminal-strip-drag-drop

Add drag and drop of free terminals onto a terminal strip (#409)
This commit is contained in:
Laurent Trinques
2026-10-01 08:14:25 +02:00
committed by GitHub
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