[education/cantor] src: Improve worksheet usability

Alexander Semke <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 4b0bfeb229303ae101bef5d28563dea707f6611b by Alexander Semke, on behalf of Nanhao Lv.
Committed on 26/07/2026 at 19:50.
Pushed by asemke into branch 'master'.

Improve worksheet usability

M  +20   -19   src/hierarchyentry.cpp
M  +302  -53   src/worksheet.cpp
M  +17   -3    src/worksheet.h
M  +5    -0    src/worksheetcontrolitem.cpp
M  +1    -0    src/worksheetcontrolitem.h
M  +110  -69   src/worksheetentry.cpp
M  +16   -0    src/worksheettexteditoritem.cpp
M  +17   -0    src/worksheettextitem.cpp
M  +2    -15   src/worksheetview.cpp

https://invent.kde.org/education/cantor/-/commit/4b0bfeb229303ae101bef5d28563dea707f6611b

diff --git a/src/hierarchyentry.cpp b/src/hierarchyentry.cpp
index 87c4d8a8..91982c3f 100644
--- a/src/hierarchyentry.cpp
+++ b/src/hierarchyentry.cpp
@@ -10,12 +10,10 @@
 
 #include <QJsonObject>
 #include <QRegularExpression>
-#include <QDrag>
-#include <QBitmap>
-#include <QMimeData>
 #include <QPainter>
 #include <QDebug>
 #include <QActionGroup>
+#include <QtMath>
 #include <QUuid>
 
 #include <KLocalizedString>
@@ -475,35 +473,35 @@ void HierarchyEntry::startDrag(QPointF grabPos)
 
     worksheet()->resetEntryCursor();
 
-    auto* drag = new QDrag(worksheetView());
     const qreal scale = worksheet()->renderer()->scale();
-    const QRectF hierarchyBound(boundingRect().x(), boundingRect().y(), boundingRect().width(), m_controlElement.boundingRect().height());
-    const QSizeF hierarchyZoneSize(size().width(), m_controlElement.boundingRect().height());
-
-    QPixmap pixmap((hierarchyZoneSize * scale).toSize());
+    const qreal hierarchyHeight = m_controlElement.rect().height();
+    const QRectF hierarchyContentBound(boundingRect().x(), boundingRect().y(), boundingRect().width(), hierarchyHeight);
+    const QRectF hierarchyBound = hierarchyContentBound.united(
+        m_controlElement.mapRectToParent(m_controlElement.boundingRect()));
+    const QSizeF hierarchyZoneSize(size().width(), hierarchyHeight);
+    constexpr qreal previewMargin = 2.0;
+    const QRectF previewRect = hierarchyBound.adjusted(-previewMargin, -previewMargin, previewMargin, previewMargin);
+    const QSizeF previewPixelSize = previewRect.size() * scale;
+
+    QPixmap pixmap(qCeil(previewPixelSize.width()), qCeil(previewPixelSize.height()));
     pixmap.fill(QColor(255, 255, 255, 0));
 
     QPainter painter(&pixmap);
 
-    const QRectF sceneRect = mapRectToScene(hierarchyBound);
-    worksheet()->render(&painter, pixmap.rect(), sceneRect);
+    const QRectF sceneRect = mapRectToScene(previewRect);
+    worksheet()->render(&painter, QRectF(QPointF(), previewPixelSize), sceneRect);
 
     painter.end();
-    const QBitmap mask = pixmap.createMaskFromColor(QColor(255, 255, 255), Qt::MaskInColor);
-
-    pixmap.setMask(mask);
-    drag->setPixmap(pixmap);
-
+    QPoint hotSpot;
     if (grabPos.isNull())
     {
         const QPointF scenePos = worksheetView()->sceneCursorPos();
-        drag->setHotSpot((mapFromScene(scenePos) * scale).toPoint());
+        hotSpot = ((mapFromScene(scenePos) - previewRect.topLeft()) * scale).toPoint();
     }
     else
-        drag->setHotSpot((grabPos * scale).toPoint());
-    drag->setMimeData(new QMimeData());
+        hotSpot = ((grabPos - previewRect.topLeft()) * scale).toPoint();
 
-    worksheet()->startDragWithHierarchy(this, drag, hierarchyZoneSize);
+    worksheet()->startDragWithHierarchy(this, pixmap, hotSpot, hierarchyZoneSize);
 }
 
 void HierarchyEntry::updateFonts(bool force)
@@ -613,5 +611,8 @@ void HierarchyEntry::handleControlElementDoubleClick()
 
 void HierarchyEntry::updateAfterSettingsChanges()
 {
+    WorksheetEntry::updateAfterSettingsChanges();
+    m_hierarchyLevelItem->updateThemeColors();
+    m_textItem->updateThemeColors();
     updateFonts();
 }
diff --git a/src/worksheet.cpp b/src/worksheet.cpp
index efbd9cf2..da3ec37b 100644
--- a/src/worksheet.cpp
+++ b/src/worksheet.cpp
@@ -30,17 +30,21 @@
 #include <QApplication>
 #include <QBuffer>
 #include <QByteArray>
-#include <QDrag>
+#include <QEventLoop>
+#include <QGraphicsPixmapItem>
 #include <QGraphicsSceneMouseEvent>
 #include <QJsonArray>
 #include <QJsonDocument>
 #include <QJsonObject>
+#include <QMouseEvent>
 #include <QPrinter>
 #include <QRegularExpression>
+#include <QSet>
 #include <QTimer>
 #include <QActionGroup>
 #include <QFile>
 #include <QScopedValueRollback>
