tdf#155796 sc: fix select with merged cells

When selecting multiple cells or modifying a selection with shift+arrow
make sure that a merge group is never partially selected.

This also fixes tdf#128678

Change-Id: Ida00939cec11240c0d06375feb21afa82a6876da
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/154093
Tested-by: Jenkins
Reviewed-by: Jaume Pujantell <jaume.pujantell@collabora.com>
(cherry picked from commit 341029de72cf957b7bc7775e51544070d4a49874)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/154126
diff --git a/sc/qa/unit/uicalc/uicalc2.cxx b/sc/qa/unit/uicalc/uicalc2.cxx
index 1ec77ad..ea5956c 100644
--- a/sc/qa/unit/uicalc/uicalc2.cxx
+++ b/sc/qa/unit/uicalc/uicalc2.cxx
@@ -1445,6 +1445,33 @@ CPPUNIT_TEST_FIXTURE(ScUiCalcTest2, testTdf156174)
    CPPUNIT_ASSERT(!pDBs->empty());
}

CPPUNIT_TEST_FIXTURE(ScUiCalcTest2, testTdf155796)
{
    createScDoc();

    goToCell("A1:A3");
    dispatchCommand(mxComponent, ".uno:ToggleMergeCells", {});
    goToCell("A4:A6");
    dispatchCommand(mxComponent, ".uno:ToggleMergeCells", {});

    goToCell("A1:A6");

    ScModelObj* pModelObj = comphelper::getFromUnoTunnel<ScModelObj>(mxComponent);
    pModelObj->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_SHIFT | KEY_UP);
    Scheduler::ProcessEventsToIdle();

    ScRangeList aMarkedArea = getViewShell()->GetViewData().GetMarkData().GetMarkedRanges();
    ScDocument* pDoc = getScDoc();
    OUString aMarkedAreaString;
    ScRangeStringConverter::GetStringFromRangeList(aMarkedAreaString, &aMarkedArea, pDoc,
                                                   formula::FormulaGrammar::CONV_OOO);

    // Without the fix in place, this test would have failed with
    // - Expected: Sheet1.A1:Sheet1.A3
    // - Actual  : Sheet1.A1:Sheet1.A5
    CPPUNIT_ASSERT_EQUAL(OUString("Sheet1.A1:Sheet1.A3"), aMarkedAreaString);
}

CPPUNIT_PLUGIN_IMPLEMENT();

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/sc/source/ui/view/tabview2.cxx b/sc/source/ui/view/tabview2.cxx
index 6b1cfef..442dc92 100644
--- a/sc/source/ui/view/tabview2.cxx
+++ b/sc/source/ui/view/tabview2.cxx
@@ -58,6 +58,19 @@ bool isCellQualified(const ScDocument* pDoc, SCCOL nCol, SCROW nRow, SCTAB nTab,
    return true;
}

bool areCellsQualified(const ScDocument* pDoc, SCCOL nColStart, SCROW nRowStart, SCCOL nColEnd,
                       SCROW nRowEnd, SCTAB nTab, bool bSelectLocked, bool bSelectUnlocked)
{
    PutInOrder(nColStart, nColEnd);
    PutInOrder(nRowStart, nRowEnd);
    for (SCCOL col = nColStart; col <= nColEnd; ++col)
        for (SCROW row = nRowStart; row <= nRowEnd; ++row)
            if (!isCellQualified(pDoc, col, row, nTab, bSelectLocked, bSelectUnlocked))
                return false;

    return true;
}

void moveCursorByProtRule(
    SCCOL& rCol, SCROW& rRow, SCCOL nMovX, SCROW nMovY, SCTAB nTab, const ScDocument* pDoc)
{
@@ -180,13 +193,9 @@ bool checkBoundary(const ScDocument* pDoc, SCCOL& rCol, SCROW& rRow)
    return bGood;
}

void moveCursorByMergedCell(
    SCCOL& rCol, SCROW& rRow, SCCOL nMovX, SCROW nMovY, SCTAB nTab,
    const ScDocument* pDoc, const ScViewData& rViewData)
