diff --git a/sc/qa/unit/ucalc.cxx b/sc/qa/unit/ucalc.cxx index c57ff746722f..74f1e873c4fc 100644 --- a/sc/qa/unit/ucalc.cxx +++ b/sc/qa/unit/ucalc.cxx @@ -5943,6 +5943,214 @@ CPPUNIT_TEST_FIXTURE(Test, testAreasWithNotes) m_pDoc->DeleteTab(0); } +namespace +{ +// A header row, then empty rows that carry a border on every side, down to nLastBorderedRow. +// This is the shape of a pre-printed form, where the bordered rows are filled in later. +void lcl_makeBorderedForm(ScDocument* pDoc, SCROW nLastBorderedRow) +{ + pDoc->SetString(0, 0, 0, u"Header"_ustr); + + ::editeng::SvxBorderLine aLine(nullptr, 50, SvxBorderLineStyle::SOLID); + SvxBoxItem aBorderItem(ATTR_BORDER); + aBorderItem.SetLine(&aLine, SvxBoxItemLine::LEFT); + aBorderItem.SetLine(&aLine, SvxBoxItemLine::RIGHT); + aBorderItem.SetLine(&aLine, SvxBoxItemLine::TOP); + aBorderItem.SetLine(&aLine, SvxBoxItemLine::BOTTOM); + ScPatternAttr aBordered(pDoc->getCellAttributeHelper()); + aBordered.ItemSetPut(aBorderItem); + pDoc->ApplyPatternAreaTab(0, 1, 2, nLastBorderedRow, 0, aBordered); +} + +// The pattern of a single border line, for marking one cell or one row. +ScPatternAttr lcl_oneLine(ScDocument* pDoc, SvxBoxItemLine eSide) +{ + ::editeng::SvxBorderLine aLine(nullptr, 50, SvxBorderLineStyle::SOLID); + SvxBoxItem aBorderItem(ATTR_BORDER); + aBorderItem.SetLine(&aLine, eSide); + ScPatternAttr aPattern(pDoc->getCellAttributeHelper()); + aPattern.ItemSetPut(aBorderItem); + return aPattern; +} + +ScPatternAttr lcl_colour(ScDocument* pDoc, const Color& rColor) +{ + ScPatternAttr aPattern(pDoc->getCellAttributeHelper()); + aPattern.ItemSetPut(SvxBrushItem(rColor, ATTR_BACKGROUND)); + return aPattern; +} + +SCROW lcl_printAreaEndRow(ScDocument* pDoc) +{ + SCCOL nEndCol; + SCROW nEndRow; + CPPUNIT_ASSERT(pDoc->GetPrintArea(0, nEndCol, nEndRow, false)); + return nEndRow; +} +} + +CPPUNIT_TEST_FIXTURE(Test, testPrintAreaCoversABorderedForm) +{ + // Empty rows carrying a border are printed, so they belong to the print area even though + // no cell in them holds anything. + m_pDoc->InsertTab(0, u"Form"_ustr); + lcl_makeBorderedForm(m_pDoc, 99); + + SCCOL nEndCol; + SCROW nEndRow; + CPPUNIT_ASSERT(m_pDoc->GetPrintArea(0, nEndCol, nEndRow, false)); + CPPUNIT_ASSERT_EQUAL_MESSAGE("the printout reaches the last bordered row", + static_cast(99), nEndRow); + CPPUNIT_ASSERT_EQUAL_MESSAGE("the printout reaches the last bordered column", + static_cast(2), nEndCol); + m_pDoc->DeleteTab(0); + + // A form of over a thousand rows is still a form. + m_pDoc->InsertTab(0, u"LongForm"_ustr); + lcl_makeBorderedForm(m_pDoc, 1499); + CPPUNIT_ASSERT_EQUAL_MESSAGE("the printout reaches the last bordered row", + static_cast(1499), lcl_printAreaEndRow(m_pDoc)); + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(Test, testPrintAreaIgnoresAColourBelowTheContent) +{ + // A colour below the last cell with content stays out of the print area, wherever it sits + // and however far it runs. + const SCROW nLast = m_pDoc->MaxRow(); + + const std::pair aRanges[] = { + { 0, nLast }, // the whole column + { 1, nLast }, // from under the header to the last row + { 10, nLast }, // from below the content to the last row + { 10, nLast - 1 }, // stopping one row short of the last row + { 49999, 49999 }, // one cell far below the content + }; + + sal_Int32 nSheet = 0; + for (const auto& rRange : aRanges) + { + m_pDoc->InsertTab(0, "Colour" + OUString::number(nSheet++)); + m_pDoc->SetString(0, 0, 0, u"Header"_ustr); + m_pDoc->ApplyPatternAreaTab(0, rRange.first, 0, rRange.second, 0, + lcl_colour(m_pDoc, COL_BLUE)); + + OString aMessage = "a colour over rows " + OString::number(rRange.first) + " to " + + OString::number(rRange.second); + CPPUNIT_ASSERT_EQUAL_MESSAGE(aMessage.getStr(), static_cast(0), + lcl_printAreaEndRow(m_pDoc)); + m_pDoc->DeleteTab(0); + } +} + +CPPUNIT_TEST_FIXTURE(Test, testPrintAreaIgnoresABorderTooWideForAForm) +{ + // A border far below the content, or one many times wider than a form, stays out of the + // print area. The printout ends at the last row with content. + const SCROW nLast = m_pDoc->MaxRow(); + + const std::pair aRanges[] = { + { nLast, nLast }, // the last row of the sheet + { 10, 50000 }, // reaching far below the content + }; + + sal_Int32 nSheet = 0; + for (const auto& rRange : aRanges) + { + m_pDoc->InsertTab(0, "Border" + OUString::number(nSheet++)); + m_pDoc->SetString(0, 0, 0, u"Header"_ustr); + m_pDoc->ApplyPatternAreaTab(0, rRange.first, 0, rRange.second, 0, + lcl_oneLine(m_pDoc, SvxBoxItemLine::BOTTOM)); + + OString aMessage = "a border over rows " + OString::number(rRange.first) + " to " + + OString::number(rRange.second); + CPPUNIT_ASSERT_EQUAL_MESSAGE(aMessage.getStr(), static_cast(0), + lcl_printAreaEndRow(m_pDoc)); + m_pDoc->DeleteTab(0); + } +} + +CPPUNIT_TEST_FIXTURE(Test, testPrintAreaIgnoresAFrameRoundAWholeColumn) +{ + // A frame round a whole column keeps the side lines on every row, the top line on the + // first row and the bottom line on the last, so it arrives as three ranges and the middle + // one covers nearly the sheet. The printout ends at the last row with content. + m_pDoc->InsertTab(0, u"Frame"_ustr); + m_pDoc->SetString(0, 0, 0, u"Header"_ustr); + const SCROW nLast = m_pDoc->MaxRow(); + + ::editeng::SvxBorderLine aLine(nullptr, 50, SvxBorderLineStyle::SOLID); + SvxBoxItem aSides(ATTR_BORDER); + aSides.SetLine(&aLine, SvxBoxItemLine::LEFT); + aSides.SetLine(&aLine, SvxBoxItemLine::RIGHT); + ScPatternAttr aSidesOnly(m_pDoc->getCellAttributeHelper()); + aSidesOnly.ItemSetPut(aSides); + m_pDoc->ApplyPatternAreaTab(0, 0, 0, nLast, 0, aSidesOnly); + + SvxBoxItem aTop(aSides); + aTop.SetLine(&aLine, SvxBoxItemLine::TOP); + ScPatternAttr aTopRow(m_pDoc->getCellAttributeHelper()); + aTopRow.ItemSetPut(aTop); + m_pDoc->ApplyPatternAreaTab(0, 0, 0, 0, 0, aTopRow); + + SvxBoxItem aBottom(aSides); + aBottom.SetLine(&aLine, SvxBoxItemLine::BOTTOM); + ScPatternAttr aBottomRow(m_pDoc->getCellAttributeHelper()); + aBottomRow.ItemSetPut(aBottom); + m_pDoc->ApplyPatternAreaTab(0, nLast, 0, nLast, 0, aBottomRow); + + CPPUNIT_ASSERT_EQUAL_MESSAGE("the printout stops at the last row with content", + static_cast(0), lcl_printAreaEndRow(m_pDoc)); + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(Test, testPrintAreaDoesNotMoveAsTheSheetIsFilledIn) +{ + // A form is printed in full wherever the last cell with content sits, so filling the form + // in row by row leaves the printout the same. + for (SCROW nContentRow : { SCROW(1), SCROW(13), SCROW(15), SCROW(16), SCROW(17), SCROW(50) }) + { + m_pDoc->InsertTab(0, "Form" + OUString::number(nContentRow)); + lcl_makeBorderedForm(m_pDoc, 99); + m_pDoc->SetString(0, nContentRow, 0, u"filled in"_ustr); + + OString aMessage = "a form, content in row " + OString::number(nContentRow); + CPPUNIT_ASSERT_EQUAL_MESSAGE(aMessage.getStr(), static_cast(99), + lcl_printAreaEndRow(m_pDoc)); + m_pDoc->DeleteTab(0); + } + + // A block too wide to be a form stays out of the printout while the content ends well + // above it. + for (SCROW nContentRow : { SCROW(0), SCROW(600), SCROW(3000) }) + { + m_pDoc->InsertTab(0, "Wide" + OUString::number(nContentRow)); + m_pDoc->SetString(0, 0, 0, u"Header"_ustr); + if (nContentRow > 0) + m_pDoc->SetString(0, nContentRow, 0, u"filled in"_ustr); + lcl_makeBorderedForm(m_pDoc, 10499); + + OString aMessage = "a wide block, content in row " + OString::number(nContentRow); + CPPUNIT_ASSERT_EQUAL_MESSAGE(aMessage.getStr(), nContentRow, + lcl_printAreaEndRow(m_pDoc)); + m_pDoc->DeleteTab(0); + } + + // A form too wide for the printout stays out of it wherever the content sits inside it. + // Bold over the upper part splits the form into two ranges that still look the same, and + // the lower one is short enough on its own to pass for a form. + m_pDoc->InsertTab(0, u"Split"_ustr); + lcl_makeBorderedForm(m_pDoc, 3000); + ScPatternAttr aBold(m_pDoc->getCellAttributeHelper()); + aBold.ItemSetPut(SvxWeightItem(WEIGHT_BOLD, ATTR_FONT_WEIGHT)); + m_pDoc->ApplyPatternAreaTab(0, 1, 2, 2500, 0, aBold); + m_pDoc->SetString(0, 2600, 0, u"filled in"_ustr); + + CPPUNIT_ASSERT_EQUAL_MESSAGE("a wide form, content in its lower range", + static_cast(2600), lcl_printAreaEndRow(m_pDoc)); + m_pDoc->DeleteTab(0); +} + CPPUNIT_TEST_FIXTURE(Test, testAnchoredRotatedShape) { m_pDoc->InsertTab(0, u"TestTab"_ustr); diff --git a/sc/source/core/data/attarray.cxx b/sc/source/core/data/attarray.cxx index db79f3d67a06..907d3cc2bde8 100644 --- a/sc/source/core/data/attarray.cxx +++ b/sc/source/core/data/attarray.cxx @@ -1919,6 +1919,12 @@ bool ScAttrArray::GetFirstVisibleAttr( SCROW& rFirstRow ) const const SCROW SC_VISATTR_STOP = 84; +// how far a bordered block reaches into the print area, in its whole length and in its +// distance below the last cell with content. The length counts the rows that already hold +// content, so it stays the same as the form is filled in. 2000 rows is some forty pages here. + +const SCROW SC_VISATTR_BORDER_STOP = 2000; + bool ScAttrArray::GetLastVisibleAttr( SCROW& rLastRow, SCROW nLastData, bool bSkipEmpty ) const { if ( mvData.empty() ) @@ -1928,8 +1934,8 @@ bool ScAttrArray::GetLastVisibleAttr( SCROW& rLastRow, SCROW nLastData, bool bSk } // #i30830# changed behavior: - // ignore all attributes starting with the first run of SC_VISATTR_STOP equal rows - // below the last content cell + // ignore a long run of equally formatted rows below the last content cell, and everything + // below it. A bordered run of the size and position a form has is kept instead. if ( nLastData == rDocument.MaxRow() ) { @@ -1965,7 +1971,30 @@ bool ScAttrArray::GetLastVisibleAttr( SCROW& rLastRow, SCROW nLastData, bool bSk if ( nAttrStartRow <= nLastData ) nAttrStartRow = nLastData + 1; SCROW nAttrSize = mvData[nEndPos].nEndRow + 1 - nAttrStartRow; + bool bStop = false; if ( nAttrSize >= SC_VISATTR_STOP ) + { + // The length is measured over the whole block, so it holds steady as the + // sheet is filled in. The distance keeps the search within + // SC_VISATTR_BORDER_STOP rows of the last cell with content. + const SvxBoxItem& rBox + = mvData[nEndPos].getScPatternAttr()->GetItem( ATTR_BORDER ); + const bool bBordered = rBox.GetLeft() || rBox.GetRight() + || rBox.GetTop() || rBox.GetBottom(); + // walk back to the first range of this block, so the length is the same + // whichever of its ranges holds the last cell with content + SCSIZE nStartPos = nPos; + while ( nStartPos > 0 + && mvData[nStartPos-1].getScPatternAttr()->IsVisibleEqual( + *mvData[nStartPos].getScPatternAttr() ) ) + --nStartPos; + const SCROW nRangeStartRow + = ( nStartPos > 0 ) ? ( mvData[nStartPos-1].nEndRow + 1 ) : 0; + bStop = !bBordered + || mvData[nEndPos].nEndRow + 1 - nRangeStartRow >= SC_VISATTR_BORDER_STOP + || mvData[nEndPos].nEndRow - nLastData >= SC_VISATTR_BORDER_STOP; + } + if ( bStop ) break; // while, ignore this range and below else if ( mvData[nEndPos].getScPatternAttr()->IsVisible() ) {