+#include <QWheelEvent>
 
 #include <KMessageBox>
 #include <KActionCollection>
@@ -62,6 +66,13 @@ const double Worksheet::TopMargin = 12;
 const double Worksheet::EntryCursorLength = 30;
 const double Worksheet::EntryCursorWidth = 2;
 
+namespace
+{
+constexpr int DragScrollMargin = 48;
+constexpr int DragScrollInterval = 50;
+constexpr int DragScrollStep = 4;
+}
+
 Worksheet::Worksheet(Cantor::Backend* backend, QWidget* parent, bool useDefaultWorksheetParameters)
     : QGraphicsScene(parent),
     m_cursorItemTimer(new QTimer(this)),
@@ -611,12 +622,21 @@ void Worksheet::invalidateLastEntry()
 
 WorksheetEntry* Worksheet::entryAt(qreal x, qreal y)
 {
-    auto* item = itemAt(x, y, QTransform());
-    while (item && (item->type() <= QGraphicsItem::UserType ||
-                    item->type() >= QGraphicsItem::UserType + 100))
-        item = item->parentItem();
-    if (item)
-        return qobject_cast<WorksheetEntry*>(item->toGraphicsObject());
+    const auto sceneItems = items(QPointF(x, y));
+    for (auto* sceneItem : sceneItems)
+    {
+        auto* item = sceneItem;
+        while (item && (item->type() <= QGraphicsItem::UserType ||
+                        item->type() >= QGraphicsItem::UserType + 100))
+            item = item->parentItem();
+
+        if (item)
+        {
+            if (auto* entry = qobject_cast<WorksheetEntry*>(item->toGraphicsObject()))
+                return entry;
+        }
+    }
+
     return nullptr;
 }
 
@@ -626,10 +646,24 @@ WorksheetEntry* Worksheet::entryAt(QPointF p)
 }
 
 void Worksheet::focusEntry(WorksheetEntry* entry)
+{
+    focusEntry(entry, WorksheetTextEditorItem::TopLeft);
+}
+
+void Worksheet::focusEntry(WorksheetEntry* entry, int pos, qreal xCoord)
 {
     if (!entry)
         return;
-    entry->focusEntry();
+
+    if (entry->focusEntry(pos, xCoord))
+    {
+        const KWorksheetCursor cursor = worksheetCursor();
+        if (cursor.isValid())
+            makeVisible(cursor);
+        else
+            makeVisible(entry);
+    }
+
     resetEntryCursor();
     //bool rt = entry->acceptRichText();
     //setActionsEnabled(rt);
@@ -637,14 +671,49 @@ void Worksheet::focusEntry(WorksheetEntry* entry)
     //ensureCursorVisible();
 }
 