void moveCursorByMergedCell(SCCOL& rCol, SCROW& rRow, SCCOL nMovX, SCROW nMovY, SCCOL nStartX,
                            SCROW nStartY, SCTAB nTab, const ScDocument* pDoc)
{
    SCCOL nOrigX = rViewData.GetCurX();
    SCROW nOrigY = rViewData.GetCurY();

    const ScTableProtection* pTabProtection = pDoc->GetTabProtection(nTab);
    bool bSelectLocked = true;
    bool bSelectUnlocked = true;
@@ -196,108 +205,198 @@ void moveCursorByMergedCell(
        bSelectUnlocked = pTabProtection->isOptionEnabled(ScTableProtection::SELECT_UNLOCKED_CELLS);
    }

    const ScMergeAttr* pMergeAttr = pDoc->GetAttr(nOrigX, nOrigY, nTab, ATTR_MERGE);

    bool bOriginMerged = false;
    SCCOL nColSpan = 1;
    SCROW nRowSpan = 1;
    if (pMergeAttr && pMergeAttr->IsMerged())
    {
        nColSpan = pMergeAttr->GetColMerge();
        nRowSpan = pMergeAttr->GetRowMerge();
        bOriginMerged = true;
    }

    if (nMovX > 0)
    {
        SCCOL nOld = rCol;
        if (bOriginMerged)
        {
            // Original cell is merged.  Push the block end outside the merged region.
            if (nOrigX < pDoc->MaxCol() && nOrigX < rCol && rCol <= nOrigX + nColSpan - 1)
                rCol = nOrigX + nColSpan;
        }
        else
        {
            pDoc->SkipOverlapped(rCol, rRow, nTab);
        }
        SCROW rowStart = std::min(rRow, nStartY);
        SCROW rowEnd = std::max(rRow, nStartY);

        if (nOld < rCol)
        for (SCROW i = rowStart; i <= rowEnd && rCol < nStartX;)
        {
            // The block end has moved.  Check the protection setting and move back if needed.
            checkBoundary(pDoc, rCol, rRow);
            if (!isCellQualified(pDoc, rCol, rRow, nTab, bSelectLocked, bSelectUnlocked))
                --rCol;
            SCCOL tmpCol = rCol;
            while (tmpCol < pDoc->MaxCol() && pDoc->IsHorOverlapped(tmpCol, i, nTab))
                ++tmpCol;
            if (tmpCol != rCol)
            {
                i = rowStart;
                if (tmpCol > nStartX)
                    --tmpCol;
                if (!areCellsQualified(pDoc, rCol + 1, rowStart, tmpCol, rowEnd, nTab,
                                       bSelectLocked, bSelectUnlocked))
                    break;
                rCol = tmpCol;
            }
            else
                ++i;
        }
    }
    if (nMovX < 0)
    else if (nMovX < 0)
    {
        SCCOL nOld = rCol;
        if (bOriginMerged)
        {
            if (nOrigX > 0 && nOrigX <= rCol && rCol < nOrigX + nColSpan - 1)
                // Block end is still within the merged region.  Push it outside.
                rCol = nOrigX - 1;
        }
        else
        {
            pDoc->SkipOverlapped(rCol, rRow, nTab);
        }
        SCROW rowStart = std::min(rRow, nStartY);
        SCROW rowEnd = std::max(rRow, nStartY);

        if (nOld > rCol)
        for (SCROW i = rowStart; i <= rowEnd && rCol > nStartX;)
        {
            // The block end has moved.  Check the protection setting and move back if needed.
            checkBoundary(pDoc, rCol, rRow);
            if (!isCellQualified(pDoc, rCol, rRow, nTab, bSelectLocked, bSelectUnlocked))
                ++rCol;
            SCCOL tmpCol = rCol;
            while (tmpCol >= 0 && pDoc->IsHorOverlapped(tmpCol + 1, i, nTab))
                --tmpCol;
            if (tmpCol != rCol)
            {
                i = rowStart;
                if (tmpCol < nStartX)
                    ++tmpCol;
                if (!areCellsQualified(pDoc, rCol - 1, rowStart, tmpCol, rowEnd, nTab,
                                       bSelectLocked, bSelectUnlocked))
                    break;
                rCol = tmpCol;
            }
            else
                ++i;
        }
    }

    if (nMovY > 0)
    {
        SCROW nOld = rRow;
        if (bOriginMerged)
        {
            // Original cell is merged.  Push the block end outside the merged region.
            if (nOrigY < pDoc->MaxRow() && nOrigY < rRow && rRow <= nOrigY + nRowSpan - 1)
                rRow = nOrigY + nRowSpan;
        }
        else
        {
            pDoc->SkipOverlapped(rCol, rRow, nTab);
        }
        SCCOL colStart = std::min(rCol, nStartX);
        SCCOL colEnd = std::max(rCol, nStartX);

        if (nOld < rRow)
        for (SCCOL i = colStart; i <= colEnd && rRow < nStartY;)
        {
            // The block end has moved.  Check the protection setting and move back if needed.
            checkBoundary(pDoc, rCol, rRow);
            if (!isCellQualified(pDoc, rCol, rRow, nTab, bSelectLocked, bSelectUnlocked))
                --rRow;
            SCROW tmpRow = rRow;
            while (tmpRow < pDoc->MaxRow() && pDoc->IsVerOverlapped(i, tmpRow, nTab))
                ++tmpRow;
            if (tmpRow != rRow)
            {
                i = colStart;
                if (tmpRow > nStartY)
                    --tmpRow;
                if (!areCellsQualified(pDoc, colStart, rRow + 1, colEnd, tmpRow, nTab,
                                       bSelectLocked, bSelectUnlocked))
                    break;
                rRow = tmpRow;
            }
            else
                ++i;
        }
    }
    if (nMovY >= 0)
        return;
    else if (nMovY < 0)
    {
        SCCOL colStart = std::min(rCol, nStartX);
        SCCOL colEnd = std::max(rCol, nStartX);

    SCROW nOld = rRow;
    if (bOriginMerged)
    {
        if (nOrigY > 0 && nOrigY <= rRow && rRow < nOrigY + nRowSpan - 1)
            // Block end is still within the merged region.  Push it outside.
            rRow = nOrigY - 1;
    }
    else
    {
        pDoc->SkipOverlapped(rCol, rRow, nTab);
    }

    if (nOld > rRow)
    {
        // The block end has moved.  Check the protection setting and move back if needed.
        checkBoundary(pDoc, rCol, rRow);
        if (!isCellQualified(pDoc, rCol, rRow, nTab, bSelectLocked, bSelectUnlocked))
            ++rRow;
        for (SCCOL i = colStart; i <= colEnd && rRow > nStartY;)
        {
            SCROW tmpRow = rRow;
            while (tmpRow >= 0 && pDoc->IsVerOverlapped(i, tmpRow + 1, nTab))
                --tmpRow;
            if (tmpRow != rRow)
            {
                i = colStart;
                if (tmpRow < nStartY)
                    ++tmpRow;
                if (!areCellsQualified(pDoc, colStart, rRow - 1, colEnd, tmpRow, nTab,
                                       bSelectLocked, bSelectUnlocked))
                    break;
                rRow = tmpRow;
            }
            else
                ++i;
        }
    }
}

void moveCursorToProperSide(SCCOL& rCol, SCROW& rRow, SCCOL nMovX, SCROW nMovY, SCCOL nStartX,
                            SCROW nStartY, SCTAB nTab, const ScDocument* pDoc)
{
    SCCOL tmpCol = rCol;
    SCROW tmpRow = rRow;

    if (nMovX > 0 && nStartX < pDoc->MaxCol() && rCol < nStartX)
    {
        SCROW rowStart = std::min(rRow, nStartY);
        SCROW rowEnd = std::max(rRow, nStartY);
        for (SCROW i = rowStart; i <= rowEnd && tmpCol < nStartX;)
        {
            if (pDoc->IsHorOverlapped(tmpCol + 1, i, nTab))
            {
                do
                {
                    ++tmpCol;
                } while (pDoc->IsHorOverlapped(tmpCol + 1, i, nTab));
                i = rowStart;
            }
            else
                ++i;
        }
        if (tmpCol < nStartX)
            tmpCol = rCol;
    }
    else if (nMovX < 0 && nStartX > 0 && rCol > nStartX)
    {
        SCROW rowStart = std::min(rRow, nStartY);
        SCROW rowEnd = std::max(rRow, nStartY);
        for (SCROW i = rowStart; i <= rowEnd && tmpCol > nStartX;)
        {
            if (pDoc->IsHorOverlapped(tmpCol, i, nTab))
            {
                do
                {
                    --tmpCol;
                } while (pDoc->IsHorOverlapped(tmpCol, i, nTab));
                i = rowStart;
            }
            else
                ++i;
        }
        if (tmpCol > nStartX)
            tmpCol = rCol;
    }

    if (nMovY > 0 && nStartY < pDoc->MaxRow() && rRow < nStartY)
    {
        SCCOL colStart = std::min(rCol, nStartX);
        SCCOL colEnd = std::max(rCol, nStartX);
        for (SCCOL i = colStart; i <= colEnd && tmpRow < nStartY;)
        {
            if (pDoc->IsVerOverlapped(i, tmpRow + 1, nTab))
            {
                do
                {
                    ++tmpRow;
                } while (pDoc->IsVerOverlapped(i, tmpRow + 1, nTab));
                i = colStart;
            }
            else
                ++i;
        }
        if (tmpRow < nStartY)
            tmpRow = rRow;
    }
    else if (nMovY < 0 && nStartY > 0 && rRow > nStartY)
    {
        SCCOL colStart = std::min(rCol, nStartX);
        SCCOL colEnd = std::max(rCol, nStartX);
        for (SCCOL i = colStart; i <= colEnd && tmpRow > nStartY;)
        {
            if (pDoc->IsVerOverlapped(i, tmpRow, nTab))
            {
                do
                {
                    --tmpRow;
                } while (pDoc->IsVerOverlapped(i, tmpRow, nTab));
                i = colStart;
            }
            else
                ++i;
        }
        if (tmpRow > nStartY)
            tmpRow = rRow;
    }

    if (tmpCol != rCol)
        rCol = tmpCol;
    if (tmpRow != rRow)
        rRow = tmpRow;
}
}