-void Worksheet::startDrag(WorksheetEntry* entry, QDrag* drag)
+bool Worksheet::execEntryDrag(const QPixmap& pixmap, const QPoint& hotSpot)
+{
+    const qreal scale = renderer()->scale();
+    m_dragPixmapItem = addPixmap(pixmap);
+    m_dragPixmapItem->setOpacity(0.75);
+    m_dragPixmapItem->setScale(scale > 0 ? 1.0 / scale : 1.0);
+    m_dragPixmapItem->setZValue(1000);
+    m_dragHotSpot = scale > 0 ? QPointF(hotSpot) / scale : QPointF(hotSpot);
+
+    QEventLoop eventLoop;
+    m_entryDragEventLoop = &eventLoop;
+    m_dragAccepted = false;
+
+    qApp->installEventFilter(this);
+    QApplication::setOverrideCursor(Qt::ClosedHandCursor);
+    updateDragPosition(worksheetView()->sceneCursorPos());
+    eventLoop.exec();
+    QApplication::restoreOverrideCursor();
+    qApp->removeEventFilter(this);
+
+    m_entryDragEventLoop = nullptr;
+
+    if (m_dragScrollTimer)
+    {
+        delete m_dragScrollTimer;
+        m_dragScrollTimer = nullptr;
+    }
+
+    delete m_dragPixmapItem;
+    m_dragPixmapItem = nullptr;
+
+    return m_dragAccepted;
+}
+
+void Worksheet::startDrag(WorksheetEntry* entry, const QPixmap& pixmap, const QPoint& hotSpot)
 {
-    if (m_readOnly || !entry || !drag)
+    if (m_readOnly || !entry || pixmap.isNull())
         return;
 
     resetEntryCursor();
 
     m_dragEntry = entry;
+    m_dragWithHierarchy = false;
 
     WorksheetEntry* originalPrevious = entry->previous();
     WorksheetEntry* originalNext = entry->next();
@@ -667,11 +736,11 @@ void Worksheet::startDrag(WorksheetEntry* entry, QDrag* drag)
 
     m_dragEntry->hide();
 
-    const Qt::DropAction action = drag->exec();
+    const bool accepted = execEntryDrag(pixmap, hotSpot);
 
     bool positionChanged = false;
 
-    if (action == Qt::MoveAction && m_placeholderEntry)
+    if (accepted && m_placeholderEntry)
     {
         previous = m_placeholderEntry->previous();
         next = m_placeholderEntry->next();
@@ -694,33 +763,33 @@ void Worksheet::startDrag(WorksheetEntry* entry, QDrag* drag)
         setLastEntry(m_dragEntry);
 
     m_dragEntry->show();
-    const bool hierarchyMoved = m_dragEntry->type() == HierarchyEntry::Type;
-
-    m_dragEntry->focusEntry();
+    WorksheetEntry* draggedEntry = m_dragEntry;
     const QPointF scenePosition = worksheetView()->sceneCursorPos();
 
-    if (entryAt(scenePosition) != m_dragEntry)
-        m_dragEntry->hideActionBar();
+    if (entryAt(scenePosition) != draggedEntry)
+        draggedEntry->hideActionBar();
 
     m_dragEntry = nullptr;
 
-    if (hierarchyMoved && positionChanged)
+    if (positionChanged)
         updateHierarchyLayout();
 
     updateLayout();
+    focusEntry(draggedEntry);
 
     if (positionChanged)
         setModified();
 }
 
-void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, QDrag* drag, QSizeF responsibleZoneSize)
+void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, const QPixmap& pixmap, const QPoint& hotSpot, QSizeF responsibleZoneSize)
 {
-    if (m_readOnly || !entry || !drag)
+    if (m_readOnly || !entry || pixmap.isNull())
         return;
 
     resetEntryCursor();
 
     m_dragEntry = entry;
+    m_dragWithHierarchy = true;
     m_hierarchySubentriesDrag = hierarchySubelements(entry);
     m_hierarchyDragSize = responsibleZoneSize;
 
@@ -754,11 +823,11 @@ void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, QDrag* drag, QSize
     for (auto* subentry : m_hierarchySubentriesDrag)
         subentry->hide();
 
-    const Qt::DropAction action = drag->exec();
+    const bool accepted = execEntryDrag(pixmap, hotSpot);
 
     bool positionChanged = false;
 
-    if (action == Qt::MoveAction && m_placeholderEntry)
+    if (accepted && m_placeholderEntry)
     {
         previous = m_placeholderEntry->previous();
         next = m_placeholderEntry->next();
@@ -791,11 +860,11 @@ void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, QDrag* drag, QSize
     for (auto* subentry : m_hierarchySubentriesDrag)
         subentry->show();
 
-    m_dragEntry->focusEntry();
+    WorksheetEntry* draggedEntry = m_dragEntry;
 
     const QPointF scenePosition = worksheetView()->sceneCursorPos();
-    if (entryAt(scenePosition) != m_dragEntry)
-        m_dragEntry->hideActionBar();
+    if (entryAt(scenePosition) != draggedEntry)
+        draggedEntry->hideActionBar();
 
 #ifndef NDEBUG
     for (auto* current = firstEntry(); current; current = current->next())
@@ -804,9 +873,11 @@ void Worksheet::startDragWithHierarchy(HierarchyEntry* entry, QDrag* drag, QSize
 
     m_hierarchySubentriesDrag.clear();
     m_dragEntry = nullptr;
+    m_dragWithHierarchy = false;
 
     updateHierarchyLayout();
     updateLayout();
+    focusEntry(draggedEntry);
 
     if (positionChanged)
         setModified();
@@ -820,7 +891,7 @@ void Worksheet::evaluate()
         loginToSession();
 
     // evaluate the worksheet if the login was successful
-    if (m_session && m_session->status() == Cantor::Session::Done) {
+    if (m_session && m_session->status() == Cantor::Session::Done && firstEntry()) {
         firstEntry()->evaluate(WorksheetEntry::EvaluateNext);
         setModified();
     }
@@ -2094,7 +2165,27 @@ void Worksheet::mousePressEvent(QGraphicsSceneMouseEvent* event)
 void Worksheet::keyPressEvent(QKeyEvent* event)
 {
     if (m_readOnly)
+    {
+        const bool navigationKey = event->key() == Qt::Key_Left
+            || event->key() == Qt::Key_Right
+            || event->key() == Qt::Key_Up
+            || event->key() == Qt::Key_Down
+            || event->key() == Qt::Key_Home
+            || event->key() == Qt::Key_End
+            || event->key() == Qt::Key_PageUp
+            || event->key() == Qt::Key_PageDown
+            || event->key() == Qt::Key_Tab
+            || event->key() == Qt::Key_Backtab
+            || event->key() == Qt::Key_Escape;
+        const bool safeModifiers = !(event->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier));
+        const bool safeShortcut = event->matches(QKeySequence::Copy) || event->matches(QKeySequence::SelectAll);
+
+        if ((navigationKey && safeModifiers) || safeShortcut)
+            QGraphicsScene::keyPressEvent(event);
+        else
+            event->ignore();
         return;
+    }
 
     if ((event->modifiers() & Qt::ControlModifier) && (event->key() == Qt::Key_1))
         worksheetView()->actualSize();
@@ -2104,6 +2195,75 @@ void Worksheet::keyPressEvent(QKeyEvent* event)
     QGraphicsScene::keyPressEvent(event);
 }
 
+bool Worksheet::eventFilter(QObject* watched, QEvent* event)
+{
+    if (m_dragEntry && m_entryDragEventLoop)
+    {
+        if (event->type() == QEvent::MouseMove && watched->isWidgetType())
+        {
+            updateDragPosition(worksheetView()->sceneCursorPos());
+            event->accept();
+            return true;
+        }
+
+        if (event->type() == QEvent::Wheel)
+        {
+            auto* wheelEvent = static_cast<QWheelEvent*>(event);
+            int delta = wheelEvent->pixelDelta().y();
+            if (delta == 0)
+                delta = wheelEvent->angleDelta().y() / 2;
+
+            if (delta != 0)
+            {
+                worksheetView()->scrollBy(-delta);
+                updateDragPosition(worksheetView()->sceneCursorPos());
+            }
+
+            wheelEvent->accept();
+            return true;
+        }
+
+        if (event->type() == QEvent::MouseButtonRelease && watched->isWidgetType())
+        {
+            auto* mouseEvent = static_cast<QMouseEvent*>(event);
+            if (mouseEvent->button() == Qt::LeftButton)
+            {
+                m_dragAccepted = true;
+                m_entryDragEventLoop->quit();
+                mouseEvent->accept();
+                return true;
+            }
+        }
+
+        if (event->type() == QEvent::KeyPress)
+        {
+            auto* keyEvent = static_cast<QKeyEvent*>(event);
+            if (keyEvent->key() == Qt::Key_Escape)
+            {
+                m_dragAccepted = false;
+                m_entryDragEventLoop->quit();
+            }
+
+            keyEvent->accept();
+            return true;
+        }
+
+        if (event->type() == QEvent::ShortcutOverride || event->type() == QEvent::KeyRelease)
+        {
+            event->accept();
+            return true;
+        }
+
+        if (event->type() == QEvent::ApplicationDeactivate)
+        {
+            m_dragAccepted = false;
+            m_entryDragEventLoop->quit();
+        }
+    }
+
+    return QGraphicsScene::eventFilter(watched, event);
+}
+
 void Worksheet::setActionCollection(KActionCollection* collection)
 {
     m_collection = collection;
@@ -2654,14 +2814,14 @@ void Worksheet::dragLeaveEvent(QGraphicsSceneDragDropEvent* event)
     updateLayout();
 }
 
-void Worksheet::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
+void Worksheet::updateDragPosition(const QPointF& pos)
 {
-    if (!m_dragEntry) {
-        QGraphicsScene::dragMoveEvent(event);
+    if (!m_dragEntry)
         return;
-    }
 
-    QPointF pos = event->scenePos();
+    if (m_dragPixmapItem)
+        m_dragPixmapItem->setPos(pos - m_dragHotSpot);
+
     auto* entry = entryAt(pos);
     WorksheetEntry* prev = nullptr;
     WorksheetEntry* next = nullptr;
@@ -2681,8 +2841,6 @@ void Worksheet::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
         }
     }
 
-    const bool dragWithHierarchy = !m_hierarchySubentriesDrag.empty();
-
     if (m_placeholderEntry)
     {
         if (prev == m_placeholderEntry)
@@ -2694,7 +2852,7 @@ void Worksheet::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
 
     if (prev || next)
     {
-        const QSizeF placeholderSize = dragWithHierarchy ? m_hierarchyDragSize : m_dragEntry->size();
+        const QSizeF placeholderSize = m_dragWithHierarchy ? m_hierarchyDragSize : m_dragEntry->size();
 
         if (!m_placeholderEntry)
             m_placeholderEntry = new PlaceHolderEntry(this, QSizeF(0, 0));
@@ -2737,16 +2895,27 @@ void Worksheet::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
 
     const QPoint viewPos = worksheetView()->mapFromScene(pos);
     const int viewHeight = worksheetView()->viewport()->height();
-    if ((viewPos.y() < 10 || viewPos.y() > viewHeight - 10) &&
+    if ((viewPos.y() < DragScrollMargin || viewPos.y() > viewHeight - DragScrollMargin) &&
         !m_dragScrollTimer) {
         m_dragScrollTimer = new QTimer(this);
         m_dragScrollTimer->setSingleShot(true);
-        m_dragScrollTimer->setInterval(100);
+        m_dragScrollTimer->setInterval(DragScrollInterval);
         connect(m_dragScrollTimer, SIGNAL(timeout()), this,
                 SLOT(updateDragScrollTimer()));
         m_dragScrollTimer->start();
     }
 
+}
+
+void Worksheet::dragMoveEvent(QGraphicsSceneDragDropEvent* event)
+{
+    if (!m_dragEntry)
+    {
+        QGraphicsScene::dragMoveEvent(event);
+        return;
+    }
+
+    updateDragPosition(event->scenePos());
     event->accept();
 }
 
@@ -2766,16 +2935,18 @@ void Worksheet::updateDragScrollTimer()
     const QWidget* viewport = worksheetView()->viewport();
     const int viewHeight = viewport->height();
     if (!m_dragEntry || !(viewport->rect().contains(viewPos)) ||
-        (viewPos.y() >= 10 && viewPos.y() <= viewHeight - 10)) {
+        (viewPos.y() >= DragScrollMargin && viewPos.y() <= viewHeight - DragScrollMargin)) {
         delete m_dragScrollTimer;
         m_dragScrollTimer = nullptr;
         return;
     }
 
-    if (viewPos.y() < 10)
-        worksheetView()->scrollBy(-10*(10 - viewPos.y()));
+    if (viewPos.y() < DragScrollMargin)
+        worksheetView()->scrollBy(-DragScrollStep * (DragScrollMargin - viewPos.y()));
     else
-        worksheetView()->scrollBy(10*(viewHeight - viewPos.y()));
+        worksheetView()->scrollBy(DragScrollStep * (viewPos.y() - (viewHeight - DragScrollMargin)));
+
+    updateDragPosition(worksheetView()->sceneCursorPos());
 
     m_dragScrollTimer->start();
 }
@@ -2995,41 +3166,119 @@ void Worksheet::selectionEvaluate()
     // run entries in worksheet order: from top to down
     for (auto* entry = firstEntry(); entry; entry = entry->next())
         if (m_selectedEntries.indexOf(entry) != -1)
-            entry->evaluate();
+            entry->evaluate(WorksheetEntry::DoNothing);
 }
 
 void Worksheet::selectionMoveUp()
 {
-    bool moveHierarchyEntry = false;
+    bool moved = false;
+    WorksheetEntry* focusTarget = nullptr;
+    QSet<WorksheetEntry*> entriesMovedWithHierarchy;
+    for (auto* selectedEntry : m_selectedEntries)
+    {
+        if (selectedEntry->type() != HierarchyEntry::Type)
+            continue;
+
+        const auto subentries = hierarchySubelements(static_cast<HierarchyEntry*>(selectedEntry));
+        for (auto* subentry : subentries)
+            entriesMovedWithHierarchy.insert(subentry);
+    }
+
     // movement up should have an order from top to down.
     for(auto* entry = firstEntry(); entry; entry = entry->next())
         if(m_selectedEntries.indexOf(entry) != -1)
-            if (entry->previous() && m_selectedEntries.indexOf(entry->previous()) == -1)
+        {
+            if (entriesMovedWithHierarchy.contains(entry))
+                continue;
+
+            if (!focusTarget)
+                focusTarget = entry;
+            WorksheetEntry* previousEntry = entry->previous();
+            bool previousBlockSelected = previousEntry && m_selectedEntries.indexOf(previousEntry) != -1;
+            if (!previousBlockSelected && entry->type() == HierarchyEntry::Type)
+            {
+                auto* hierarchyEntry = static_cast<HierarchyEntry*>(entry);
+                for (auto* selectedEntry : m_selectedEntries)
+                {
+                    if (selectedEntry->type() != HierarchyEntry::Type)
+                        continue;
+
+                    auto* selectedHierarchy = static_cast<HierarchyEntry*>(selectedEntry);
+                    if (selectedHierarchy->level() != hierarchyEntry->level())
+                        continue;
+
+                    const auto subentries = hierarchySubelements(selectedHierarchy);
+                    WorksheetEntry* selectedBlockLast = subentries.empty() ? selectedEntry : subentries.back();
+                    if (selectedBlockLast == previousEntry)
+                    {
+                        previousBlockSelected = true;
+                        break;
+                    }
+                }
+            }
+
+            if (previousEntry && !previousBlockSelected)
             {
+                WorksheetEntry* oldPrevious = previousEntry;
                 entry->moveToPrevious(false);
-                if (entry->type() == HierarchyEntry::Type)
-                    moveHierarchyEntry = true;
+                moved = moved || entry->previous() != oldPrevious;
             }
-    if (moveHierarchyEntry)
+        }
+
+    if (moved)
+    {
         updateHierarchyLayout();
-    updateLayout();
+        updateLayout();
+        makeVisible(focusTarget);
+    }
 }
 
 void Worksheet::selectionMoveDown()
 {
-    bool moveHierarchyEntry = false;
+    bool moved = false;
+    WorksheetEntry* focusTarget = nullptr;
+    QSet<WorksheetEntry*> entriesMovedWithHierarchy;
+    for (auto* selectedEntry : m_selectedEntries)
+    {
+        if (selectedEntry->type() != HierarchyEntry::Type)
+            continue;
+
+        const auto subentries = hierarchySubelements(static_cast<HierarchyEntry*>(selectedEntry));
+        for (auto* subentry : subentries)
+            entriesMovedWithHierarchy.insert(subentry);
+    }
+
     // movement up should have an order from down to top.
     for(auto* entry = lastEntry(); entry; entry = entry->previous())
         if(m_selectedEntries.indexOf(entry) != -1)
-            if (entry->next() && m_selectedEntries.indexOf(entry->next()) == -1)
+        {
+            if (entriesMovedWithHierarchy.contains(entry))
+                continue;
+
+            if (!focusTarget)
+                focusTarget = entry;
+            WorksheetEntry* nextEntry = entry->next();
+            if (entry->type() == HierarchyEntry::Type)
             {
+                const auto subentries = hierarchySubelements(static_cast<HierarchyEntry*>(entry));
+                if (!subentries.empty())
+                    nextEntry = subentries.back()->next();
+            }
+
+            if (nextEntry && m_selectedEntries.indexOf(nextEntry) == -1)
+            {
+                WorksheetEntry* oldPrevious = entry->previous();
                 entry->moveToNext(false);
-                if (entry->type() == HierarchyEntry::Type)
-                    moveHierarchyEntry = true;
+                moved = moved || entry->previous() != oldPrevious;
             }
-    if (moveHierarchyEntry)
+        }
+
+    if (moved)
+    {
         updateHierarchyLayout();
-    updateLayout();
+        updateLayout();
+        makeVisible(focusTarget);
+    }
 }
 
 void Worksheet::notifyEntryFocus(WorksheetEntry* entry)
diff --git a/src/worksheet.h b/src/worksheet.h
index 7f6bed38..d2af0fd0 100644
--- a/src/worksheet.h
+++ b/src/worksheet.h
@@ -37,9 +37,11 @@ class PlaceHolderEntry;
 class WorksheetTextItem;
 
 class QAction;
-class QDrag;
+class QEventLoop;
 class QGraphicsObject;
+class QGraphicsPixmapItem;
 class QMenu;
+class QPixmap;
 class QPrinter;
 class QSyntaxHighlighter;
 class KActionCollection;
@@ -84,8 +86,8 @@ class Worksheet : public QGraphicsScene
 
     void setModified();
 
-    void startDrag(WorksheetEntry*, QDrag*);
-    void startDragWithHierarchy(HierarchyEntry*, QDrag*, QSizeF responsibleZoneSize);
+    void startDrag(WorksheetEntry*, const QPixmap&, const QPoint& hotSpot);
+    void startDragWithHierarchy(HierarchyEntry*, const QPixmap&, const QPoint& hotSpot, QSizeF responsibleZoneSize);
 
     void setActionCollection(KActionCollection*);
     QMenu* createContextMenu();
@@ -197,6 +199,7 @@ class Worksheet : public QGraphicsScene
     void print(QPrinter*);
     void paste();
     void focusEntry(WorksheetEntry*);
+    void focusEntry(WorksheetEntry*, int pos, qreal xCoord = 0);
 
     void evaluate();
     void evaluateCurrentEntry();
@@ -320,8 +323,12 @@ class Worksheet : public QGraphicsScene
     bool isValidEntry(WorksheetEntry*);
 
   private:
+    friend class WorksheetEntry;
+
     friend class WorksheetHierarchyManager;
 
+    bool eventFilter(QObject*, QEvent*) override;
+
   private Q_SLOTS:
     //void checkEntriesForSanity();
 
@@ -341,6 +348,8 @@ class Worksheet : public QGraphicsScene
     WorksheetEntry* entryAt(qreal x, qreal y);
     WorksheetEntry* entryAt(QPointF);
     WorksheetEntry* entryAt(int row);
+    bool execEntryDrag(const QPixmap&, const QPoint& hotSpot);
+    void updateDragPosition(const QPointF&);
     void removeDragPlaceholder();
     void updateEntryCursor(QGraphicsSceneMouseEvent*);
     void addEntryFromEntryCursor();
@@ -370,6 +379,11 @@ class Worksheet : public QGraphicsScene
     WorksheetEntry* m_firstEntry{nullptr};
     WorksheetEntry* m_lastEntry{nullptr};
     WorksheetEntry* m_dragEntry{nullptr};
+    QEventLoop* m_entryDragEventLoop{nullptr};
+    QGraphicsPixmapItem* m_dragPixmapItem{nullptr};
+    QPointF m_dragHotSpot;
+    bool m_dragAccepted{false};
+    bool m_dragWithHierarchy{false};
     std::vector<WorksheetEntry*> m_hierarchySubentriesDrag;
     QSizeF m_hierarchyDragSize;
     WorksheetEntry* m_choosenCursorEntry{nullptr};
diff --git a/src/worksheetcontrolitem.cpp b/src/worksheetcontrolitem.cpp
index 311d230d..823fd8b7 100644
--- a/src/worksheetcontrolitem.cpp
+++ b/src/worksheetcontrolitem.cpp
@@ -21,6 +21,11 @@ WorksheetControlItem::WorksheetControlItem(Worksheet* worksheet, WorksheetEntry*
     setFlags(flags() | QGraphicsItem::ItemIsMovable | QGraphicsItem::ItemIsFocusable);
 }
 
+QRectF WorksheetControlItem::boundingRect() const
+{
+    return rect().normalized().adjusted(-1, -1, 1, 1);
+}
+
 void WorksheetControlItem::paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
 {
     Q_UNUSED(option);
diff --git a/src/worksheetcontrolitem.h b/src/worksheetcontrolitem.h
index d7fb6181..133e40fc 100644
--- a/src/worksheetcontrolitem.h
+++ b/src/worksheetcontrolitem.h
@@ -16,6 +16,7 @@ class WorksheetControlItem: public QObject, public QGraphicsRectItem
   Q_OBJECT
   public:
     WorksheetControlItem(Worksheet* worksheet, WorksheetEntry* parent);
+    QRectF boundingRect() const override;
 
   Q_SIGNALS:
     void doubleClick();
diff --git a/src/worksheetentry.cpp b/src/worksheetentry.cpp
index cee2c97b..70f8ec39 100644
--- a/src/worksheetentry.cpp
+++ b/src/worksheetentry.cpp
@@ -19,18 +19,16 @@
 #include "worksheettoolbutton.h"
 #include "worksheetview.h"
 
-#include <QDrag>
 #include <QIcon>
 #include <QPropertyAnimation>
 #include <QParallelAnimationGroup>
 #include <QMetaMethod>
-#include <QMimeData>
 #include <QGraphicsProxyWidget>
-#include <QBitmap>
 #include <QJsonArray>
 #include <QJsonObject>
 #include <QPainter>
 #include <QGraphicsSceneMouseEvent>
+#include <QtMath>
 
 #include <KColorScheme>
 #include <KLocalizedString>
@@ -278,29 +276,26 @@ void WorksheetEntry::startDrag(QPointF grabPos)
     // We need reset entry cursor manually, because otherwise the entry cursor will be visible on draggable item
     worksheet()->resetEntryCursor();
 
-    QDrag* drag = new QDrag(worksheetView());
-    qDebug() << size();
     const qreal scale = worksheet()->renderer()->scale();
-    QPixmap pixmap((size()*scale).toSize());
+    constexpr qreal previewMargin = 2.0;
+    const QRectF previewRect = boundingRect().adjusted(-previewMargin, -previewMargin, previewMargin, previewMargin);
+    const QSizeF previewPixelSize = previewRect.size() * scale;
+    QPixmap pixmap(qCeil(previewPixelSize.width()), qCeil(previewPixelSize.height()));
     pixmap.fill(QColor(255, 255, 255, 0));
     QPainter painter(&pixmap);
-    const QRectF sceneRect = mapRectToScene(boundingRect());
-    worksheet()->render(&painter, pixmap.rect(), sceneRect);
+    const QRectF sceneRect = mapRectToScene(previewRect);
+    worksheet()->render(&painter, QRectF(QPointF(), previewPixelSize), sceneRect);
     painter.end();
-    QBitmap mask = pixmap.createMaskFromColor(QColor(255, 255, 255),
-                                              Qt::MaskInColor);
-    pixmap.setMask(mask);
 
-    drag->setPixmap(pixmap);
+    QPoint hotSpot;
     if (grabPos.isNull()) {
         const QPointF scenePos = worksheetView()->sceneCursorPos();
-        drag->setHotSpot((mapFromScene(scenePos) * scale).toPoint());
+        hotSpot = ((mapFromScene(scenePos) - previewRect.topLeft()) * scale).toPoint();
     } else {
-        drag->setHotSpot((grabPos * scale).toPoint());
+        hotSpot = ((grabPos - previewRect.topLeft()) * scale).toPoint();
     }
-    drag->setMimeData(new QMimeData());
 
-    worksheet()->startDrag(this, drag);
+    worksheet()->startDrag(this, pixmap, hotSpot);
 }
 
 
@@ -509,7 +504,7 @@ void WorksheetEntry::evaluateNext(EvaluationOption opt)
             entry->evaluate(EvaluateNext);
         } else if (opt == FocusNext) {
             worksheet()->setModified();
-            entry->focusEntry(WorksheetTextEditorItem::BottomRight);
+            worksheet()->focusEntry(entry, WorksheetTextEditorItem::BottomRight);
         } 
         else 
             worksheet()->setModified();
@@ -517,7 +512,7 @@ void WorksheetEntry::evaluateNext(EvaluationOption opt)
         if (!worksheet()->isLoadingFromFile() && (!isEmpty() || type() != CommandEntry::Type))
             worksheet()->appendCommandEntry();
         else
-            focusEntry();
+            worksheet()->focusEntry(this);
         worksheet()->setModified();
     }
 }
@@ -762,12 +757,6 @@ void WorksheetEntry::startRemoving(bool warn)
             return;
     }
 
-    if (!worksheet()->animationsEnabled()) {
-        m_aboutToBeRemoved = true;
-        remove();
-        return;
-    }
-
     if (m_aboutToBeRemoved)
         return;
 
@@ -775,17 +764,22 @@ void WorksheetEntry::startRemoving(bool warn)
         if (!next()) {
             if (previous() && previous()->isEmpty() &&
                 !previous()->aboutToBeRemoved()) {
-                previous()->focusEntry();
+                worksheet()->focusEntry(previous());
             } else {
                 WorksheetEntry* next = worksheet()->appendCommandEntry();
                 setNext(next);
-                next->focusEntry();
             }
         } else {
-            next()->focusEntry();
+            worksheet()->focusEntry(next());
         }
     }
 
+    if (!worksheet()->animationsEnabled()) {
+        m_aboutToBeRemoved = true;
+        remove();
+        return;
+    }
+
     if (m_animation) {
         endAnimation();
     }
@@ -1025,66 +1019,113 @@ void WorksheetEntry::setCellSelected(bool val)
 
 void WorksheetEntry::moveToNext(bool updateLayout)
 {
-    WorksheetEntry* next = this->next();
-    if (next)
+    WorksheetEntry* blockLast = this;
+    if (type() == HierarchyEntry::Type)
     {
-        if (next->next())
-        {
-            next->next()->setPrevious(this);
-            this->setNext(next->next());
-        }
-        else
-        {
-            worksheet()->setLastEntry(this);
-            this->setNext(nullptr);
-        }
+        const auto subentries = worksheet()->hierarchySubelements(static_cast<HierarchyEntry*>(this));
+        if (!subentries.empty())
+            blockLast = subentries.back();
+    }
 
-        next->setPrevious(this->previous());
-        next->setNext(this);
+    WorksheetEntry* nextBlockFirst = blockLast->next();
+    if (!nextBlockFirst)
+        return;
 
-        this->setPrevious(next);
-        if (next->previous())
-            next->previous()->setNext(next);
-        else
-            worksheet()->setFirstEntry(next);
+    WorksheetEntry* nextBlockLast = nextBlockFirst;
+    if (type() == HierarchyEntry::Type && nextBlockFirst->type() == HierarchyEntry::Type)
+    {
+        const auto subentries = worksheet()->hierarchySubelements(static_cast<HierarchyEntry*>(nextBlockFirst));
+        if (!subentries.empty())
+            nextBlockLast = subentries.back();
+    }
 
-        if (updateLayout)
-            worksheet()->updateLayout();
+    WorksheetEntry* before = previous();
+    WorksheetEntry* after = nextBlockLast->next();
 
-        worksheet()->setModified();
+    if (before)
+        before->setNext(nextBlockFirst);
+    else
+        worksheet()->setFirstEntry(nextBlockFirst);
+    nextBlockFirst->setPrevious(before);
+
+    nextBlockLast->setNext(this);
+    setPrevious(nextBlockLast);
+
+    blockLast->setNext(after);
+    if (after)
+        after->setPrevious(blockLast);
+    else
+        worksheet()->setLastEntry(blockLast);
+
+    if (updateLayout)
+    {
+        worksheet()->updateHierarchyLayout();
+        worksheet()->updateLayout();
     }
+
+    worksheet()->setModified();
 }
 
 void WorksheetEntry::moveToPrevious(bool updateLayout)
 {
-    WorksheetEntry* previous = this->previous();
-    if (previous)
+    WorksheetEntry* previousBlockLast = previous();
+    if (!previousBlockLast)
+        return;
+
+    WorksheetEntry* blockLast = this;
+    WorksheetEntry* previousBlockFirst = previousBlockLast;
+
+    if (type() == HierarchyEntry::Type)
     {
-        if (previous->previous())
-        {
-            previous->previous()->setNext(this);
-            this->setPrevious(previous->previous());
-        }
-        else
+        auto* hierarchyEntry = static_cast<HierarchyEntry*>(this);
+        const auto subentries = worksheet()->hierarchySubelements(hierarchyEntry);
+        if (!subentries.empty())
+            blockLast = subentries.back();
+
+        for (auto* candidate = previousBlockLast; candidate; candidate = candidate->previous())
         {
-            worksheet()->setFirstEntry(this);
-            this->setPrevious(nullptr);
+            if (candidate->type() != HierarchyEntry::Type)
+                continue;
+
+            auto* candidateHierarchy = static_cast<HierarchyEntry*>(candidate);
+            if (candidateHierarchy->level() < hierarchyEntry->level())
+                break;
+            if (candidateHierarchy->level() != hierarchyEntry->level())
+                continue;
+
+            const auto candidateSubentries = worksheet()->hierarchySubelements(candidateHierarchy);
+            WorksheetEntry* candidateLast = candidateSubentries.empty() ? candidate : candidateSubentries.back();
+            if (candidateLast == previousBlockLast)
+                previousBlockFirst = candidate;
+            break;
         }
+    }
 
-        previous->setNext(this->next());
-        previous->setPrevious(this);
+    WorksheetEntry* before = previousBlockFirst->previous();
+    WorksheetEntry* after = blockLast->next();
 
-        this->setNext(previous);
-        if (previous->next())
-            previous->next()->setPrevious(previous);
-        else
-            worksheet()->setLastEntry(previous);
+    if (before)
+        before->setNext(this);
+    else
+        worksheet()->setFirstEntry(this);
+    setPrevious(before);
 
-        if (updateLayout)
-            worksheet()->updateLayout();
+    blockLast->setNext(previousBlockFirst);
+    previousBlockFirst->setPrevious(blockLast);
 
-        worksheet()->setModified();
+    previousBlockLast->setNext(after);
+    if (after)
+        after->setPrevious(previousBlockLast);
+    else
+        worksheet()->setLastEntry(previousBlockLast);
+
+    if (updateLayout)
+    {
+        worksheet()->updateHierarchyLayout();
+        worksheet()->updateLayout();
     }
+
+    worksheet()->setModified();
 }
 
 void WorksheetEntry::recalculateControlGeometry()