void ScTabView::PaintMarks(SCCOL nStartCol, SCROW nStartRow, SCCOL nEndCol, SCROW nEndRow )
@@ -491,88 +590,39 @@ void ScTabView::MarkCursor( SCCOL nCurX, SCROW nCurY, SCTAB nCurZ,

        if ( bCellSelection )
        {
            // Expand selection area accordingly when the current selection ends
            // with a merged cell.
            SCCOL nCurXOffset = 0;
            SCCOL nBlockStartXOffset = 0;
            SCROW nCurYOffset = 0;
            SCROW nBlockStartYOffset = 0;
            bool bBlockStartMerged = false;

            // The following block checks whether or not the "BlockStart" (anchor)
            // cell is merged.  If it's merged, it'll then move the position of the
            // anchor cell to the corner that's diagonally opposite of the
            // direction of a current selection area.  For instance, if a current
            // selection is moving in the upperleft direction, the anchor cell will
            // move to the lower-right corner of the merged anchor cell, and so on.

            const ScMergeAttr* pMergeAttr =
                rDocument.GetAttr( nBlockStartXOrig, nBlockStartYOrig, nTab, ATTR_MERGE );
            if ( pMergeAttr->IsMerged() )
            // Expand selection area accordingly when the current selection cuts
            // through a merged cell.
            ScRange cellSel(nBlockStartXOrig, nBlockStartYOrig, nTab, nCurX, nCurY, nTab);
            cellSel.PutInOrder();
            ScRange oldSel;
            do
            {
                SCCOL nColSpan = pMergeAttr->GetColMerge();
                SCROW nRowSpan = pMergeAttr->GetRowMerge();
                oldSel = cellSel;
                rDocument.ExtendOverlapped(cellSel);
                rDocument.ExtendMerge(cellSel);
            } while (oldSel != cellSel);

                if ( nCurX < nBlockStartXOrig + nColSpan - 1 || nCurY < nBlockStartYOrig + nRowSpan - 1 )
                {
                    nBlockStartX = nCurX >= nBlockStartXOrig ? nBlockStartXOrig : nBlockStartXOrig + nColSpan - 1;
                    nBlockStartY = nCurY >= nBlockStartYOrig ? nBlockStartYOrig : nBlockStartYOrig + nRowSpan - 1;
                    nCurXOffset  = (nCurX >= nBlockStartXOrig && nCurX < nBlockStartXOrig + nColSpan - 1) ?
                        nBlockStartXOrig - nCurX + nColSpan - 1 : 0;
                    nCurYOffset  = (nCurY >= nBlockStartYOrig && nCurY < nBlockStartYOrig + nRowSpan - 1) ?
                        nBlockStartYOrig - nCurY + nRowSpan - 1 : 0;
                    bBlockStartMerged = true;
                }
            }

            // The following block checks whether or not the current cell is
            // merged.  If it is, it'll then set the appropriate X & Y offset
            // values (nCurXOffset & nCurYOffset) such that the selection area will
            // grow by those specified offset amounts.  Note that the values of
            // nCurXOffset/nCurYOffset may also be specified in the previous code
            // block, in which case whichever value is greater will take on.

            pMergeAttr = rDocument.GetAttr( nCurX, nCurY, nTab, ATTR_MERGE );
            if ( pMergeAttr->IsMerged() )
            // Preserve the directionallity of the selection
            if (nCurX >= nBlockStartXOrig)
            {
                SCCOL nColSpan = pMergeAttr->GetColMerge();
                SCROW nRowSpan = pMergeAttr->GetRowMerge();

                if ( nBlockStartX < nCurX + nColSpan - 1 || nBlockStartY < nCurY + nRowSpan - 1 )
                {
                    if ( nBlockStartX <= nCurX + nColSpan - 1 )
                    {
                        SCCOL nCurXOffsetTemp = (nCurX < nCurX + nColSpan - 1) ? nColSpan - 1 : 0;
                        nCurXOffset = std::max(nCurXOffset, nCurXOffsetTemp);
                    }
                    if ( nBlockStartY <= nCurY + nRowSpan - 1 )
                    {
                        SCROW nCurYOffsetTemp = (nCurY < nCurY + nRowSpan - 1) ? nRowSpan - 1 : 0;
                        nCurYOffset = std::max(nCurYOffset, nCurYOffsetTemp);
                    }
                    if ( ( nBlockStartX > nCurX || nBlockStartY > nCurY ) &&
                         ( nBlockStartX <= nCurX + nColSpan - 1 || nBlockStartY <= nCurY + nRowSpan - 1 ) )
                    {
                        nBlockStartXOffset = (nBlockStartX > nCurX && nBlockStartX <= nCurX + nColSpan - 1) ? nCurX - nBlockStartX : 0;
                        nBlockStartYOffset = (nBlockStartY > nCurY && nBlockStartY <= nCurY + nRowSpan - 1) ? nCurY - nBlockStartY : 0;
                    }
                }
                nBlockStartX = cellSel.aStart.Col();
                nBlockEndX = cellSel.aEnd.Col();
            }
            else
            {
                // The current cell is not merged.  Move the anchor cell to its
                // original position.
                if ( !bBlockStartMerged )
                {
                    nBlockStartX = nBlockStartXOrig;
                    nBlockStartY = nBlockStartYOrig;
                }
                nBlockStartX = cellSel.aEnd.Col();
                nBlockEndX = cellSel.aStart.Col();
            }

            nBlockStartX = nBlockStartX + nBlockStartXOffset >= 0 ? nBlockStartX + nBlockStartXOffset : 0;
            nBlockStartY = nBlockStartY + nBlockStartYOffset >= 0 ? nBlockStartY + nBlockStartYOffset : 0;
            nBlockEndX = std::min<SCCOL>(nCurX + nCurXOffset, rDocument.MaxCol());
            nBlockEndY = std::min(nCurY + nCurYOffset, rDocument.MaxRow());
            if (nCurY >= nBlockStartYOrig)
            {
                nBlockStartY = cellSel.aStart.Row();
                nBlockEndY = cellSel.aEnd.Row();
            }
            else
            {
                nBlockStartY = cellSel.aEnd.Row();
                nBlockEndY = cellSel.aStart.Row();
            }
        }
        else
        {
@@ -586,8 +636,8 @@ void ScTabView::MarkCursor( SCCOL nCurX, SCROW nCurY, SCTAB nCurZ,
        UpdateSelectionOverlay();
        SelectionChanged();

        nOldCurX = nCurX;
        nOldCurY = nCurY;
        nOldCurX = nBlockEndX;
        nOldCurY = nBlockEndY;

        aViewData.GetViewShell()->UpdateInputHandler();
    }
@@ -973,11 +1023,22 @@ void ScTabView::ExpandBlock(SCCOL nMovX, SCROW nMovY, ScFollowMode eMode)
        // Note that the origin position *never* moves during selection.

        if (!IsBlockMode())
        {
            InitBlockMode(nOrigX, nOrigY, nTab, true);
            const ScMergeAttr* pMergeAttr = rDoc.GetAttr(nOrigX, nOrigY, nTab, ATTR_MERGE);
            if (pMergeAttr && pMergeAttr->IsMerged())
            {
                nBlockEndX = nOrigX + pMergeAttr->GetColMerge() - 1;
                nBlockEndY = nOrigY + pMergeAttr->GetRowMerge() - 1;
            }
        }

        moveCursorToProperSide(nBlockEndX, nBlockEndY, nMovX, nMovY, nBlockStartX, nBlockStartY,
                               nTab, &rDoc);
        moveCursorByProtRule(nBlockEndX, nBlockEndY, nMovX, nMovY, nTab, &rDoc);
        checkBoundary(&rDoc, nBlockEndX, nBlockEndY);
        moveCursorByMergedCell(nBlockEndX, nBlockEndY, nMovX, nMovY, nTab, &rDoc, aViewData);
        moveCursorByMergedCell(nBlockEndX, nBlockEndY, nMovX, nMovY, nBlockStartX, nBlockStartY,
                               nTab, &rDoc);
        checkBoundary(&rDoc, nBlockEndX, nBlockEndY);

        MarkCursor(nBlockEndX, nBlockEndY, nTab, false, false, true);