diff --git a/src/worksheettexteditoritem.cpp b/src/worksheettexteditoritem.cpp
index 31713322..6c843326 100644
--- a/src/worksheettexteditoritem.cpp
+++ b/src/worksheettexteditoritem.cpp
@@ -1002,6 +1002,22 @@ void WorksheetTextEditorItem::keyPressEvent(QKeyEvent* event)
     if (!m_view->hasFocus())
         m_view->setFocus();
 
+    if (!worksheet()->isReadOnly() && modifiers == Qt::ControlModifier
+        && (key == Qt::Key_Up || key == Qt::Key_Down))
+    {
+        auto* entry = qobject_cast<WorksheetEntry*>(parentObject());
+        if (entry)
+        {
+            if (key == Qt::Key_Up)
+                entry->moveToPrevious();
+            else
+                entry->moveToNext();
+        }
+
+        event->accept();
+        return;
+    }
+
     QString actionName;
     const KTextEditor::Cursor cursor = m_view->cursorPosition();
     const bool isSimpleNav = (modifiers == Qt::NoModifier) || ((modifiers & Qt::ShiftModifier) && !(modifiers & ~Qt::ShiftModifier));
diff --git a/src/worksheettextitem.cpp b/src/worksheettextitem.cpp
index 9fb9fcb0..19c6abc9 100644
--- a/src/worksheettextitem.cpp
+++ b/src/worksheettextitem.cpp
@@ -404,6 +404,23 @@ Cantor::Session* WorksheetTextItem::session()
 void WorksheetTextItem::keyPressEvent(QKeyEvent* event)
 {
     worksheet()->updateFocusedTextItem(this);
+
+    if (!worksheet()->isReadOnly() && event->modifiers() == Qt::ControlModifier
+        && (event->key() == Qt::Key_Up || event->key() == Qt::Key_Down))
+    {
+        auto* entry = qobject_cast<WorksheetEntry*>(parentObject());
+        if (entry)
+        {
+            if (event->key() == Qt::Key_Up)
+                entry->moveToPrevious();
+            else
+                entry->moveToNext();
+        }
+
+        event->accept();
+        return;
+    }
+
     switch (event->key()) {
     case Qt::Key_Left:
         if (event->modifiers() == Qt::NoModifier && textCursor().atStart()) {
diff --git a/src/worksheetview.cpp b/src/worksheetview.cpp
index 13760b0a..55b14974 100644
--- a/src/worksheetview.cpp
+++ b/src/worksheetview.cpp
@@ -186,21 +186,8 @@ void WorksheetView::scrollBy(int dy)
     if (!verticalScrollBar())
         return;
 
-    int ny = verticalScrollBar()->value() + dy;
-    if (ny < 0)
-        ny = 0;
-    else if (ny > verticalScrollBar()->maximum())
-        ny = verticalScrollBar()->maximum();
-
-    int x;
-    if (horizontalScrollBar())
-        x = horizontalScrollBar()->value();
-    else
-        x = 0;
-
-    const qreal w = viewport()->width() / m_scale;
-    const qreal h = viewport()->height() / m_scale;
-    makeVisible(QRectF(x, ny, w, h));
+    endAnimation();
+    verticalScrollBar()->setValue(verticalScrollBar()->value() + dy);
 }
 
 void WorksheetView::endAnimation()
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.