From 267c6f2ac71f92999e969232431ba04678e7437e Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 07:54:39 +0200 Subject: Adding upstream version 4:24.2.0. Signed-off-by: Daniel Baumann --- sw/qa/extras/tiledrendering/tiledrendering.cxx | 4048 ++++++++++++++++++++++++ 1 file changed, 4048 insertions(+) create mode 100644 sw/qa/extras/tiledrendering/tiledrendering.cxx (limited to 'sw/qa/extras/tiledrendering/tiledrendering.cxx') diff --git a/sw/qa/extras/tiledrendering/tiledrendering.cxx b/sw/qa/extras/tiledrendering/tiledrendering.cxx new file mode 100644 index 0000000000..2325ce93b4 --- /dev/null +++ b/sw/qa/extras/tiledrendering/tiledrendering.cxx @@ -0,0 +1,4048 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::ostream& operator<<(std::ostream& os, ViewShellId id) +{ + os << static_cast(id); + return os; +} + +/// Testsuite for the SwXTextDocument methods implementing the vcl::ITiledRenderable interface. +class SwTiledRenderingTest : public SwModelTestBase +{ +public: + SwTiledRenderingTest(); + virtual void setUp() override; + virtual void tearDown() override; + +protected: + SwXTextDocument* createDoc(const char* pName = nullptr); + void setupLibreOfficeKitViewCallback(SfxViewShell* pViewShell); + static void callback(int nType, const char* pPayload, void* pData); + void callbackImpl(int nType, const char* pPayload); + // First invalidation. + tools::Rectangle m_aInvalidation; + /// Union of all invalidations. + tools::Rectangle m_aInvalidations; + Size m_aDocumentSize; + OString m_aTextSelection; + bool m_bFound; + std::vector m_aSearchResultSelection; + std::vector m_aSearchResultPart; + int m_nSelectionBeforeSearchResult; + int m_nSelectionAfterSearchResult; + int m_nInvalidations; + int m_nRedlineTableSizeChanged; + int m_nRedlineTableEntryModified; + int m_nTrackedChangeIndex; + bool m_bFullInvalidateSeen; + OString m_sHyperlinkText; + OString m_sHyperlinkLink; + OString m_aFormFieldButton; + OString m_aContentControl; + OString m_ShapeSelection; + TestLokCallbackWrapper m_callbackWrapper; +}; + +SwTiledRenderingTest::SwTiledRenderingTest() + : SwModelTestBase("/sw/qa/extras/tiledrendering/data/"), + m_bFound(true), + m_nSelectionBeforeSearchResult(0), + m_nSelectionAfterSearchResult(0), + m_nInvalidations(0), + m_nRedlineTableSizeChanged(0), + m_nRedlineTableEntryModified(0), + m_nTrackedChangeIndex(-1), + m_bFullInvalidateSeen(false), + m_callbackWrapper(&callback, this) +{ +} + +void SwTiledRenderingTest::setUp() +{ + SwModelTestBase::setUp(); + + SwGlobals::ensure(); + SW_MOD()->ClearRedlineAuthors(); + + comphelper::LibreOfficeKit::setActive(true); +} + +void SwTiledRenderingTest::tearDown() +{ + if (mxComponent.is()) + { + SwXTextDocument* pTextDocument = dynamic_cast(mxComponent.get()); + if (pTextDocument) + { + SwWrtShell* pWrtShell = pTextDocument->GetDocShell()->GetWrtShell(); + if (pWrtShell) + { + pWrtShell->GetSfxViewShell()->setLibreOfficeKitViewCallback(nullptr); + } + } + mxComponent->dispose(); + mxComponent.clear(); + } + m_callbackWrapper.clear(); + comphelper::LibreOfficeKit::setActive(false); + + test::BootstrapFixture::tearDown(); +} + +SwXTextDocument* SwTiledRenderingTest::createDoc(const char* pName) +{ + if (!pName) + createSwDoc(); + else + createSwDoc(pName); + + SwXTextDocument* pTextDocument = dynamic_cast(mxComponent.get()); + CPPUNIT_ASSERT(pTextDocument); + pTextDocument->initializeForTiledRendering(uno::Sequence()); + return pTextDocument; +} + +void SwTiledRenderingTest::setupLibreOfficeKitViewCallback(SfxViewShell* pViewShell) +{ + pViewShell->setLibreOfficeKitViewCallback(&m_callbackWrapper); + m_callbackWrapper.setLOKViewId(SfxLokHelper::getView(pViewShell)); +} + +void SwTiledRenderingTest::callback(int nType, const char* pPayload, void* pData) +{ + static_cast(pData)->callbackImpl(nType, pPayload); +} + +void SwTiledRenderingTest::callbackImpl(int nType, const char* pPayload) +{ + OString aPayload(pPayload); + switch (nType) + { + case LOK_CALLBACK_INVALIDATE_TILES: + { + tools::Rectangle aInvalidation; + uno::Sequence aSeq = comphelper::string::convertCommaSeparated(OUString::createFromAscii(pPayload)); + if (std::string_view("EMPTY") == pPayload) + { + m_bFullInvalidateSeen = true; + return; + } + + CPPUNIT_ASSERT(aSeq.getLength() == 4 || aSeq.getLength() == 5); + aInvalidation.SetLeft(aSeq[0].toInt32()); + aInvalidation.SetTop(aSeq[1].toInt32()); + aInvalidation.setWidth(aSeq[2].toInt32()); + aInvalidation.setHeight(aSeq[3].toInt32()); + if (m_aInvalidation.IsEmpty()) + { + m_aInvalidation = aInvalidation; + } + m_aInvalidations.Union(aInvalidation); + ++m_nInvalidations; + } + break; + case LOK_CALLBACK_DOCUMENT_SIZE_CHANGED: + { + uno::Sequence aSeq = comphelper::string::convertCommaSeparated(OUString::createFromAscii(pPayload)); + CPPUNIT_ASSERT_EQUAL(static_cast(2), aSeq.getLength()); + m_aDocumentSize.setWidth(aSeq[0].toInt32()); + m_aDocumentSize.setHeight(aSeq[1].toInt32()); + } + break; + case LOK_CALLBACK_TEXT_SELECTION: + { + m_aTextSelection = pPayload; + if (m_aSearchResultSelection.empty()) + ++m_nSelectionBeforeSearchResult; + else + ++m_nSelectionAfterSearchResult; + } + break; + case LOK_CALLBACK_SEARCH_NOT_FOUND: + { + m_bFound = false; + } + break; + case LOK_CALLBACK_SEARCH_RESULT_SELECTION: + { + m_aSearchResultSelection.clear(); + boost::property_tree::ptree aTree; + std::stringstream aStream(pPayload); + boost::property_tree::read_json(aStream, aTree); + for (const boost::property_tree::ptree::value_type& rValue : aTree.get_child("searchResultSelection")) + { + m_aSearchResultSelection.emplace_back(rValue.second.get("rectangles").c_str()); + m_aSearchResultPart.push_back(std::atoi(rValue.second.get("part").c_str())); + } + } + break; + case LOK_CALLBACK_REDLINE_TABLE_SIZE_CHANGED: + { + ++m_nRedlineTableSizeChanged; + } + break; + case LOK_CALLBACK_REDLINE_TABLE_ENTRY_MODIFIED: + { + ++m_nRedlineTableEntryModified; + } + break; + case LOK_CALLBACK_STATE_CHANGED: + { + OString aTrackedChangeIndexPrefix(".uno:TrackedChangeIndex="_ostr); + if (aPayload.startsWith(aTrackedChangeIndexPrefix)) + { + OString sIndex = aPayload.copy(aTrackedChangeIndexPrefix.getLength()); + if (sIndex.isEmpty()) + m_nTrackedChangeIndex = -1; + else + m_nTrackedChangeIndex = sIndex.toInt32(); + } + } + break; + case LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR: + { + if (comphelper::LibreOfficeKit::isViewIdForVisCursorInvalidation()) + { + boost::property_tree::ptree aTree; + std::stringstream aStream(pPayload); + boost::property_tree::read_json(aStream, aTree); + boost::property_tree::ptree &aChild = aTree.get_child("hyperlink"); + m_sHyperlinkText = OString(aChild.get("text", "")); + m_sHyperlinkLink = OString(aChild.get("link", "")); + } + } + break; + case LOK_CALLBACK_FORM_FIELD_BUTTON: + { + m_aFormFieldButton = OString(pPayload); + } + break; + case LOK_CALLBACK_CONTENT_CONTROL: + { + m_aContentControl = OString(pPayload); + } + break; + case LOK_CALLBACK_GRAPHIC_SELECTION: + { + m_ShapeSelection = OString(pPayload); + } + break; + } + +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testRegisterCallback) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + // Insert a character at the beginning of the document. + pWrtShell->Insert("x"); + Scheduler::ProcessEventsToIdle(); + + // Check that the top left 256x256px tile would be invalidated. + CPPUNIT_ASSERT(!m_aInvalidation.IsEmpty()); + tools::Rectangle aTopLeft(0, 0, 256*15, 256*15); // 1 px = 15 twips, assuming 96 DPI. + CPPUNIT_ASSERT(m_aInvalidation.Overlaps(aTopLeft)); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPostKeyEvent) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + // Did we manage to go after the first character? + CPPUNIT_ASSERT_EQUAL(static_cast(1), pShellCursor->GetPoint()->GetContentIndex()); + + emulateTyping(*pXTextDocument, u"x"); + // Did we manage to insert the character after the first one? + CPPUNIT_ASSERT_EQUAL(OUString("Axaa bbb."), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPostMouseEvent) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + // Did we manage to go after the first character? + CPPUNIT_ASSERT_EQUAL(static_cast(1), pShellCursor->GetPoint()->GetContentIndex()); + + Point aStart = pShellCursor->GetSttPos(); + aStart.setX(aStart.getX() - 1000); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, aStart.getX(), aStart.getY(), 1, MOUSE_LEFT, 0); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONUP, aStart.getX(), aStart.getY(), 1, MOUSE_LEFT, 0); + Scheduler::ProcessEventsToIdle(); + // The new cursor position must be before the first word. + CPPUNIT_ASSERT_EQUAL(static_cast(0), pShellCursor->GetPoint()->GetContentIndex()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSetTextSelection) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + // Move the cursor into the second word. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 5, /*bBasicCall=*/false); + // Create a selection on the word. + pWrtShell->SelWrd(); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + // Did we indeed manage to select the second word? + CPPUNIT_ASSERT_EQUAL(OUString("bbb"), pShellCursor->GetText()); + + // Now use setTextSelection() to move the start of the selection 1000 twips left. + Point aStart = pShellCursor->GetSttPos(); + aStart.setX(aStart.getX() - 1000); + pXTextDocument->setTextSelection(LOK_SETTEXTSELECTION_START, aStart.getX(), aStart.getY()); + // The new selection must include the first word, too -- but not the ending dot. + CPPUNIT_ASSERT_EQUAL(OUString("Aaa bbb"), pShellCursor->GetText()); + + // Next: test that LOK_SETTEXTSELECTION_RESET + LOK_SETTEXTSELECTION_END can be used to create a selection. + pXTextDocument->setTextSelection(LOK_SETTEXTSELECTION_RESET, aStart.getX(), aStart.getY()); + pXTextDocument->setTextSelection(LOK_SETTEXTSELECTION_END, aStart.getX() + 1000, aStart.getY()); + CPPUNIT_ASSERT_EQUAL(OUString("Aaa b"), pShellCursor->GetText()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testGetTextSelection) +{ + SwXTextDocument* pXTextDocument = createDoc("shape-with-text.fodt"); + // No crash, just empty output for unexpected mime type. + CPPUNIT_ASSERT_EQUAL(OString(), apitest::helper::transferable::getTextSelection(pXTextDocument->getSelection(), "foo/bar"_ostr)); + + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + // Move the cursor into the first word. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 2, /*bBasicCall=*/false); + // Create a selection by on the word. + pWrtShell->SelWrd(); + + // Make sure that we selected text from the body text. + CPPUNIT_ASSERT_EQUAL("Hello"_ostr, apitest::helper::transferable::getTextSelection(pXTextDocument->getSelection(), "text/plain;charset=utf-8"_ostr)); + + // Make sure we produce something for HTML. + CPPUNIT_ASSERT(!apitest::helper::transferable::getTextSelection(pXTextDocument->getSelection(), "text/html"_ostr).isEmpty()); + + // Now select some shape text and check again. + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell->GetDrawView(); + pView->SdrBeginTextEdit(pObject); + CPPUNIT_ASSERT(pView->GetTextEditObject()); + EditView& rEditView = pView->GetTextEditOutlinerView()->GetEditView(); + ESelection aWordSelection(0, 0, 0, 5); + rEditView.SetSelection(aWordSelection); + CPPUNIT_ASSERT_EQUAL("Shape"_ostr, apitest::helper::transferable::getTextSelection(pXTextDocument->getSelection(), "text/plain;charset=utf-8"_ostr)); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testGetTextSelectionLineLimit) +{ + static OStringLiteral sOriginalText(u8"Estonian employs the Latin script as the basis for its alphabet, which adds the letters ä, ö, ü, and õ, plus the later additions š and ž. The letters c, q, w, x and y are limited to proper names of foreign origin, and f, z, š, and ž appear in loanwords and foreign names only. Ö and Ü are pronounced similarly to their equivalents in Swedish and German. Unlike in standard German but like Swedish (when followed by 'r') and Finnish, Ä is pronounced [æ], as in English mat. The vowels Ä, Ö and Ü are clearly separate phonemes and inherent in Estonian, although the letter shapes come from German. The letter õ denotes /ɤ/, unrounded /o/, or a close-mid back unrounded vowel. It is almost identical to the Bulgarian ъ /ɤ̞/ and the Vietnamese ơ, and is also used to transcribe the Russian ы."); + static OStringLiteral sExpectedHtml(u8"Estonian employs the Latin script as the basis for its alphabet, which adds the letters ä, ö, ü, and õ, plus the later additions š and ž. The letters c, q, w, x and y are limited to proper names of foreign origin, and f, z, š, and ž appear in loanwords and foreign names only. Ö and Ü are pronounced similarly to their equivalents in Swedish and German. Unlike in standard German but like Swedish (when followed by 'r') and Finnish, Ä is pronounced [æ], as in English mat. The vowels Ä, Ö and Ü are clearly separate phonemes and inherent in Estonian, although the letter shapes come from German. The letter õ denotes /ɤ/, unrounded /o/, or a close-mid back unrounded vowel. It is almost identical to the Bulgarian ъ /ɤ̞/ and the Vietnamese ơ, and is also used to transcribe the Russian ы."); + + SwXTextDocument* pXTextDocument = createDoc("estonian.odt"); + + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + // Move the cursor into the first word. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 2, /*bBasicCall=*/false); + // Create a selection. + pWrtShell->SelAll(); + + OString sPlainText = apitest::helper::transferable::getTextSelection(pXTextDocument->getSelection(), "text/plain;charset=utf-8"_ostr); + + CPPUNIT_ASSERT_EQUAL(OString(sOriginalText), sPlainText.trim()); + + OString sHtmlText = apitest::helper::transferable::getTextSelection(pXTextDocument->getSelection(), "text/html"_ostr); + + int nStart = sHtmlText.indexOf(u8"Estonian"); + + CPPUNIT_ASSERT(sHtmlText.match(sExpectedHtml, nStart)); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testGetTextSelectionMultiLine) +{ + // Test will check if correct number of new line marks / paragraphs is generated + static OStringLiteral sOriginalText(u8"Heading\n\ +Let's have text; we need to be able to select the text inside the shape, but also the various individual ones too:\n\ +\n\ +\n\ +\n\ +\n\ +\n\ +And this is all for Writer shape objects\n\ +Heading on second page"); + + static OStringLiteral sExpectedHtml(u8"Heading\n\ +

Let's have text; we need to be able to select the text inside the shape, but also the various individual ones too:

\n\ +



\n\ +



\n\ +



\n\ +



\n\ +



\n\ +

And this is all for Writer shape objects

\n\ +

Heading on second page

"); + + SwXTextDocument* pXTextDocument = createDoc("multiline.odt"); + + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + // Create a selection. + pWrtShell->SelAll(); + + OString sPlainText = apitest::helper::transferable::getTextSelection(pXTextDocument->getSelection(), "text/plain;charset=utf-8"_ostr); + + CPPUNIT_ASSERT_EQUAL(OString(sOriginalText), sPlainText.trim()); + + OString sHtmlText = apitest::helper::transferable::getTextSelection(pXTextDocument->getSelection(), "text/html"_ostr); + + int nStart = sHtmlText.indexOf(u8"Heading"); + + CPPUNIT_ASSERT(sHtmlText.match(sExpectedHtml, nStart)); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSetGraphicSelection) +{ + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + pWrtShell->SelectObj(Point(), 0, pObject); + SdrHdlList handleList(nullptr); + pObject->AddToHdlList(handleList); + // Make sure the rectangle has 8 handles: at each corner and at the center of each edge. + CPPUNIT_ASSERT_EQUAL(static_cast(8), handleList.GetHdlCount()); + // Take the bottom center one. + SdrHdl* pHdl = handleList.GetHdl(6); + CPPUNIT_ASSERT_EQUAL(int(SdrHdlKind::Lower), static_cast(pHdl->GetKind())); + tools::Rectangle aShapeBefore = pObject->GetSnapRect(); + // Resize. + pXTextDocument->setGraphicSelection(LOK_SETGRAPHICSELECTION_START, pHdl->GetPos().getX(), pHdl->GetPos().getY()); + pXTextDocument->setGraphicSelection(LOK_SETGRAPHICSELECTION_END, pHdl->GetPos().getX(), pHdl->GetPos().getY() + 1000); + tools::Rectangle aShapeAfter = pObject->GetSnapRect(); + // Check that a resize happened, but aspect ratio is not kept. + CPPUNIT_ASSERT_EQUAL(aShapeBefore.getOpenWidth(), aShapeAfter.getOpenWidth()); + CPPUNIT_ASSERT_EQUAL(aShapeBefore.getOpenHeight() + 1000, aShapeAfter.getOpenHeight()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testResetSelection) +{ + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + // Select one character. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/true, 1, /*bBasicCall=*/false); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + // We have a text selection. + CPPUNIT_ASSERT(pShellCursor->HasMark()); + + pXTextDocument->resetSelection(); + // We no longer have a text selection. + CPPUNIT_ASSERT(!pShellCursor->HasMark()); + + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + Point aPoint = pObject->GetSnapRect().Center(); + // Select the shape. + pWrtShell->EnterSelFrameMode(&aPoint); + // We have a graphic selection. + CPPUNIT_ASSERT(pWrtShell->IsSelFrameMode()); + + pXTextDocument->resetSelection(); + // We no longer have a graphic selection. + CPPUNIT_ASSERT(!pWrtShell->IsSelFrameMode()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testInsertShape) +{ + SwXTextDocument* pXTextDocument = createDoc("2-pages.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + + pXTextDocument->setClientVisibleArea(tools::Rectangle(0, 0, 10000, 4000)); + comphelper::dispatchCommand(".uno:BasicShapes.circle", uno::Sequence()); + + // check that the shape was inserted in the visible area, not outside + IDocumentDrawModelAccess &rDrawModelAccess = pWrtShell->GetDoc()->getIDocumentDrawModelAccess(); + SdrPage* pPage = rDrawModelAccess.GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + + CPPUNIT_ASSERT_EQUAL(tools::Rectangle(Point(3299, 299), Size(3403, 3403)), pObject->GetSnapRect()); + + // check that it is in the foreground layer + CPPUNIT_ASSERT_EQUAL(rDrawModelAccess.GetHeavenId().get(), pObject->GetLayer().get()); +} + +static void lcl_search(bool bBackward) +{ + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"SearchItem.SearchString", uno::Any(OUString("shape"))}, + {"SearchItem.Backward", uno::Any(bBackward)} + })); + comphelper::dispatchCommand(".uno:ExecuteSearch", aPropertyValues); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSearch) +{ + SwXTextDocument* pXTextDocument = createDoc("search.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + SwNodeOffset nNode = pWrtShell->getShellCursor(false)->Start()->GetNode().GetIndex(); + + // First hit, in the second paragraph, before the shape. + lcl_search(false); + CPPUNIT_ASSERT(!pWrtShell->GetDrawView()->GetTextEditObject()); + SwNodeOffset nActual = pWrtShell->getShellCursor(false)->Start()->GetNode().GetIndex(); + CPPUNIT_ASSERT_EQUAL(nNode + 1, nActual); + /// Make sure we get search result selection for normal find as well, not only find all. + CPPUNIT_ASSERT(!m_aSearchResultSelection.empty()); + + // Next hit, in the shape. + lcl_search(false); + CPPUNIT_ASSERT(pWrtShell->GetDrawView()->GetTextEditObject()); + + // Next hit, in the shape, still. + lcl_search(false); + CPPUNIT_ASSERT(pWrtShell->GetDrawView()->GetTextEditObject()); + + // Last hit, in the last paragraph, after the shape. + lcl_search(false); + CPPUNIT_ASSERT(!pWrtShell->GetDrawView()->GetTextEditObject()); + nActual = pWrtShell->getShellCursor(false)->Start()->GetNode().GetIndex(); + CPPUNIT_ASSERT_EQUAL(nNode + 7, nActual); + + // Now change direction and make sure that the first 2 hits are in the shape, but not the 3rd one. + lcl_search(true); + CPPUNIT_ASSERT(pWrtShell->GetDrawView()->GetTextEditObject()); + lcl_search(true); + CPPUNIT_ASSERT(pWrtShell->GetDrawView()->GetTextEditObject()); + lcl_search(true); + CPPUNIT_ASSERT(!pWrtShell->GetDrawView()->GetTextEditObject()); + nActual = pWrtShell->getShellCursor(false)->Start()->GetNode().GetIndex(); + CPPUNIT_ASSERT_EQUAL(nNode + 1, nActual); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSearchViewArea) +{ + SwXTextDocument* pXTextDocument = createDoc("search.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + // Go to the second page, 1-based. + pWrtShell->GotoPage(2, false); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + // Get the ~top left corner of the second page. + Point aPoint = pShellCursor->GetSttPos(); + + // Go back to the first page, search while the cursor is there, but the + // visible area is the second page. + pWrtShell->GotoPage(1, false); + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"SearchItem.SearchString", uno::Any(OUString("Heading"))}, + {"SearchItem.Backward", uno::Any(false)}, + {"SearchItem.SearchStartPointX", uno::Any(static_cast(aPoint.getX()))}, + {"SearchItem.SearchStartPointY", uno::Any(static_cast(aPoint.getY()))} + })); + comphelper::dispatchCommand(".uno:ExecuteSearch", aPropertyValues); + // This was just "Heading", i.e. SwView::SearchAndWrap() did not search from only the top of the second page. + CPPUNIT_ASSERT_EQUAL(OUString("Heading on second page"), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSearchTextFrame) +{ + SwXTextDocument* pXTextDocument = createDoc("search.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"SearchItem.SearchString", uno::Any(OUString("TextFrame"))}, + {"SearchItem.Backward", uno::Any(false)}, + })); + comphelper::dispatchCommand(".uno:ExecuteSearch", aPropertyValues); + // This was empty: nothing was highlighted after searching for 'TextFrame'. + CPPUNIT_ASSERT(!m_aTextSelection.isEmpty()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSearchTextFrameWrapAround) +{ + SwXTextDocument* pXTextDocument = createDoc("search.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"SearchItem.SearchString", uno::Any(OUString("TextFrame"))}, + {"SearchItem.Backward", uno::Any(false)}, + })); + comphelper::dispatchCommand(".uno:ExecuteSearch", aPropertyValues); + CPPUNIT_ASSERT(m_bFound); + comphelper::dispatchCommand(".uno:ExecuteSearch", aPropertyValues); + // This failed, i.e. the second time 'not found' was reported, instead of wrapping around. + CPPUNIT_ASSERT(m_bFound); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDocumentSizeChanged) +{ + // Get the current document size. + SwXTextDocument* pXTextDocument = createDoc("2-pages.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + Size aSize = pXTextDocument->getDocumentSize(); + + // Delete the second page and see how the size changes. + pWrtShell->Down(false); + pWrtShell->DelLeft(); + // Document width should not change, this was 0. + CPPUNIT_ASSERT_EQUAL(aSize.getWidth(), m_aDocumentSize.getWidth()); + // Document height should be smaller now. + CPPUNIT_ASSERT(aSize.getHeight() > m_aDocumentSize.getHeight()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSearchAll) +{ + SwXTextDocument* pXTextDocument = createDoc("search.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"SearchItem.SearchString", uno::Any(OUString("shape"))}, + {"SearchItem.Backward", uno::Any(false)}, + {"SearchItem.Command", uno::Any(static_cast(SvxSearchCmd::FIND_ALL))}, + })); + comphelper::dispatchCommand(".uno:ExecuteSearch", aPropertyValues); + // This was 0; should be 2 results in the body text. + CPPUNIT_ASSERT_EQUAL(static_cast(2), m_aSearchResultSelection.size()); + // Writer documents are always a single part. + CPPUNIT_ASSERT_EQUAL(0, m_aSearchResultPart[0]); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSearchAllNotifications) +{ + SwXTextDocument* pXTextDocument = createDoc("search.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + // Reset notification counter before search. + m_nSelectionBeforeSearchResult = 0; + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"SearchItem.SearchString", uno::Any(OUString("shape"))}, + {"SearchItem.Backward", uno::Any(false)}, + {"SearchItem.Command", uno::Any(static_cast(SvxSearchCmd::FIND_ALL))}, + })); + comphelper::dispatchCommand(".uno:ExecuteSearch", aPropertyValues); + Scheduler::ProcessEventsToIdle(); + + // This was 5, make sure that we get no notifications about selection changes during search. + CPPUNIT_ASSERT_EQUAL(0, m_nSelectionBeforeSearchResult); + // But we do get the selection afterwards. + CPPUNIT_ASSERT(m_nSelectionAfterSearchResult > 0); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPageDownInvalidation) +{ + SwXTextDocument* pXTextDocument = createDoc("pagedown-invalidation.odt"); + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {".uno:HideWhitespace", uno::Any(true)}, + })); + pXTextDocument->initializeForTiledRendering(aPropertyValues); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + comphelper::dispatchCommand(".uno:PageDown", uno::Sequence()); + + // This was 2. + CPPUNIT_ASSERT_EQUAL(0, m_nInvalidations); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPartHash) +{ + SwXTextDocument* pXTextDocument = createDoc("pagedown-invalidation.odt"); + int nParts = pXTextDocument->getParts(); + for (int it = 0; it < nParts; it++) + { + CPPUNIT_ASSERT(!pXTextDocument->getPartHash(it).isEmpty()); + } +} + +namespace { + + /// A view callback tracks callbacks invoked on one specific view. + class ViewCallback final + { + SfxViewShell* mpViewShell; + int mnView; + public: + bool m_bOwnCursorInvalidated; + int m_nOwnCursorInvalidatedBy; + bool m_bOwnCursorAtOrigin; + tools::Rectangle m_aOwnCursor; + bool m_bViewCursorInvalidated; + tools::Rectangle m_aViewCursor; + bool m_bOwnSelectionSet; + bool m_bViewSelectionSet; + OString m_aViewSelection; + bool m_bTilesInvalidated; + bool m_bViewCursorVisible; + bool m_bGraphicViewSelection; + bool m_bGraphicSelection; + bool m_bViewLock; + /// Set if any callback was invoked. + bool m_bCalled; + /// Redline table size changed payload + boost::property_tree::ptree m_aRedlineTableChanged; + /// Redline table modified payload + boost::property_tree::ptree m_aRedlineTableModified; + /// Post-it / annotation payload. + boost::property_tree::ptree m_aComment; + TestLokCallbackWrapper m_callbackWrapper; + + ViewCallback(SfxViewShell* pViewShell = nullptr, std::function const & rBeforeInstallFunc = {}) + : m_bOwnCursorInvalidated(false), + m_nOwnCursorInvalidatedBy(-1), + m_bOwnCursorAtOrigin(false), + m_bViewCursorInvalidated(false), + m_bOwnSelectionSet(false), + m_bViewSelectionSet(false), + m_bTilesInvalidated(false), + m_bViewCursorVisible(false), + m_bGraphicViewSelection(false), + m_bGraphicSelection(false), + m_bViewLock(false), + m_bCalled(false), + m_callbackWrapper(&callback, this) + { + // Because one call-site wants to set the bool fields up before the callback is installed + if (rBeforeInstallFunc) + rBeforeInstallFunc(*this); + + mpViewShell = pViewShell ? pViewShell : SfxViewShell::Current(); + mpViewShell->setLibreOfficeKitViewCallback(&m_callbackWrapper); + mnView = SfxLokHelper::getView(); + m_callbackWrapper.setLOKViewId( mnView ); + } + + ~ViewCallback() + { + SfxLokHelper::setView(mnView); + mpViewShell->setLibreOfficeKitViewCallback(nullptr); + } + + static void callback(int nType, const char* pPayload, void* pData) + { + static_cast(pData)->callbackImpl(nType, pPayload); + } + + void callbackImpl(int nType, const char* pPayload) + { + OString aPayload(pPayload); + m_bCalled = true; + switch (nType) + { + case LOK_CALLBACK_INVALIDATE_TILES: + { + m_bTilesInvalidated = true; + } + break; + case LOK_CALLBACK_INVALIDATE_VISIBLE_CURSOR: + { + m_bOwnCursorInvalidated = true; + + OString sRect; + if(comphelper::LibreOfficeKit::isViewIdForVisCursorInvalidation()) + { + std::stringstream aStream(pPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + sRect = OString(aTree.get_child("rectangle").get_value()); + m_nOwnCursorInvalidatedBy = aTree.get_child("viewId").get_value(); + } + else + sRect = aPayload; + uno::Sequence aSeq = comphelper::string::convertCommaSeparated(OUString::fromUtf8(sRect)); + if (std::string_view("EMPTY") == pPayload) + return; + CPPUNIT_ASSERT_EQUAL(static_cast(4), aSeq.getLength()); + m_aOwnCursor.SetLeft(aSeq[0].toInt32()); + m_aOwnCursor.SetTop(aSeq[1].toInt32()); + m_aOwnCursor.setWidth(aSeq[2].toInt32()); + m_aOwnCursor.setHeight(aSeq[3].toInt32()); + if (m_aOwnCursor.Left() == 0 && m_aOwnCursor.Top() == 0) + m_bOwnCursorAtOrigin = true; + } + break; + case LOK_CALLBACK_INVALIDATE_VIEW_CURSOR: + { + m_bViewCursorInvalidated = true; + std::stringstream aStream(pPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + OString aRect( aTree.get_child("rectangle").get_value() ); + + uno::Sequence aSeq = comphelper::string::convertCommaSeparated(OUString::fromUtf8(aRect)); + if (std::string_view("EMPTY") == pPayload) + return; + CPPUNIT_ASSERT_EQUAL(static_cast(4), aSeq.getLength()); + m_aViewCursor.SetLeft(aSeq[0].toInt32()); + m_aViewCursor.SetTop(aSeq[1].toInt32()); + m_aViewCursor.setWidth(aSeq[2].toInt32()); + m_aViewCursor.setHeight(aSeq[3].toInt32()); + } + break; + case LOK_CALLBACK_TEXT_SELECTION: + { + m_bOwnSelectionSet = true; + } + break; + case LOK_CALLBACK_TEXT_VIEW_SELECTION: + { + m_bViewSelectionSet = true; + m_aViewSelection = aPayload; + } + break; + case LOK_CALLBACK_VIEW_CURSOR_VISIBLE: + { + std::stringstream aStream(pPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + m_bViewCursorVisible = aTree.get_child("visible").get_value() == "true"; + } + break; + case LOK_CALLBACK_GRAPHIC_VIEW_SELECTION: + { + std::stringstream aStream(pPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + m_bGraphicViewSelection = aTree.get_child("selection").get_value() != "EMPTY"; + } + break; + case LOK_CALLBACK_GRAPHIC_SELECTION: + { + m_bGraphicSelection = aPayload != "EMPTY"; + } + break; + case LOK_CALLBACK_VIEW_LOCK: + { + std::stringstream aStream(pPayload); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + m_bViewLock = aTree.get_child("rectangle").get_value() != "EMPTY"; + } + break; + case LOK_CALLBACK_REDLINE_TABLE_SIZE_CHANGED: + { + m_aRedlineTableChanged.clear(); + std::stringstream aStream(pPayload); + boost::property_tree::read_json(aStream, m_aRedlineTableChanged); + m_aRedlineTableChanged = m_aRedlineTableChanged.get_child("redline"); + } + break; + case LOK_CALLBACK_REDLINE_TABLE_ENTRY_MODIFIED: + { + m_aRedlineTableModified.clear(); + std::stringstream aStream(pPayload); + boost::property_tree::read_json(aStream, m_aRedlineTableModified); + m_aRedlineTableModified = m_aRedlineTableModified.get_child("redline"); + } + break; + case LOK_CALLBACK_COMMENT: + { + m_aComment.clear(); + std::stringstream aStream(pPayload); + boost::property_tree::read_json(aStream, m_aComment); + m_aComment = m_aComment.get_child("comment"); + } + break; + } + } + }; + + class TestResultListener : public cppu::WeakImplHelper + { + public: + sal_uInt32 m_nDocRepair; + + TestResultListener() : m_nDocRepair(0) + { + } + + virtual void SAL_CALL dispatchFinished(const css::frame::DispatchResultEvent& rEvent) override + { + if (rEvent.State == frame::DispatchResultState::SUCCESS) + { + rEvent.Result >>= m_nDocRepair; + } + } + + virtual void SAL_CALL disposing(const css::lang::EventObject&) override + { + } + }; + +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testMissingInvalidation) +{ + // Create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + ViewCallback aView1; + int nView1 = SfxLokHelper::getView(); + SfxLokHelper::createView(); + ViewCallback aView2; + int nView2 = SfxLokHelper::getView(); + + // First view: put the cursor into the first word. + SfxLokHelper::setView(nView1); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + + // Second view: select the first word. + SfxLokHelper::setView(nView2); + CPPUNIT_ASSERT(pXTextDocument->GetDocShell()->GetWrtShell() != pWrtShell); + pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + pWrtShell->SelWrd(); + + // Now delete the selected word and make sure both views are invalidated. + Scheduler::ProcessEventsToIdle(); + aView1.m_bTilesInvalidated = false; + aView2.m_bTilesInvalidated = false; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, awt::Key::DELETE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, awt::Key::DELETE); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT(aView1.m_bTilesInvalidated); + CPPUNIT_ASSERT(aView2.m_bTilesInvalidated); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testViewCursors) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + ViewCallback aView1; + SfxLokHelper::createView(); + ViewCallback aView2; + + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT(aView1.m_bOwnCursorInvalidated); + CPPUNIT_ASSERT(aView1.m_bViewCursorInvalidated); + CPPUNIT_ASSERT(aView2.m_bOwnCursorInvalidated); + // This failed: the cursor position of view1 was only known to view2 once + // it changed. + CPPUNIT_ASSERT(aView2.m_bViewCursorInvalidated); + + // Make sure that aView1 gets a view-only selection notification, while + // aView2 gets a real selection notification. + aView1.m_bOwnSelectionSet = false; + aView1.m_bViewSelectionSet = false; + aView2.m_bOwnSelectionSet = false; + aView2.m_bViewSelectionSet = false; + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + // Move the cursor into the second word. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 5, /*bBasicCall=*/false); + // Create a selection on the word. + pWrtShell->SelWrd(); + Scheduler::ProcessEventsToIdle(); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + // Did we indeed manage to select the second word? + CPPUNIT_ASSERT_EQUAL(OUString("bbb"), pShellCursor->GetText()); + CPPUNIT_ASSERT(!aView1.m_bOwnSelectionSet); + // This failed, aView1 did not get notification about selection changes in + // aView2. + CPPUNIT_ASSERT(aView1.m_bViewSelectionSet); + CPPUNIT_ASSERT(aView2.m_bOwnSelectionSet); + CPPUNIT_ASSERT(!aView2.m_bViewSelectionSet); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testShapeViewCursors) +{ + // Load a document and create a view, so we have 2 ones. + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + ViewCallback aView1; + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + ViewCallback aView2; + SwWrtShell* pWrtShell2 = pXTextDocument->GetDocShell()->GetWrtShell(); + + // Start shape text in the second view. + SdrPage* pPage = pWrtShell2->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell2->GetDrawView(); + pWrtShell2->GetView().BeginTextEdit(pObject, pView->GetSdrPageView(), pWrtShell2->GetWin()); + emulateTyping(*pXTextDocument, u"x"); + // Press a key in the second view, while the first one observes this. + aView1.m_bViewCursorInvalidated = false; + aView2.m_bOwnCursorInvalidated = false; + const tools::Rectangle aLastOwnCursor1 = aView1.m_aOwnCursor; + const tools::Rectangle aLastViewCursor1 = aView1.m_aViewCursor; + const tools::Rectangle aLastOwnCursor2 = aView2.m_aOwnCursor; + const tools::Rectangle aLastViewCursor2 = aView2.m_aViewCursor; + + emulateTyping(*pXTextDocument, u"y"); + // Make sure that aView1 gets a view-only cursor notification, while + // aView2 gets a real cursor notification. + CPPUNIT_ASSERT_EQUAL(aView1.m_aOwnCursor, aLastOwnCursor1); + CPPUNIT_ASSERT(aView1.m_bViewCursorInvalidated); + CPPUNIT_ASSERT(aLastViewCursor1 != aView1.m_aViewCursor); + CPPUNIT_ASSERT(aView2.m_bOwnCursorInvalidated); + CPPUNIT_ASSERT(aLastOwnCursor2 != aView2.m_aOwnCursor); + CPPUNIT_ASSERT_EQUAL(aLastViewCursor2, aView2.m_aViewCursor); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testViewCursorVisibility) +{ + // Load a document that has a shape and create two views. + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + ViewCallback aView1; + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + ViewCallback aView2; + // This failed, initially the view cursor in the second view wasn't visible. + CPPUNIT_ASSERT(aView2.m_bViewCursorVisible); + + // Click on the shape in the second view. + aView1.m_bViewCursorVisible = true; + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + Point aCenter = pObject->GetSnapRect().Center(); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, aCenter.getX(), aCenter.getY(), 1, MOUSE_LEFT, 0); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONUP, aCenter.getX(), aCenter.getY(), 1, MOUSE_LEFT, 0); + Scheduler::ProcessEventsToIdle(); + // Make sure the "view/text" cursor of the first view gets a notification. + CPPUNIT_ASSERT(!aView1.m_bViewCursorVisible); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testViewCursorCleanup) +{ + // Load a document that has a shape and create two views. + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + ViewCallback aView1; + int nView2 = SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + { + ViewCallback aView2; + + // Click on the shape in the second view. + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + Point aCenter = pObject->GetSnapRect().Center(); + aView1.m_bGraphicViewSelection = false; + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, aCenter.getX(), aCenter.getY(), 1, MOUSE_LEFT, 0); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONUP, aCenter.getX(), aCenter.getY(), 1, MOUSE_LEFT, 0); + Scheduler::ProcessEventsToIdle(); + // Make sure there is a graphic view selection on the first view. + CPPUNIT_ASSERT(aView1.m_bGraphicViewSelection); + } + // Now destroy the second view. + SfxLokHelper::destroyView(nView2); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(static_cast(1), SfxLokHelper::getViewsCount(0)); + // Make sure that the graphic view selection on the first view is cleaned up. + CPPUNIT_ASSERT(!aView1.m_bGraphicViewSelection); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testViewLock) +{ + // Load a document that has a shape and create two views. + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + ViewCallback aView1; + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + ViewCallback aView2; + + // Begin text edit in the second view and assert that the first gets a lock + // notification. + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell->GetDrawView(); + aView1.m_bViewLock = false; + pWrtShell->GetView().BeginTextEdit(pObject, pView->GetSdrPageView(), pWrtShell->GetWin()); + CPPUNIT_ASSERT(aView1.m_bViewLock); + + // End text edit in the second view, and assert that the lock is removed in + // the first view. + pWrtShell->EndTextEdit(); + CPPUNIT_ASSERT(!aView1.m_bViewLock); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testTextEditViewInvalidations) +{ + // Load a document that has a shape and create two views. + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + ViewCallback aView1; + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + ViewCallback aView2; + + // Begin text edit in the second view. + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell->GetDrawView(); + pWrtShell->GetView().BeginTextEdit(pObject, pView->GetSdrPageView(), pWrtShell->GetWin()); + emulateTyping(*pXTextDocument, u"x"); + + // Assert that both views are invalidated when pressing a key while in text edit. + aView1.m_bTilesInvalidated = false; + emulateTyping(*pXTextDocument, u"y"); + + CPPUNIT_ASSERT(aView1.m_bTilesInvalidated); + + pWrtShell->EndTextEdit(); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoInvalidations) +{ + // Load a document and create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + ViewCallback aView1; + int nView1 = SfxLokHelper::getView(); + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + ViewCallback aView2; + SfxLokHelper::setView(nView1); + + // Insert a character the end of the document. + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->EndOfSection(); + emulateTyping(*pXTextDocument, u"c"); + // ProcessEventsToIdle resets the view; set it again + SfxLokHelper::setView(nView1); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + CPPUNIT_ASSERT_EQUAL(OUString("Aaa bbb.c"), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); + + // Undo and assert that both views are invalidated. + Scheduler::ProcessEventsToIdle(); + aView1.m_bTilesInvalidated = false; + aView2.m_bTilesInvalidated = false; + comphelper::dispatchCommand(".uno:Undo", {}); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT(aView1.m_bTilesInvalidated); + // Undo was dispatched on the first view, this second view was not invalidated. + CPPUNIT_ASSERT(aView2.m_bTilesInvalidated); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoLimiting) +{ + // Load a document and create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell1 = pXTextDocument->GetDocShell()->GetWrtShell(); + int nView1 = SfxLokHelper::getView(); + int nView2 = SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + + // Insert a character the end of the document in the second view. + SwWrtShell* pWrtShell2 = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell2->EndOfSection(); + emulateTyping(*pXTextDocument, u"c"); + SwShellCursor* pShellCursor = pWrtShell2->getShellCursor(false); + CPPUNIT_ASSERT_EQUAL(OUString("Aaa bbb.c"), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); + + // Assert that the first view can't undo, but the second view can. + CPPUNIT_ASSERT(!pWrtShell1->GetLastUndoInfo(nullptr, nullptr, &pWrtShell1->GetView())); + CPPUNIT_ASSERT(pWrtShell2->GetLastUndoInfo(nullptr, nullptr, &pWrtShell2->GetView())); + + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoReordering) +{ + // Create two views and a document of 2 paragraphs. + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell1 = pXTextDocument->GetDocShell()->GetWrtShell(); + int nView1 = SfxLokHelper::getView(); + int nView2 = SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + SwWrtShell* pWrtShell2 = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell2->SplitNode(); + SfxLokHelper::setView(nView1); + pWrtShell1->SttEndDoc(/*bStt=*/true); + SwTextNode* pTextNode1 = pWrtShell1->GetCursor()->GetPointNode().GetTextNode(); + // View 1 types into the first paragraph. + emulateTyping(*pXTextDocument, u"a"); + SfxLokHelper::setView(nView2); + pWrtShell2->SttEndDoc(/*bStt=*/false); + SwTextNode* pTextNode2 = pWrtShell2->GetCursor()->GetPointNode().GetTextNode(); + // View 2 types into the second paragraph. + emulateTyping(*pXTextDocument, u"z"); + CPPUNIT_ASSERT_EQUAL(OUString("a"), pTextNode1->GetText()); + CPPUNIT_ASSERT_EQUAL(OUString("z"), pTextNode2->GetText()); + + // When view 1 presses undo: + SfxLokHelper::setView(nView1); + dispatchCommand(mxComponent, ".uno:Undo", {}); + + // Then make sure view 1's last undo action is invoked, out of order: + // Without the accompanying fix in place, this test would have failed with: + // - Expression: pTextNode1->GetText().isEmpty() + // i.e. the "a" in the first paragraph was not removed. + CPPUNIT_ASSERT(pTextNode1->GetText().isEmpty()); + // Last undo action is not invoked, as it belongs to view 2. + CPPUNIT_ASSERT_EQUAL(OUString("z"), pTextNode2->GetText()); + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoReorderingRedo) +{ + // Create two views and a document of 2 paragraphs. + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell1 = pXTextDocument->GetDocShell()->GetWrtShell(); + int nView1 = SfxLokHelper::getView(); + int nView2 = SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + SwWrtShell* pWrtShell2 = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell2->SplitNode(); + SfxLokHelper::setView(nView1); + pWrtShell1->SttEndDoc(/*bStt=*/true); + SwTextNode* pTextNode1 = pWrtShell1->GetCursor()->GetPointNode().GetTextNode(); + // View 1 types into the first paragraph, twice. + emulateTyping(*pXTextDocument, u"f"); + // Go to the start of the paragraph, to avoid grouping. + pWrtShell1->SttEndDoc(/*bStt=*/true); + emulateTyping(*pXTextDocument, u"s"); + SfxLokHelper::setView(nView2); + pWrtShell2->SttEndDoc(/*bStt=*/false); + SwTextNode* pTextNode2 = pWrtShell2->GetCursor()->GetPointNode().GetTextNode(); + // View 2 types into the second paragraph. + emulateTyping(*pXTextDocument, u"z"); + CPPUNIT_ASSERT_EQUAL(OUString("sf"), pTextNode1->GetText()); + CPPUNIT_ASSERT_EQUAL(OUString("z"), pTextNode2->GetText()); + + // When view 1 presses undo, twice: + SfxLokHelper::setView(nView1); + dispatchCommand(mxComponent, ".uno:Undo", {}); + // First just s(econd) is erased: + CPPUNIT_ASSERT_EQUAL(OUString("f"), pTextNode1->GetText()); + dispatchCommand(mxComponent, ".uno:Undo", {}); + + // Then make sure view 1's undo actions are invoked, out of order: + // Without the accompanying fix in place, this test would have failed with: + // - Expression: pTextNode1->GetText().isEmpty() + // i.e. out of order undo was executed only once, not twice. + CPPUNIT_ASSERT(pTextNode1->GetText().isEmpty()); + // The top undo action is not invoked, as it belongs to view 2. + CPPUNIT_ASSERT_EQUAL(OUString("z"), pTextNode2->GetText()); + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoReorderingRedo2) +{ + // Create two views. + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell1 = pXTextDocument->GetDocShell()->GetWrtShell(); + int nView1 = SfxLokHelper::getView(); + int nView2 = SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + SwWrtShell* pWrtShell2 = pXTextDocument->GetDocShell()->GetWrtShell(); + + // Type in the first view. + SfxLokHelper::setView(nView1); + pWrtShell1->SttEndDoc(/*bStt=*/true); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 'f', 0); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 'f', 0); + Scheduler::ProcessEventsToIdle(); + + // Type to the same paragraph in the second view. + SfxLokHelper::setView(nView2); + pWrtShell2->SttEndDoc(/*bStt=*/true); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 's', 0); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 's', 0); + Scheduler::ProcessEventsToIdle(); + + // Delete in the first view and undo. + SfxLokHelper::setView(nView1); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, awt::Key::BACKSPACE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, awt::Key::BACKSPACE); + Scheduler::ProcessEventsToIdle(); + dispatchCommand(mxComponent, ".uno:Undo", {}); + Scheduler::ProcessEventsToIdle(); + + // Query the undo state, now that a "delete" is on the redo stack and an "insert" belongs to the + // view on the undo stack, so the types are different. + SwUndoId nUndoId(SwUndoId::EMPTY); + // Without the accompanying fix in place, this test would have failed with: + // runtime error: downcast which does not point to an object of type 'const SwUndoInsert' + // note: object is of type 'SwUndoDelete' + // in an UBSan build. + pWrtShell1->GetLastUndoInfo(nullptr, &nUndoId, &pWrtShell1->GetView()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoReorderingMulti) +{ + // Create two views and a document of 2 paragraphs. + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell1 = pXTextDocument->GetDocShell()->GetWrtShell(); + int nView1 = SfxLokHelper::getView(); + int nView2 = SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + SwWrtShell* pWrtShell2 = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell2->SplitNode(); + SfxLokHelper::setView(nView1); + pWrtShell1->SttEndDoc(/*bStt=*/true); + SwTextNode* pTextNode1 = pWrtShell1->GetCursor()->GetPointNode().GetTextNode(); + // View 1 types into the first paragraph. + emulateTyping(*pXTextDocument, u"a"); + SfxLokHelper::setView(nView2); + pWrtShell2->SttEndDoc(/*bStt=*/false); + SwTextNode* pTextNode2 = pWrtShell2->GetCursor()->GetPointNode().GetTextNode(); + // View 2 types into the second paragraph, twice. + emulateTyping(*pXTextDocument, u"x"); + // Go to the start of the paragraph, to avoid grouping. + pWrtShell2->SttPara(); + emulateTyping(*pXTextDocument, u"y"); + CPPUNIT_ASSERT_EQUAL(OUString("a"), pTextNode1->GetText()); + CPPUNIT_ASSERT_EQUAL(OUString("yx"), pTextNode2->GetText()); + + // When view 1 presses undo: + SfxLokHelper::setView(nView1); + dispatchCommand(mxComponent, ".uno:Undo", {}); + + // Then make sure view 1's undo action is invoked, out of order: + // Without the accompanying fix in place, this test would have failed with: + // - Expression: pTextNode1->GetText().isEmpty() + // i.e. out of order undo was not executed, the first paragraph was still "a". + CPPUNIT_ASSERT(pTextNode1->GetText().isEmpty()); + // The top 2 undo actions are not invoked, as they belong to view 2. + CPPUNIT_ASSERT_EQUAL(OUString("yx"), pTextNode2->GetText()); + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoShapeLimiting) +{ + // Load a document and create a view. + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + SwWrtShell* pWrtShell1 = pXTextDocument->GetDocShell()->GetWrtShell(); + int nView1 = SfxLokHelper::getView(); + int nView2 = SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + SwWrtShell* pWrtShell2 = pXTextDocument->GetDocShell()->GetWrtShell(); + + // Start shape text in the second view. + SdrPage* pPage = pWrtShell2->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell2->GetDrawView(); + pWrtShell2->GetView().BeginTextEdit(pObject, pView->GetSdrPageView(), pWrtShell2->GetWin()); + emulateTyping(*pXTextDocument, u"x"); + pWrtShell2->EndTextEdit(); + + // Assert that the first view can't and the second view can undo the insertion. + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + sw::UndoManager& rUndoManager = pDoc->GetUndoManager(); + rUndoManager.SetView(&pWrtShell1->GetView()); + // This was 1: first view could undo the change of the second view. + CPPUNIT_ASSERT_EQUAL(static_cast(0), rUndoManager.GetUndoActionCount()); + rUndoManager.SetView(&pWrtShell2->GetView()); + CPPUNIT_ASSERT_EQUAL(static_cast(1), rUndoManager.GetUndoActionCount()); + + rUndoManager.SetView(nullptr); + + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoDispatch) +{ + // Load a document and create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + int nView1 = SfxLokHelper::getView(); + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + int nView2 = SfxLokHelper::getView(); + + // Insert a character in the first view. + SfxLokHelper::setView(nView1); + emulateTyping(*pXTextDocument, u"c"); + + // Click before the first word in the second view. + SfxLokHelper::setView(nView2); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + Point aStart = pShellCursor->GetSttPos(); + aStart.setX(aStart.getX() - 1000); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, aStart.getX(), aStart.getY(), 1, MOUSE_LEFT, 0); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONUP, aStart.getX(), aStart.getY(), 1, MOUSE_LEFT, 0); + Scheduler::ProcessEventsToIdle(); + uno::Reference xDesktop = frame::Desktop::create(comphelper::getProcessComponentContext()); + uno::Reference xFrame2 = xDesktop->getActiveFrame(); + + // Now switch back to the first view, and make sure that the active frame is updated. + SfxLokHelper::setView(nView1); + uno::Reference xFrame1 = xDesktop->getActiveFrame(); + // This failed: setView() did not update the active frame. + CPPUNIT_ASSERT(xFrame1 != xFrame2); + + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoRepairDispatch) +{ + // Load a document and create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + int nView1 = SfxLokHelper::getView(); + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + int nView2 = SfxLokHelper::getView(); + + // Insert a character in the first view. + SfxLokHelper::setView(nView1); + emulateTyping(*pXTextDocument, u"c"); + + // Assert that by default the second view can't undo the action. + SfxLokHelper::setView(nView2); + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + sw::UndoManager& rUndoManager = pDoc->GetUndoManager(); + CPPUNIT_ASSERT_EQUAL(static_cast(1), rUndoManager.GetUndoActionCount()); + comphelper::dispatchCommand(".uno:Undo", {}); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(static_cast(1), rUndoManager.GetUndoActionCount()); + + // But the same is allowed in repair mode. + SfxLokHelper::setView(nView2); + CPPUNIT_ASSERT_EQUAL(static_cast(1), rUndoManager.GetUndoActionCount()); + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"Repair", uno::Any(true)} + })); + comphelper::dispatchCommand(".uno:Undo", aPropertyValues); + Scheduler::ProcessEventsToIdle(); + // This was 1: repair mode couldn't undo the action, either. + CPPUNIT_ASSERT_EQUAL(static_cast(0), rUndoManager.GetUndoActionCount()); + + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testShapeTextUndoShells) +{ + // Load a document and create a view. + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + sal_Int32 nView1 = SfxLokHelper::getView(); + + // Begin text edit. + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell->GetDrawView(); + pWrtShell->GetView().BeginTextEdit(pObject, pView->GetSdrPageView(), pWrtShell->GetWin()); + emulateTyping(*pXTextDocument, u"x"); + pWrtShell->EndTextEdit(); + + // Make sure that the undo item remembers who created it. + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + sw::UndoManager& rUndoManager = pDoc->GetUndoManager(); + CPPUNIT_ASSERT_EQUAL(size_t(1), rUndoManager.GetUndoActionCount()); + CPPUNIT_ASSERT_EQUAL(OUString("Edit text of Shape 'Shape1'"), rUndoManager.GetUndoActionComment(0)); + + // This was -1: the view shell id for the undo action wasn't known. + CPPUNIT_ASSERT_EQUAL(ViewShellId(nView1), rUndoManager.GetUndoAction()->GetViewShellId()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testShapeTextUndoGroupShells) +{ + // Load a document and create a view. + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + ViewCallback aView1; + sal_Int32 nView1 = SfxLokHelper::getView(); + + // Begin text edit. + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell->GetDrawView(); + pWrtShell->GetView().BeginTextEdit(pObject, pView->GetSdrPageView(), pWrtShell->GetWin()); + emulateTyping(*pXTextDocument, u"x"); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, awt::Key::BACKSPACE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, awt::Key::BACKSPACE); + Scheduler::ProcessEventsToIdle(); + + // Make sure that the undo item remembers who created it. + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + sw::UndoManager& rUndoManager = pDoc->GetUndoManager(); + CPPUNIT_ASSERT_EQUAL(size_t(0), rUndoManager.GetUndoActionCount()); + + pWrtShell->EndTextEdit(); + pWrtShell->GetView().BeginTextEdit(pObject, pView->GetSdrPageView(), pWrtShell->GetWin()); + + CPPUNIT_ASSERT_EQUAL(size_t(1), rUndoManager.GetUndoActionCount()); + CPPUNIT_ASSERT_EQUAL(OUString("Edit text of Shape 'Shape1'"), rUndoManager.GetUndoActionComment(0)); + + // This was -1: the view shell id for the (top) undo list action wasn't known. + CPPUNIT_ASSERT_EQUAL(ViewShellId(nView1), rUndoManager.GetUndoAction()->GetViewShellId()); + + // Create an editeng text selection in the first view. + EditView& rEditView = pView->GetTextEditOutlinerView()->GetEditView(); + emulateTyping(*pXTextDocument, u"x"); + // 0th para, 0th char -> 0th para, 1st char. + ESelection aWordSelection(0, 0, 0, 1); + rEditView.SetSelection(aWordSelection); + + // Create a second view, and make sure that the new view sees the same + // cursor position as the old one. + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering({}); + ViewCallback aView2; + // Difference was 935 twips, the new view didn't see the editeng cursor of + // the old one. The new difference should be <1px, but here we deal with twips. + CPPUNIT_ASSERT(std::abs(aView1.m_aOwnCursor.Top() - aView2.m_aViewCursor.Top()) < 10); + // This was false, editeng text selection of the first view wasn't noticed + // by the second view. + CPPUNIT_ASSERT(aView2.m_bViewSelectionSet); + // This was false, the new view wasn't aware of the shape text lock created + // by the old view. + CPPUNIT_ASSERT(aView2.m_bViewLock); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testTrackChanges) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + + // Turn on track changes, type "zzz" at the end, and move to the start. + uno::Reference xPropertySet(mxComponent, uno::UNO_QUERY); + xPropertySet->setPropertyValue("RecordChanges", uno::Any(true)); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + ViewCallback aView(pWrtShell->GetSfxViewShell()); + pWrtShell->EndOfSection(); + pWrtShell->Insert("zzz"); + pWrtShell->StartOfSection(); + + // Get the redline just created + const SwRedlineTable& rTable = pWrtShell->GetDoc()->getIDocumentRedlineAccess().GetRedlineTable(); + CPPUNIT_ASSERT_EQUAL(static_cast(1), rTable.size()); + SwRangeRedline* pRedline = rTable[0]; + + // Reject the change by id, while the cursor does not cover the tracked change. + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"RejectTrackedChange", uno::Any(o3tl::narrowing(pRedline->GetId()))} + })); + comphelper::dispatchCommand(".uno:RejectTrackedChange", aPropertyValues); + Scheduler::ProcessEventsToIdle(); + + // Assert that the reject was performed. + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + // This was 'Aaa bbb.zzz', the change wasn't rejected. + CPPUNIT_ASSERT_EQUAL(OUString("Aaa bbb."), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testTrackChangesCallback) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // Turn on track changes and type "x". + uno::Reference xPropertySet(mxComponent, uno::UNO_QUERY); + xPropertySet->setPropertyValue("RecordChanges", uno::Any(true)); + m_nRedlineTableSizeChanged = 0; + pWrtShell->Insert("x"); + + // Assert that we get exactly one notification about the redline insert. + // This was 0, as LOK_CALLBACK_REDLINE_TABLE_SIZE_CHANGED wasn't sent. + CPPUNIT_ASSERT_EQUAL(1, m_nRedlineTableSizeChanged); + + CPPUNIT_ASSERT_EQUAL(-1, m_nTrackedChangeIndex); + pWrtShell->Left(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + SfxItemSet aSet(pWrtShell->GetDoc()->GetAttrPool(), svl::Items); + SfxVoidItem aItem(FN_REDLINE_ACCEPT_DIRECT); + aSet.Put(aItem); + pWrtShell->GetView().GetState(aSet); + // This failed, LOK_CALLBACK_STATE_CHANGED wasn't sent. + CPPUNIT_ASSERT_EQUAL(0, m_nTrackedChangeIndex); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testRedlineUpdateCallback) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // Turn on track changes, type "xx" and delete the second one. + uno::Reference xPropertySet(mxComponent, uno::UNO_QUERY); + xPropertySet->setPropertyValue("RecordChanges", uno::Any(true)); + pWrtShell->Insert("xx"); + m_nRedlineTableEntryModified = 0; + pWrtShell->DelLeft(); + + // Assert that we get exactly one notification about the redline update. + // This was 0, as LOK_CALLBACK_REDLINE_TABLE_ENTRY_MODIFIED wasn't sent. + CPPUNIT_ASSERT_EQUAL(1, m_nRedlineTableEntryModified); + + // Turn off the change tracking mode, make some modification to left of the + // redline so that its position changes + xPropertySet->setPropertyValue("RecordChanges", uno::Any(false)); + pWrtShell->Left(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + pWrtShell->Insert("This text is left of the redline"); + + // Position of the redline has changed => Modify callback + CPPUNIT_ASSERT_EQUAL(2, m_nRedlineTableEntryModified); + + pWrtShell->DelLeft(); + // Deletion also emits Modify callback + CPPUNIT_ASSERT_EQUAL(3, m_nRedlineTableEntryModified); + + // Make changes to the right of the redline => no position change in redline + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 100/*Go enough right */, /*bBasicCall=*/false); + pWrtShell->Insert("This text is right of the redline"); + + // No Modify callbacks + CPPUNIT_ASSERT_EQUAL(3, m_nRedlineTableEntryModified); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testGetViewRenderState) +{ + SwXTextDocument* pXTextDocument = createDoc(); + int nFirstViewId = SfxLokHelper::getView(); + ViewCallback aView1; + { + SwViewOption aViewOptions; + aViewOptions.SetViewMetaChars(true); + aViewOptions.SetOnlineSpell(true); + pXTextDocument->GetDocShell()->GetWrtShell()->ApplyViewOptions(aViewOptions); + } + CPPUNIT_ASSERT_EQUAL("PS;Default"_ostr, pXTextDocument->getViewRenderState()); + + // Create a second view + SfxLokHelper::createView(); + int nSecondViewId = SfxLokHelper::getView(); + ViewCallback aView2; + { + // Give the second view different options + SwViewOption aViewOptions; + aViewOptions.SetViewMetaChars(false); + aViewOptions.SetOnlineSpell(true); + pXTextDocument->GetDocShell()->GetWrtShell()->ApplyViewOptions(aViewOptions); + } + CPPUNIT_ASSERT_EQUAL("S;Default"_ostr, pXTextDocument->getViewRenderState()); + + // Switch back to the first view, and check that the options string is the same + SfxLokHelper::setView(nFirstViewId); + CPPUNIT_ASSERT_EQUAL("PS;Default"_ostr, pXTextDocument->getViewRenderState()); + + // Switch back to the second view, and change to dark mode + SfxLokHelper::setView(nSecondViewId); + { + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + SwView* pView = pDoc->GetDocShell()->GetView(); + uno::Reference xFrame = pView->GetViewFrame().GetFrame().GetFrameInterface(); + uno::Sequence aPropertyValues = comphelper::InitPropertySequence( + { + { "NewTheme", uno::Any(OUString("Dark")) }, + } + ); + comphelper::dispatchCommand(".uno:ChangeTheme", xFrame, aPropertyValues); + } + CPPUNIT_ASSERT_EQUAL("S;Dark"_ostr, pXTextDocument->getViewRenderState()); + // Switch back to the first view, and check that the options string is the same + SfxLokHelper::setView(nFirstViewId); + CPPUNIT_ASSERT_EQUAL("PS;Default"_ostr, pXTextDocument->getViewRenderState()); +} + +// Helper function to get a tile to a bitmap and check the pixel color +static void assertTilePixelColor(SwXTextDocument* pXTextDocument, int nPixelX, int nPixelY, Color aColor) +{ + size_t nCanvasSize = 1024; + size_t nTileSize = 256; + std::vector aPixmap(nCanvasSize * nCanvasSize * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasSize, nCanvasSize), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasSize, nCanvasSize, 0, 0, 15360, 7680); + pDevice->EnableMapMode(false); + Bitmap aBitmap = pDevice->GetBitmap(Point(0, 0), Size(nTileSize, nTileSize)); + BitmapScopedReadAccess pAccess(aBitmap); + Color aActualColor(pAccess->GetPixel(nPixelX, nPixelY)); + CPPUNIT_ASSERT_EQUAL(aColor, aActualColor); +} + +// Test that changing the theme in one view doesn't change it in the other view +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testThemeViewSeparation) +{ + Color aDarkColor(0x1c, 0x1c, 0x1c); + // Add a minimal dark scheme + { + svtools::EditableColorConfig aColorConfig; + svtools::ColorConfigValue aValue; + aValue.bIsVisible = true; + aValue.nColor = aDarkColor; + aColorConfig.SetColorValue(svtools::DOCCOLOR, aValue); + aColorConfig.AddScheme(u"Dark"_ustr); + } + // Add a minimal light scheme + { + svtools::EditableColorConfig aColorConfig; + svtools::ColorConfigValue aValue; + aValue.bIsVisible = true; + aValue.nColor = COL_WHITE; + aColorConfig.SetColorValue(svtools::DOCCOLOR, aValue); + aColorConfig.AddScheme(u"Light"_ustr); + } + SwXTextDocument* pXTextDocument = createDoc(); + int nFirstViewId = SfxLokHelper::getView(); + ViewCallback aView1; + // Set first view to light scheme + { + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + SwView* pView = pDoc->GetDocShell()->GetView(); + uno::Reference xFrame = pView->GetViewFrame().GetFrame().GetFrameInterface(); + uno::Sequence aPropertyValues = comphelper::InitPropertySequence( + { + { "NewTheme", uno::Any(OUString("Light")) }, + } + ); + comphelper::dispatchCommand(".uno:ChangeTheme", xFrame, aPropertyValues); + } + // First view is in light scheme + assertTilePixelColor(pXTextDocument, 255, 255, COL_WHITE); + // Create second view + SfxLokHelper::createView(); + int nSecondViewId = SfxLokHelper::getView(); + ViewCallback aView2; + // Set second view to dark scheme + { + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + SwView* pView = pDoc->GetDocShell()->GetView(); + uno::Reference xFrame = pView->GetViewFrame().GetFrame().GetFrameInterface(); + uno::Sequence aPropertyValues = comphelper::InitPropertySequence( + { + { "NewTheme", uno::Any(OUString("Dark")) }, + } + ); + comphelper::dispatchCommand(".uno:ChangeTheme", xFrame, aPropertyValues); + } + assertTilePixelColor(pXTextDocument, 255, 255, aDarkColor); + // First view still in light scheme + SfxLokHelper::setView(nFirstViewId); + assertTilePixelColor(pXTextDocument, 255, 255, COL_WHITE); + // Second view still in dark scheme + SfxLokHelper::setView(nSecondViewId); + assertTilePixelColor(pXTextDocument, 255, 255, aDarkColor); + // Switch second view back to light scheme + { + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + SwView* pView = pDoc->GetDocShell()->GetView(); + uno::Reference xFrame = pView->GetViewFrame().GetFrame().GetFrameInterface(); + uno::Sequence aPropertyValues = comphelper::InitPropertySequence( + { + { "NewTheme", uno::Any(OUString("Light")) }, + } + ); + comphelper::dispatchCommand(".uno:ChangeTheme", xFrame, aPropertyValues); + } + // Now in light scheme + assertTilePixelColor(pXTextDocument, 255, 255, COL_WHITE); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSetViewGraphicSelection) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("frame.odt"); + int nView1 = SfxLokHelper::getView(); + ViewCallback aView1; + // Create a second view, and switch back to the first view. + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering({}); + SfxLokHelper::setView(nView1); + + // Mark the textframe in the first view. + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell->GetDrawView(); + pView->MarkObj(pObject, pView->GetSdrPageView()); + CPPUNIT_ASSERT(aView1.m_bGraphicSelection); + + // Now start to switch to the second view (part of setView()). + pWrtShell->ShellLoseFocus(); + // This failed, mark handles were hidden in the first view. + CPPUNIT_ASSERT(!pView->areMarkHandlesHidden()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testCreateViewGraphicSelection) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("frame.odt"); + ViewCallback aView1; + + // Mark the textframe in the first view. + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + SdrView* pView = pWrtShell->GetDrawView(); + aView1.m_bGraphicSelection = true; + pView->MarkObj(pObject, pView->GetSdrPageView()); + pWrtShell->HideCursor(); + CPPUNIT_ASSERT(aView1.m_bGraphicSelection); + + // Create a second view. + SfxLokHelper::createView(); + // This was false, creating a second view cleared the selection of the + // first one. + CPPUNIT_ASSERT(aView1.m_bGraphicSelection); + + // Make sure that the hidden text cursor isn't visible in the second view, either. + ViewCallback aView2(SfxViewShell::Current(), + [](ViewCallback& rView) { rView.m_bViewCursorVisible = true; }); + // This was true, the second view didn't get the visibility of the text + // cursor of the first view. + CPPUNIT_ASSERT(!aView2.m_bViewCursorVisible); + // This was false, the second view didn't get the graphic selection of the + // first view. + CPPUNIT_ASSERT(aView2.m_bGraphicViewSelection); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testCreateViewTextSelection) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + ViewCallback aView1; + + // Create a text selection: + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + // Move the cursor into the second word. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 5, /*bBasicCall=*/false); + // Create a selection on the word. + pWrtShell->SelWrd(); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + // Did we indeed manage to select the second word? + CPPUNIT_ASSERT_EQUAL(OUString("bbb"), pShellCursor->GetText()); + + // Create a second view. + SfxLokHelper::createView(); + + // Make sure that the text selection is visible in the second view. + ViewCallback aView2; + // This failed, the second view didn't get the text selection of the first view. + CPPUNIT_ASSERT(!aView2.m_aViewSelection.isEmpty()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testRedlineColors) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + + // Turn on track changes, type "zzz" at the end. + uno::Reference xPropertySet(mxComponent, uno::UNO_QUERY); + xPropertySet->setPropertyValue("RecordChanges", uno::Any(true)); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->EndOfSection(); + pWrtShell->Insert("zzz"); + + // Assert that info about exactly one author is returned. + tools::JsonWriter aJsonWriter; + pXTextDocument->getTrackedChangeAuthors(aJsonWriter); + std::stringstream aStream((std::string(aJsonWriter.finishAndGetAsOString()))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + CPPUNIT_ASSERT_EQUAL(static_cast(1), aTree.get_child("authors").size()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testCommentEndTextEdit) +{ + // Create a document, type a character and remember the cursor position. + SwXTextDocument* pXTextDocument = createDoc(); + ViewCallback aView1; + emulateTyping(*pXTextDocument, u"x"); + tools::Rectangle aBodyCursor = aView1.m_aOwnCursor; + + // Create a comment and type a character there as well. + const int nCtrlAltC = KEY_MOD1 + KEY_MOD2 + 512 + 'c' - 'a'; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 'c', nCtrlAltC); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 'c', nCtrlAltC); + emulateTyping(*pXTextDocument, u"x"); + // End comment text edit by clicking in the body text area, and assert that + // no unexpected cursor callbacks are emitted at origin (top left corner of + // the document). + aView1.m_bOwnCursorAtOrigin = false; + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, aBodyCursor.Left(), aBodyCursor.Top(), 1, MOUSE_LEFT, 0); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONUP, aBodyCursor.Left(), aBodyCursor.Top(), 1, MOUSE_LEFT, 0); + Scheduler::ProcessEventsToIdle(); + // This failed, the cursor was at 0, 0 at some point during end text edit + // of the comment. + CPPUNIT_ASSERT(!aView1.m_bOwnCursorAtOrigin); + + // Hit enter and expect invalidation. + Scheduler::ProcessEventsToIdle(); + aView1.m_bTilesInvalidated = false; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RETURN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RETURN); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT(aView1.m_bTilesInvalidated); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testCommentInsert) +{ + // Load a document with an as-char image in it. + comphelper::LibreOfficeKit::setTiledAnnotations(false); + SwXTextDocument* pXTextDocument = createDoc("image-comment.odt"); + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + SwView* pView = pDoc->GetDocShell()->GetView(); + + selectShape(1); + + // Add a comment. + uno::Reference xFrame = pView->GetViewFrame().GetFrame().GetFrameInterface(); + uno::Sequence aPropertyValues = comphelper::InitPropertySequence( + { + {"Text", uno::Any(OUString("some text"))}, + {"Author", uno::Any(OUString("me"))}, + }); + ViewCallback aView; + comphelper::dispatchCommand(".uno:InsertAnnotation", xFrame, aPropertyValues); + Scheduler::ProcessEventsToIdle(); + OString aAnchorPos(aView.m_aComment.get_child("anchorPos").get_value()); + // Without the accompanying fix in place, this test would have failed with + // - Expected: 1418, 1418, 0, 0 + // - Actual : 1418, 1418, 1024, 1024 + // i.e. the anchor position was a non-empty rectangle. + CPPUNIT_ASSERT_EQUAL("1418, 1418, 0, 0"_ostr, aAnchorPos); + comphelper::LibreOfficeKit::setTiledAnnotations(true); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testCursorPosition) +{ + // Load a document and register a callback, should get an own cursor. + SwXTextDocument* pXTextDocument = createDoc(); + ViewCallback aView1; + + // Create a second view, so the first view gets a collaborative cursor. + SfxLokHelper::createView(); + pXTextDocument->initializeForTiledRendering({}); + ViewCallback aView2; + + // Make sure the two are exactly the same. + // This failed, own cursor was at '1418, 1418', collaborative cursor was at + // '1425, 1425', due to pixel alignment. + CPPUNIT_ASSERT_EQUAL(aView1.m_aOwnCursor.toString(), aView1.m_aViewCursor.toString()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPaintCallbacks) +{ + // Test that paintTile() never results in callbacks, which can cause a + // paint <-> invalidate loop. + + // Load a document and register a callback for the first view. + SwXTextDocument* pXTextDocument = createDoc(); + ViewCallback aView1; + + // Create a second view and paint a tile on that second view. + SfxLokHelper::createView(); + int nCanvasWidth = 256; + int nCanvasHeight = 256; + std::vector aBuffer(nCanvasWidth * nCanvasHeight * 4); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), Fraction(1.0), Point(), aBuffer.data()); + // Make sure that painting a tile in the second view doesn't invoke + // callbacks on the first view. + aView1.m_bCalled = false; + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, /*nTilePosY=*/0, /*nTileWidth=*/3840, /*nTileHeight=*/3840); + CPPUNIT_ASSERT(!aView1.m_bCalled); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testUndoRepairResult) +{ + // Load a document and create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + int nView1 = SfxLokHelper::getView(); + SfxLokHelper::createView(); + rtl::Reference pResult2 = new TestResultListener(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + int nView2 = SfxLokHelper::getView(); + + // Insert a character in the second view. + SfxLokHelper::setView(nView2); + emulateTyping(*pXTextDocument, u"b"); + + // Insert a character in the first view. + SfxLokHelper::setView(nView1); + emulateTyping(*pXTextDocument, u"a"); + + // Assert that by default the second view can't undo the action. + SfxLokHelper::setView(nView2); + comphelper::dispatchCommand(".uno:Undo", {}, pResult2); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(static_cast(SID_REPAIRPACKAGE), pResult2->m_nDocRepair); + + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testRedoRepairResult) +{ + // Load a document and create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + int nView1 = SfxLokHelper::getView(); + SfxLokHelper::createView(); + rtl::Reference pResult2 = new TestResultListener(); + pXTextDocument->initializeForTiledRendering(uno::Sequence()); + int nView2 = SfxLokHelper::getView(); + + // Insert a character in the second view. + SfxLokHelper::setView(nView2); + emulateTyping(*pXTextDocument, u"b"); + + // Insert a character in the first view. + SfxLokHelper::setView(nView1); + emulateTyping(*pXTextDocument, u"a"); + + comphelper::dispatchCommand(".uno:Undo", {}, pResult2); + CPPUNIT_ASSERT_EQUAL(static_cast(0), pResult2->m_nDocRepair); + + // Assert that by default the second view can't redo the action. + SfxLokHelper::setView(nView2); + comphelper::dispatchCommand(".uno:Redo", {}, pResult2); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(static_cast(SID_REPAIRPACKAGE), pResult2->m_nDocRepair); + + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +namespace { + + void checkUndoRepairStates(SwXTextDocument* pXTextDocument, SwView* pView1, SwView* pView2) + { + SfxItemSet aItemSet1(pXTextDocument->GetDocShell()->GetDoc()->GetAttrPool(), svl::Items); + SfxItemSet aItemSet2(pXTextDocument->GetDocShell()->GetDoc()->GetAttrPool(), svl::Items); + // first view, undo enabled + pView1->GetState(aItemSet1); + CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aItemSet1.GetItemState(SID_UNDO)); + const SfxUInt32Item *pUnsetItem = dynamic_cast(aItemSet1.GetItem(SID_UNDO)); + CPPUNIT_ASSERT(!pUnsetItem); + // second view, undo conflict + pView2->GetState(aItemSet2); + CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aItemSet2.GetItemState(SID_UNDO)); + const SfxUInt32Item* pUInt32Item = dynamic_cast(aItemSet2.GetItem(SID_UNDO)); + CPPUNIT_ASSERT(pUInt32Item); + CPPUNIT_ASSERT_EQUAL(static_cast(SID_REPAIRPACKAGE), pUInt32Item->GetValue()); + }; + +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDisableUndoRepair) +{ + // Create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + ViewCallback aView1; + SwView* pView1 = dynamic_cast(SfxViewShell::Current()); + CPPUNIT_ASSERT(pView1); + int nView1 = SfxLokHelper::getView(); + SfxLokHelper::createView(); + ViewCallback aView2; + SwView* pView2 = dynamic_cast(SfxViewShell::Current()); + CPPUNIT_ASSERT(pView2); + int nView2 = SfxLokHelper::getView(); + + { + SfxItemSet aItemSet1(pXTextDocument->GetDocShell()->GetDoc()->GetAttrPool(), svl::Items); + SfxItemSet aItemSet2(pXTextDocument->GetDocShell()->GetDoc()->GetAttrPool(), svl::Items); + pView1->GetState(aItemSet1); + CPPUNIT_ASSERT_EQUAL(SfxItemState::DISABLED, aItemSet1.GetItemState(SID_UNDO)); + pView2->GetState(aItemSet2); + CPPUNIT_ASSERT_EQUAL(SfxItemState::DISABLED, aItemSet2.GetItemState(SID_UNDO)); + } + + // Insert a character in the first view. + SfxLokHelper::setView(nView1); + emulateTyping(*pXTextDocument, u"k"); + checkUndoRepairStates(pXTextDocument, pView1, pView2); + + // Insert a character in the second view. + SfxLokHelper::setView(nView2); + emulateTyping(*pXTextDocument, u"u"); + { + SfxItemSet aItemSet1(pXTextDocument->GetDocShell()->GetDoc()->GetAttrPool(), svl::Items); + SfxItemSet aItemSet2(pXTextDocument->GetDocShell()->GetDoc()->GetAttrPool(), svl::Items); + // second view, undo enabled + pView2->GetState(aItemSet2); + CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aItemSet2.GetItemState(SID_UNDO)); + const SfxUInt32Item *pUnsetItem = dynamic_cast(aItemSet2.GetItem(SID_UNDO)); + CPPUNIT_ASSERT(!pUnsetItem); + // first view, undo conflict + pView1->GetState(aItemSet1); + CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aItemSet1.GetItemState(SID_UNDO)); + const SfxUInt32Item* pUInt32Item = dynamic_cast(aItemSet1.GetItem(SID_UNDO)); + CPPUNIT_ASSERT(pUInt32Item); + CPPUNIT_ASSERT_EQUAL(static_cast(SID_REPAIRPACKAGE), pUInt32Item->GetValue()); + } + + // Insert a character in the first view. + SfxLokHelper::setView(nView1); + emulateTyping(*pXTextDocument, u"l"); + checkUndoRepairStates(pXTextDocument, pView1, pView2); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testAllTrackedChanges) +{ + // Load a document. + createDoc("dummy.fodt"); + + uno::Reference xPropSet(mxComponent, uno::UNO_QUERY); + xPropSet->setPropertyValue("RecordChanges", uno::Any(true)); + + // view #1 + SwView* pView1 = dynamic_cast(SfxViewShell::Current()); + CPPUNIT_ASSERT(pView1); + SwWrtShell* pWrtShell1 = pView1->GetWrtShellPtr(); + + // view #2 + int nView1 = SfxLokHelper::getView(); + int nView2 = SfxLokHelper::createView(); + SwView* pView2 = dynamic_cast(SfxViewShell::Current()); + CPPUNIT_ASSERT(pView2); + CPPUNIT_ASSERT(pView1 != pView2); + SwWrtShell* pWrtShell2 = pView2->GetWrtShellPtr(); + // Insert text and reject all + { + pWrtShell1->StartOfSection(); + pWrtShell1->Insert("hxx"); + + pWrtShell2->EndOfSection(); + pWrtShell2->Insert("cxx"); + } + + // Get the redline + const SwRedlineTable& rTable = pWrtShell2->GetDoc()->getIDocumentRedlineAccess().GetRedlineTable(); + CPPUNIT_ASSERT_EQUAL(static_cast(2), rTable.size()); + { + SfxVoidItem aItem(FN_REDLINE_REJECT_ALL); + pView1->GetViewFrame().GetDispatcher()->ExecuteList(FN_REDLINE_REJECT_ALL, + SfxCallMode::SYNCHRON, { &aItem }); + } + + // The reject all was performed. + CPPUNIT_ASSERT_EQUAL(static_cast(0), rTable.size()); + { + SwShellCursor* pShellCursor = pWrtShell1->getShellCursor(false); + CPPUNIT_ASSERT_EQUAL(OUString("Aaa bbb."), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); + } + + // Insert text and accept all + { + pWrtShell1->StartOfSection(); + pWrtShell1->Insert("hyy"); + + pWrtShell2->EndOfSection(); + pWrtShell2->Insert("cyy"); + } + + CPPUNIT_ASSERT_EQUAL(static_cast(2), rTable.size()); + { + SfxVoidItem aItem(FN_REDLINE_ACCEPT_ALL); + pView1->GetViewFrame().GetDispatcher()->ExecuteList(FN_REDLINE_ACCEPT_ALL, + SfxCallMode::SYNCHRON, { &aItem }); + } + + // The accept all was performed + CPPUNIT_ASSERT_EQUAL(static_cast(0), rTable.size()); + { + SwShellCursor* pShellCursor = pWrtShell2->getShellCursor(false); + CPPUNIT_ASSERT_EQUAL(OUString("hyyAaa bbb.cyy"), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); + } + + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDocumentRepair) +{ + // Create two views. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + // view #1 + SfxViewShell* pView1 = SfxViewShell::Current(); + + // view #2 + int nView1 = SfxLokHelper::getView(); + SfxLokHelper::createView(); + SfxViewShell* pView2 = SfxViewShell::Current(); + int nView2 = SfxLokHelper::getView(); + CPPUNIT_ASSERT(pView1 != pView2); + { + std::unique_ptr pItem1; + std::unique_ptr pItem2; + pView1->GetViewFrame().GetBindings().QueryState(SID_DOC_REPAIR, pItem1); + pView2->GetViewFrame().GetBindings().QueryState(SID_DOC_REPAIR, pItem2); + CPPUNIT_ASSERT(pItem1); + CPPUNIT_ASSERT(pItem2); + CPPUNIT_ASSERT_EQUAL(false, pItem1->GetValue()); + CPPUNIT_ASSERT_EQUAL(false, pItem2->GetValue()); + } + + // Insert a character in the second view. + SfxLokHelper::setView(nView2); + emulateTyping(*pXTextDocument, u"u"); + { + std::unique_ptr pItem1; + std::unique_ptr pItem2; + pView1->GetViewFrame().GetBindings().QueryState(SID_DOC_REPAIR, pItem1); + pView2->GetViewFrame().GetBindings().QueryState(SID_DOC_REPAIR, pItem2); + CPPUNIT_ASSERT(pItem1); + CPPUNIT_ASSERT(pItem2); + CPPUNIT_ASSERT_EQUAL(true, pItem1->GetValue()); + CPPUNIT_ASSERT_EQUAL(true, pItem2->GetValue()); + } + + SfxLokHelper::setView(nView1); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); + SfxLokHelper::setView(nView2); + SfxViewShell::Current()->setLibreOfficeKitViewCallback(nullptr); +} + +namespace { + + void checkPageHeaderOrFooter(const SfxViewShell* pViewShell, TypedWhichId nWhich, bool bValue) + { + uno::Sequence aSeq; + SfxPoolItemHolder aResult; + pViewShell->GetDispatcher()->QueryState(nWhich, aResult); + const SfxStringListItem* pListItem(static_cast(aResult.getItem())); + CPPUNIT_ASSERT(pListItem); + pListItem->GetStringList(aSeq); + if (bValue) + { + CPPUNIT_ASSERT_EQUAL(sal_Int32(1), aSeq.getLength()); + CPPUNIT_ASSERT_EQUAL(OUString("Default Page Style"), aSeq[0]); + } + else + CPPUNIT_ASSERT(!aSeq.hasElements()); + }; + +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPageHeader) +{ + createDoc("dummy.fodt"); + SfxViewShell* pViewShell = SfxViewShell::Current(); + // Check Page Header State + checkPageHeaderOrFooter(pViewShell, FN_INSERT_PAGEHEADER, false); + // Insert Page Header + { + SfxStringItem aStyle(FN_INSERT_PAGEHEADER, "Default Page Style"); + SfxBoolItem aItem(FN_PARAM_1, true); + pViewShell->GetDispatcher()->ExecuteList(FN_INSERT_PAGEHEADER, SfxCallMode::API | SfxCallMode::SYNCHRON, {&aStyle, &aItem}); + } + // Check Page Header State + checkPageHeaderOrFooter(pViewShell, FN_INSERT_PAGEHEADER, true); + + // Remove Page Header + { + SfxStringItem aStyle(FN_INSERT_PAGEHEADER, "Default Page Style"); + SfxBoolItem aItem(FN_PARAM_1, false); + pViewShell->GetDispatcher()->ExecuteList(FN_INSERT_PAGEHEADER, SfxCallMode::API | SfxCallMode::SYNCHRON, {&aStyle, &aItem}); + } + // Check Page Header State + checkPageHeaderOrFooter(pViewShell, FN_INSERT_PAGEHEADER, false); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPageFooter) +{ + createDoc("dummy.fodt"); + SfxViewShell* pViewShell = SfxViewShell::Current(); + // Check Page Footer State + checkPageHeaderOrFooter(pViewShell, FN_INSERT_PAGEFOOTER, false); + // Insert Page Footer + { + SfxStringItem aPageStyle(FN_INSERT_PAGEFOOTER, "Default Page Style"); + SfxBoolItem aItem(FN_PARAM_1, true); + pViewShell->GetDispatcher()->ExecuteList(FN_INSERT_PAGEFOOTER, SfxCallMode::API | SfxCallMode::SYNCHRON, {&aPageStyle, &aItem}); + } + // Check Page Footer State + checkPageHeaderOrFooter(pViewShell, FN_INSERT_PAGEFOOTER, true); + + // Remove Page Footer + { + SfxStringItem aPageStyle(FN_INSERT_PAGEFOOTER, "Default Page Style"); + SfxBoolItem aItem(FN_PARAM_1, false); + pViewShell->GetDispatcher()->ExecuteList(FN_INSERT_PAGEFOOTER, SfxCallMode::API | SfxCallMode::SYNCHRON, {&aPageStyle, &aItem}); + } + // Check Footer State + checkPageHeaderOrFooter(pViewShell, FN_INSERT_PAGEFOOTER, false); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testTdf115088) +{ + // We have three lines in the test document and we try to copy the second and third line + // To the beginning of the document + SwXTextDocument* pXTextDocument = createDoc("tdf115088.odt"); + + // Select and copy second and third line + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_HOME | KEY_MOD1); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_HOME | KEY_MOD1); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN | KEY_SHIFT); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN | KEY_SHIFT); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RIGHT | KEY_SHIFT); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RIGHT | KEY_SHIFT); + Scheduler::ProcessEventsToIdle(); + comphelper::dispatchCommand(".uno:Copy", uno::Sequence()); + + // Move cursor to the beginning of the first line and paste + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_HOME | KEY_MOD1); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_HOME | KEY_MOD1); + Scheduler::ProcessEventsToIdle(); + comphelper::dispatchCommand(".uno:PasteUnformatted", uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + + // Check the resulting text in the document. (it was 1Text\n1\n1\n1) + CPPUNIT_ASSERT_EQUAL(OUString("1\n1Text\n1\n1"), pXTextDocument->getText()->getString()); + + mxComponent->dispose(); + mxComponent.clear(); + comphelper::LibreOfficeKit::setActive(false); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testRedlineField) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + + // Turn on track changes and type "x". + uno::Reference xPropertySet(mxComponent, uno::UNO_QUERY); + xPropertySet->setPropertyValue("RecordChanges", uno::Any(true)); + + SwDateTimeField aDate(static_cast(pWrtShell->GetFieldType(0, SwFieldIds::DateTime))); + //aDate->SetDateTime(::DateTime(::DateTime::SYSTEM)); + pWrtShell->InsertField2(aDate); + + // Get the redline just created + const SwRedlineTable& rTable = pWrtShell->GetDoc()->getIDocumentRedlineAccess().GetRedlineTable(); + CPPUNIT_ASSERT_EQUAL(static_cast(1), rTable.size()); + SwRangeRedline* pRedline = rTable[0]; + CPPUNIT_ASSERT(pRedline->GetDescr().indexOf(aDate.GetFieldName())!= -1); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testIMESupport) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + VclPtr pDocWindow = pXTextDocument->getDocWindow(); + + SwView* pView = dynamic_cast(SfxViewShell::Current()); + assert(pView); + SwWrtShell* pWrtShell = pView->GetWrtShellPtr(); + + // sequence of chinese IME compositions when 'nihao' is typed in an IME + const std::vector aUtf8Inputs{ "年"_ostr, "你"_ostr, "你好"_ostr, "你哈"_ostr, "你好"_ostr, "你好"_ostr }; + std::vector aInputs; + std::transform(aUtf8Inputs.begin(), aUtf8Inputs.end(), + std::back_inserter(aInputs), [](OString aInput) { + return OUString::fromUtf8(aInput); + }); + for (const auto& aInput: aInputs) + { + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, aInput); + } + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + // the cursor should be at position 2nd + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + CPPUNIT_ASSERT_EQUAL(static_cast(2), pShellCursor->GetPoint()->GetContentIndex()); + + // content contains only the last IME composition, not all + CPPUNIT_ASSERT_EQUAL(OUString(aInputs[aInputs.size() - 1] + "Aaa bbb."), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testIMEFormattingAtEndOfParagraph) +{ + comphelper::LibreOfficeKit::setActive(); + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + VclPtr pDocWindow = pXTextDocument->getDocWindow(); + + SwView* pView = dynamic_cast(SfxViewShell::Current()); + assert(pView); + SwWrtShell* pWrtShell = pView->GetWrtShellPtr(); + + // delete all characters + + for (int i = 0; i < 9; i++) + { + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DELETE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DELETE); + } + + Scheduler::ProcessEventsToIdle(); + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "a"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + // status: "a" + + comphelper::dispatchCommand(".uno:Bold", uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "b"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RETURN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RETURN); + Scheduler::ProcessEventsToIdle(); + + // status: "ab\n" + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "a"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + std::unique_ptr pWeightItem; + pView->GetViewFrame().GetBindings().QueryState(SID_ATTR_CHAR_WEIGHT, pWeightItem); + CPPUNIT_ASSERT(pWeightItem); + + CPPUNIT_ASSERT_EQUAL(FontWeight::WEIGHT_BOLD, pWeightItem->GetWeight()); + + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RETURN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RETURN); + Scheduler::ProcessEventsToIdle(); + + // status: "ab\n + // a\n" + + comphelper::dispatchCommand(".uno:Bold", uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "b"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + std::unique_ptr pWeightItem2; + pView->GetViewFrame().GetBindings().QueryState(SID_ATTR_CHAR_WEIGHT, pWeightItem2); + CPPUNIT_ASSERT(pWeightItem2); + + CPPUNIT_ASSERT_EQUAL(FontWeight::WEIGHT_NORMAL, pWeightItem2->GetWeight()); + + // status: "ab\n + // a\n" + // b" + + comphelper::dispatchCommand(".uno:Bold", uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "a"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + std::unique_ptr pWeightItem3; + pView->GetViewFrame().GetBindings().QueryState(SID_ATTR_CHAR_WEIGHT, pWeightItem3); + CPPUNIT_ASSERT(pWeightItem3); + + CPPUNIT_ASSERT_EQUAL(FontWeight::WEIGHT_BOLD, pWeightItem3->GetWeight()); + + comphelper::dispatchCommand(".uno:Bold", uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "b"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + std::unique_ptr pWeightItem4; + pView->GetViewFrame().GetBindings().QueryState(SID_ATTR_CHAR_WEIGHT, pWeightItem4); + CPPUNIT_ASSERT(pWeightItem4); + + CPPUNIT_ASSERT_EQUAL(FontWeight::WEIGHT_NORMAL, pWeightItem4->GetWeight()); + + // status: "ab\n + // a\n" + // bab" + + // the cursor should be at position 3rd + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + CPPUNIT_ASSERT_EQUAL(static_cast(3), pShellCursor->GetPoint()->GetContentIndex()); + + // check the content + CPPUNIT_ASSERT_EQUAL(OUString("bab"), pShellCursor->GetPoint()->GetNode().GetTextNode()->GetText()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testIMEFormattingAfterHeader) +{ + comphelper::LibreOfficeKit::setActive(); + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + VclPtr pDocWindow = pXTextDocument->getDocWindow(); + + SwView* pView = dynamic_cast(SfxViewShell::Current()); + assert(pView); + + // delete all characters + + comphelper::dispatchCommand(".uno:SelectAll", uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DELETE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DELETE); + + Scheduler::ProcessEventsToIdle(); + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "a"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RETURN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RETURN); + Scheduler::ProcessEventsToIdle(); + + // status: "a\n" + + comphelper::dispatchCommand( + ".uno:StyleApply?Style:string=Heading 2&FamilyName:string=ParagraphStyles", + uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "b"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "b"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + Scheduler::ProcessEventsToIdle(); + + std::unique_ptr pWeightItem; + pView->GetViewFrame().GetBindings().QueryState(SID_ATTR_CHAR_WEIGHT, pWeightItem); + CPPUNIT_ASSERT(pWeightItem); + + CPPUNIT_ASSERT_EQUAL(FontWeight::WEIGHT_BOLD, pWeightItem->GetWeight()); + + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RETURN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RETURN); + Scheduler::ProcessEventsToIdle(); + + // status: "a\n + //

bb

\n" + + pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, "c"); + pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, ""); + Scheduler::ProcessEventsToIdle(); + + // status: "a\n + //

bb

\n" + // c" + + std::unique_ptr pWeightItem2; + pView->GetViewFrame().GetBindings().QueryState(SID_ATTR_CHAR_WEIGHT, pWeightItem2); + CPPUNIT_ASSERT(pWeightItem2); + + CPPUNIT_ASSERT_EQUAL(FontWeight::WEIGHT_NORMAL, pWeightItem2->GetWeight()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSplitNodeRedlineCallback) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("splitnode_redline_callback.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // 1. test case + // Move cursor between the two tracked changes + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + Scheduler::ProcessEventsToIdle(); + + // Add a new line + m_nRedlineTableEntryModified = 0; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RETURN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RETURN); + Scheduler::ProcessEventsToIdle(); + + // Assert that we get a notification about redline modification + // The redline after the inserted node gets a different vertical position + CPPUNIT_ASSERT_EQUAL(1, m_nRedlineTableEntryModified); + + // 2. test case + // Move cursor back to the first line, so adding new line will affect both tracked changes + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_HOME | KEY_MOD1); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_HOME | KEY_MOD1); + Scheduler::ProcessEventsToIdle(); + + // Add a new line + m_nRedlineTableEntryModified = 0; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RETURN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RETURN); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(2, m_nRedlineTableEntryModified); + + // 3. test case + // Move cursor to the end of the document, so adding a new line won't affect any tracked changes + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_END | KEY_MOD1); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_END | KEY_MOD1); + Scheduler::ProcessEventsToIdle(); + + // Add a new line + m_nRedlineTableEntryModified = 0; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RETURN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RETURN); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(0, m_nRedlineTableEntryModified); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDeleteNodeRedlineCallback) +{ + // Load a document. + SwXTextDocument* pXTextDocument = createDoc("removenode_redline_callback.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // 1. test case + // Move cursor between the two tracked changes + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DOWN); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DOWN); + Scheduler::ProcessEventsToIdle(); + + // Remove one (empty) line + m_nRedlineTableEntryModified = 0; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DELETE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DELETE); + Scheduler::ProcessEventsToIdle(); + + // Assert that we get a notification about redline modification + // The redline after the removed node gets a different vertical position + CPPUNIT_ASSERT_EQUAL(1, m_nRedlineTableEntryModified); + + // 2. test case + // Move cursor back to the first line, so removing one line will affect both tracked changes + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_HOME | KEY_MOD1); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_HOME | KEY_MOD1); + Scheduler::ProcessEventsToIdle(); + + // Remove a new line + m_nRedlineTableEntryModified = 0; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DELETE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DELETE); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(2, m_nRedlineTableEntryModified); + + // 3. test case + // Move cursor to the end of the document, so removing one line won't affect any tracked changes + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_END | KEY_MOD1); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_END | KEY_MOD1); + Scheduler::ProcessEventsToIdle(); + + // Remove a line + m_nRedlineTableEntryModified = 0; + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_BACKSPACE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_BACKSPACE); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(0, m_nRedlineTableEntryModified); +} + + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testVisCursorInvalidation) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + ViewCallback aView1; + int nView1 = SfxLokHelper::getView(); + + SfxLokHelper::createView(); + int nView2 = SfxLokHelper::getView(); + ViewCallback aView2; + Scheduler::ProcessEventsToIdle(); + + // Move visible cursor in the first view + SfxLokHelper::setView(nView1); + Scheduler::ProcessEventsToIdle(); + + aView1.m_bOwnCursorInvalidated = false; + aView1.m_bViewCursorInvalidated = false; + aView2.m_bOwnCursorInvalidated = false; + aView2.m_bViewCursorInvalidated = false; + + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_RIGHT); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_RIGHT); + Scheduler::ProcessEventsToIdle(); + + CPPUNIT_ASSERT(!aView1.m_bViewCursorInvalidated); + CPPUNIT_ASSERT(aView1.m_bOwnCursorInvalidated); + CPPUNIT_ASSERT(aView2.m_bViewCursorInvalidated); + CPPUNIT_ASSERT(!aView2.m_bOwnCursorInvalidated); + + // Insert text in the second view which moves the other view's cursor too + SfxLokHelper::setView(nView2); + + Scheduler::ProcessEventsToIdle(); + aView1.m_bOwnCursorInvalidated = false; + aView1.m_bViewCursorInvalidated = false; + aView2.m_bOwnCursorInvalidated = false; + aView2.m_bViewCursorInvalidated = false; + + emulateTyping(*pXTextDocument, u"x"); + + CPPUNIT_ASSERT(aView1.m_bViewCursorInvalidated); + CPPUNIT_ASSERT(aView1.m_bOwnCursorInvalidated); + CPPUNIT_ASSERT(aView2.m_bViewCursorInvalidated); + CPPUNIT_ASSERT(aView2.m_bOwnCursorInvalidated); + // Check that views have correct location for the other's cursor. + CPPUNIT_ASSERT_EQUAL(aView1.m_aOwnCursor, aView2.m_aViewCursor); + CPPUNIT_ASSERT_EQUAL(aView2.m_aOwnCursor, aView1.m_aViewCursor); + // Their cursors should be on the same line, first view's more to the right. + CPPUNIT_ASSERT_EQUAL(aView1.m_aOwnCursor.getY(), aView2.m_aOwnCursor.getY()); + CPPUNIT_ASSERT_GREATER(aView2.m_aOwnCursor.getX(), aView1.m_aOwnCursor.getX()); + + // Do the same as before, but set the related compatibility flag first + SfxLokHelper::setView(nView2); + + comphelper::LibreOfficeKit::setViewIdForVisCursorInvalidation(true); + + Scheduler::ProcessEventsToIdle(); + aView1.m_bOwnCursorInvalidated = false; + aView1.m_bViewCursorInvalidated = false; + aView2.m_bOwnCursorInvalidated = false; + aView2.m_bViewCursorInvalidated = false; + + emulateTyping(*pXTextDocument, u"x"); + + CPPUNIT_ASSERT(aView1.m_bViewCursorInvalidated); + CPPUNIT_ASSERT(aView1.m_bOwnCursorInvalidated); + CPPUNIT_ASSERT_EQUAL(nView2, aView1.m_nOwnCursorInvalidatedBy); + CPPUNIT_ASSERT(aView2.m_bViewCursorInvalidated); + CPPUNIT_ASSERT(aView2.m_bOwnCursorInvalidated); + CPPUNIT_ASSERT_EQUAL(nView2, aView2.m_nOwnCursorInvalidatedBy); + CPPUNIT_ASSERT_EQUAL(aView1.m_aOwnCursor, aView2.m_aViewCursor); + CPPUNIT_ASSERT_EQUAL(aView2.m_aOwnCursor, aView1.m_aViewCursor); + // Their cursors should be on the same line, first view's more to the right. + CPPUNIT_ASSERT_EQUAL(aView1.m_aOwnCursor.getY(), aView2.m_aOwnCursor.getY()); + CPPUNIT_ASSERT_GREATER(aView2.m_aOwnCursor.getX(), aView1.m_aOwnCursor.getX()); + + comphelper::LibreOfficeKit::setViewIdForVisCursorInvalidation(false); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDeselectCustomShape) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + Point aStart = pShellCursor->GetSttPos(); + aStart.setX(aStart.getX() - 1000); + aStart.setY(aStart.getY() - 1000); + + comphelper::dispatchCommand(".uno:BasicShapes.hexagon", uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(static_cast(1), pWrtShell->GetDrawView()->GetMarkedObjectList().GetMarkCount()); + + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, aStart.getX(), aStart.getY(), 1, MOUSE_LEFT, 0); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONUP, aStart.getX(), aStart.getY(), 1, MOUSE_LEFT, 0); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(static_cast(0), pWrtShell->GetDrawView()->GetMarkedObjectList().GetMarkCount()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSemiTransparent) +{ + // Load a document where the top left tile contains a semi-transparent rectangle shape. + SwXTextDocument* pXTextDocument = createDoc("semi-transparent.odt"); + + // Render a larger area, and then get the color of the bottom right corner of our tile. + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + size_t nTileSize = 256; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/15360, /*nTileHeight=*/7680); + pDevice->EnableMapMode(false); + Bitmap aBitmap = pDevice->GetBitmap(Point(0, 0), Size(nTileSize, nTileSize)); + BitmapScopedReadAccess pAccess(aBitmap); + Color aColor(pAccess->GetPixel(255, 255)); + + // Without the accompanying fix in place, this test would have failed with 'Expected greater or + // equal than: 190; Actual: 159'. This means the semi-transparent gray rectangle was darker than + // expected, as it was painted twice. + CPPUNIT_ASSERT_GREATEREQUAL(190, static_cast(aColor.GetRed())); + CPPUNIT_ASSERT_GREATEREQUAL(190, static_cast(aColor.GetGreen())); + CPPUNIT_ASSERT_GREATEREQUAL(190, static_cast(aColor.GetBlue())); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testHighlightNumbering) +{ + // Load a document where the top left tile contains a semi-transparent rectangle shape. + SwXTextDocument* pXTextDocument = createDoc("tdf114799_highlight.docx"); + + // Render a larger area, and then get the color of the bottom right corner of our tile. + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + size_t nTileSize = 256; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/15360, /*nTileHeight=*/7680); + pDevice->EnableMapMode(false); + Bitmap aBitmap = pDevice->GetBitmap(Point(0, 0), Size(nTileSize, nTileSize)); + BitmapScopedReadAccess pAccess(aBitmap); + + // Yellow highlighting over numbering + Color aColor(pAccess->GetPixel(103, 148)); + CPPUNIT_ASSERT_EQUAL(COL_YELLOW, aColor); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testHighlightNumbering_shd) +{ + // Load a document where the top left tile contains a semi-transparent rectangle shape. + SwXTextDocument* pXTextDocument = createDoc("tdf114799_shd.docx"); + + // Render a larger area, and then get the color of the bottom right corner of our tile. + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + size_t nTileSize = 256; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/15360, /*nTileHeight=*/7680); + pDevice->EnableMapMode(false); + Bitmap aBitmap = pDevice->GetBitmap(Point(0, 0), Size(nTileSize, nTileSize)); + BitmapScopedReadAccess pAccess(aBitmap); + + // No highlighting over numbering - w:shd does not apply to numbering. + Color aColor(pAccess->GetPixel(103, 148)); + CPPUNIT_ASSERT_EQUAL(COL_WHITE, aColor); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPilcrowRedlining) +{ + // Load a document where the top left tile contains + // paragraph and line break symbols with redlining. + SwXTextDocument* pXTextDocument = createDoc("pilcrow-redlining.fodt"); + + // show non printing characters, including pilcrow and + // line break symbols with redlining + comphelper::dispatchCommand(".uno:ControlCodes", {}); + + // Render a larger area, and then get the color of the bottom right corner of our tile. + size_t nCanvasWidth = 2048; + size_t nCanvasHeight = 1024; + size_t nTileSize = 512; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/15360, /*nTileHeight=*/7680); + pDevice->EnableMapMode(false); + Bitmap aBitmap = pDevice->GetBitmap(Point(100, 100), Size(nTileSize, nTileSize)); + BitmapScopedReadAccess pAccess(aBitmap); + + const char* aTexts[] = { + "Insert paragraph break", + "Insert paragraph break (empty line)", + "Delete paragraph break", + "Delete paragraph break (empty line)", + "Insert line break", + "Insert line break (empty line)", + "Delete line break", + "Delete line break (empty line)" + }; + + // Check redlining (strike out and underline) over the paragraph and line break symbols + for (int nLine = 0; nLine < 8; ++nLine) + { + bool bHasRedlineColor = false; + for (int i = 0; i < 36 && !bHasRedlineColor; ++i) + { + int nY = 96 + nLine * 36 + i; + for (sal_uInt32 j = 0; j < nTileSize - 1; ++j) + { + Color aColor(pAccess->GetPixel(nY, j)); + Color aColor2(pAccess->GetPixel(nY+1, j)); + Color aColor3(pAccess->GetPixel(nY, j+1)); + Color aColor4(pAccess->GetPixel(nY+1, j+1)); + // 4-pixel same color square sign strike out or underline of redlining + // if its color is not white, black or non-printing character color + if ( aColor == aColor2 && aColor == aColor3 && aColor == aColor4 && + aColor != COL_WHITE && aColor != COL_BLACK && + aColor != NON_PRINTING_CHARACTER_COLOR ) + { + bHasRedlineColor = true; + break; + } + } + } + + CPPUNIT_ASSERT_MESSAGE(aTexts[nLine], bHasRedlineColor); + } + + comphelper::dispatchCommand(".uno:ControlCodes", {}); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testShowHiddenCharsWhenShowFormatting) +{ + // In LOKit, ignore the config setting for + // Tools - Options - Writer - Formatting Aids - Display Formatting - Hidden characters + // and always show hidden content when showing pilcrow formatting + + createSwDoc("hiddenLoremIpsum.docx"); + + // Since LOKit is active in TiledRendering, turning on "Show formatting" will show hidden text. + comphelper::dispatchCommand(".uno:ControlCodes", {}); // show format marks + Scheduler::ProcessEventsToIdle(); + + // Without this patch, no body text would be visible - so only 1 page instead of 3. + CPPUNIT_ASSERT_EQUAL(3, getPages()); + + comphelper::dispatchCommand(".uno:ControlCodes", {}); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDoubleUnderlineAndStrikeOut) +{ + // Load a document where the tracked text moving is visible with + // double underline and strike out character formatting + SwXTextDocument* pXTextDocument = createDoc("double-underline_and_strike-out.fodt"); + + // Render a larger area, and then get the color of the bottom right corner of our tile. + size_t nCanvasWidth = 700; + size_t nCanvasHeight = 350; + size_t nTileSize = 350; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/15360, /*nTileHeight=*/7680); + pDevice->EnableMapMode(false); + Bitmap aBitmap = pDevice->GetBitmap(Point(0, 0), Size(nTileSize, nTileSize)); + BitmapScopedReadAccess pAccess(aBitmap); + bool bGreenLine = false; + size_t nGreenLine = 0; + // count green horizontal lines by tracking a column of pixels counting the + // separated continuous green pixel sequences. + for (size_t nLine = 0; nLine < nTileSize; ++nLine) + { + Color aColor(pAccess->GetPixel(nLine, 100)); + if ( aColor == COL_GREEN ) + { + if ( bGreenLine == false ) + { + ++nGreenLine; + bGreenLine = true; + } + } + else + bGreenLine = false; + } + // tdf#152214 this was 0 (missing double underline and double strike out) + CPPUNIT_ASSERT_EQUAL(size_t(4), nGreenLine); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testTdf43244_SpacesOnMargin) +{ + // Load a document where the top left tile contains + // paragraph and line break symbols with redlining. + SwXTextDocument* pXTextDocument = createDoc("tdf43244_SpacesOnMargin.odt"); + + // show non printing characters, including pilcrow and + // line break symbols with redlining + comphelper::dispatchCommand(".uno:ControlCodes", {}); + + // Render a larger area, and then get the colors from the right side of the page. + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + size_t nTileSize = 64; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/15360, /*nTileHeight=*/7680); + pDevice->EnableMapMode(false); + Bitmap aBitmap = pDevice->GetBitmap(Point(730, 120), Size(nTileSize, nTileSize)); + BitmapScopedReadAccess pAccess(aBitmap); + + //Test if we see any spaces on the right margin in a 47x48 rectangle + bool bSpaceFound = false; + for (int i = 1; i < 48 && !bSpaceFound; i++) + { + for (int j = 0; j < i; j++) + { + Color aColor2(pAccess->GetPixel(j, i)); + Color aColor1(pAccess->GetPixel(i, j + 1)); + + if (aColor1.GetRed() < 255 || aColor2.GetRed() < 255) + { + bSpaceFound = true; + break; + } + } + } + CPPUNIT_ASSERT(bSpaceFound); + + comphelper::dispatchCommand(".uno:ControlCodes", {}); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testClipText) +{ + // Load a document where the top left tile contains table text with + // too small line height, but with top and bottom paragraph margins, + // avoiding of clipping top and bottom parts of the characters. + SwXTextDocument* pXTextDocument = createDoc("tdf117448.fodt"); + + // Render a larger area, and then get the top and bottom of the text in that tile + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + size_t nTileSize = 256; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/15360, /*nTileHeight=*/7680); + pDevice->EnableMapMode(false); + Bitmap aBitmap = pDevice->GetBitmap(Point(0, 0), Size(nTileSize, nTileSize)); + BitmapScopedReadAccess pAccess(aBitmap); + + // check top margin, it's not white completely (i.e. showing top of letter "T") + bool bClipTop = true; + for (int i = 0; i < 150; i++) + { + Color aTopTextColor(pAccess->GetPixel(98, 98 + i)); + if (aTopTextColor.GetRed() < 255) + { + bClipTop = false; + break; + } + } + CPPUNIT_ASSERT(!bClipTop); + // switch off because of false alarm on some platform, maybe related to font replacements +#if 0 + // check bottom margin, it's not white completely (i.e. showing bottom of letter "g") + bool bClipBottom = true; + for (int i = 0; i < 150; i++) + { + Color aBottomTextColor(pAccess->GetPixel(110, 98 + i)); + if (aBottomTextColor.R < 255) + { + bClipBottom = false; + break; + } + } + CPPUNIT_ASSERT(!bClipBottom); +#endif +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testAnchorTypes) +{ + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + selectShape(1); + + SwDoc* pDoc = pXTextDocument->GetDocShell()->GetDoc(); + SwView* pView = pXTextDocument->GetDocShell()->GetView(); + SfxItemSet aSet(pDoc->GetAttrPool(), svl::Items); + SfxBoolItem aItem(FN_TOOL_ANCHOR_PAGE); + aSet.Put(aItem); + auto pShell = dynamic_cast(pView->GetCurShell()); + pShell->GetState(aSet); + // Without the accompanying fix in place, this test would have failed, setting the anchor type + // to other than as/at-char was possible. + CPPUNIT_ASSERT(!aSet.HasItem(FN_TOOL_ANCHOR_PAGE)); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testLanguageStatus) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwView* pView = pXTextDocument->GetDocShell()->GetView(); + std::unique_ptr pItem; + pView->GetViewFrame().GetBindings().QueryState(SID_LANGUAGE_STATUS, pItem); + auto pStringListItem = dynamic_cast(pItem.get()); + CPPUNIT_ASSERT(pStringListItem); + + uno::Sequence< OUString > aList; + pStringListItem->GetStringList(aList); + CPPUNIT_ASSERT_EQUAL(OUString("English (USA);en-US"), aList[0]); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testRedlineNotificationDuringSave) +{ + // Load a document with redlines which are hidden at a layout level. + // It's an empty document, just settings.xml and content.xml are custom. + SwXTextDocument* pXTextDocument = createDoc("redline-notification-during-save.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // Save the document. + utl::MediaDescriptor aMediaDescriptor; + aMediaDescriptor["FilterName"] <<= OUString("writer8"); + uno::Reference xStorable(mxComponent, uno::UNO_QUERY); + // Without the accompanying fix in place, this test would have never returned due to an infinite + // loop while sending not needed LOK notifications for redline changes during save. + xStorable->storeToURL(maTempFile.GetURL(), aMediaDescriptor.getAsConstPropertyValueList()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testHyperlink) +{ + comphelper::LibreOfficeKit::setViewIdForVisCursorInvalidation(true); + SwXTextDocument* pXTextDocument = createDoc("hyperlink.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + m_callbackWrapper.setLOKViewId(SfxLokHelper::getView(pWrtShell->GetSfxViewShell())); + SwShellCursor* pShellCursor = pWrtShell->getShellCursor(false); + + Point aStart = pShellCursor->GetSttPos(); + aStart.setX(aStart.getX() + 1800); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONDOWN, aStart.getX(), aStart.getY(), 1, + MOUSE_LEFT, 0); + pXTextDocument->postMouseEvent(LOK_MOUSEEVENT_MOUSEBUTTONUP, aStart.getX(), aStart.getY(), 1, + MOUSE_LEFT, 0); + Scheduler::ProcessEventsToIdle(); + + CPPUNIT_ASSERT_EQUAL("hyperlink"_ostr, m_sHyperlinkText); + CPPUNIT_ASSERT_EQUAL("http://example.com/"_ostr, m_sHyperlinkLink); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testFieldmark) +{ + // Without the accompanying fix in place, this crashed on load. + createDoc("fieldmark.docx"); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDropDownFormFieldButton) +{ + SwXTextDocument* pXTextDocument = createDoc("drop_down_form_field.odt"); + pXTextDocument->setClientVisibleArea(tools::Rectangle(0, 0, 10000, 4000)); + + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // Move the cursor to trigger displaying of the field button. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + CPPUNIT_ASSERT(m_aFormFieldButton.isEmpty()); + + // Do a tile rendering to trigger the button message with a valid text area + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/10000, /*nTileHeight=*/4000); + + CPPUNIT_ASSERT(!m_aFormFieldButton.isEmpty()); + { + std::stringstream aStream((std::string(m_aFormFieldButton))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + OString sAction( aTree.get_child("action").get_value() ); + CPPUNIT_ASSERT_EQUAL("show"_ostr, sAction); + + OString sType( aTree.get_child("type").get_value() ); + CPPUNIT_ASSERT_EQUAL("drop-down"_ostr, sType); + + OString sTextArea( aTree.get_child("textArea").get_value() ); + CPPUNIT_ASSERT_EQUAL("1538, 1418, 1026, 275"_ostr, sTextArea); + + boost::property_tree::ptree aItems = aTree.get_child("params").get_child("items"); + CPPUNIT_ASSERT_EQUAL(size_t(6), aItems.size()); + + OStringBuffer aItemList; + for (auto &item : aItems) + { + aItemList.append(item.second.get_value().c_str() + + OString::Concat(";")); + } + CPPUNIT_ASSERT_EQUAL("2019/2020;2020/2021;2021/2022;2022/2023;2023/2024;2024/2025;"_ostr, aItemList.toString()); + + OString sSelected( aTree.get_child("params").get_child("selected").get_value() ); + CPPUNIT_ASSERT_EQUAL("1"_ostr, sSelected); + + OString sPlaceholder( aTree.get_child("params").get_child("placeholderText").get_value() ); + CPPUNIT_ASSERT_EQUAL("No Item specified"_ostr, sPlaceholder); + } + + // Move the cursor back so the button becomes hidden. + pWrtShell->Left(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + + CPPUNIT_ASSERT(!m_aFormFieldButton.isEmpty()); + { + std::stringstream aStream((std::string(m_aFormFieldButton))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + OString sAction( aTree.get_child("action").get_value() ); + CPPUNIT_ASSERT_EQUAL("hide"_ostr, sAction); + + OString sType( aTree.get_child("type").get_value() ); + CPPUNIT_ASSERT_EQUAL("drop-down"_ostr, sType); + } +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDropDownFormFieldButtonEditing) +{ + SwXTextDocument* pXTextDocument = createDoc("drop_down_form_field2.odt"); + pXTextDocument->setClientVisibleArea(tools::Rectangle(0, 0, 10000, 4000)); + + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // Move the cursor to trigger displaying of the field button. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + CPPUNIT_ASSERT(m_aFormFieldButton.isEmpty()); + + // Do a tile rendering to trigger the button message with a valid text area + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/10000, /*nTileHeight=*/4000); + + // The item with the index '1' is selected by default + CPPUNIT_ASSERT(!m_aFormFieldButton.isEmpty()); + { + std::stringstream aStream((std::string(m_aFormFieldButton))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + OString sSelected( aTree.get_child("params").get_child("selected").get_value() ); + CPPUNIT_ASSERT_EQUAL("1"_ostr, sSelected); + } + m_aFormFieldButton = ""_ostr; + + // Trigger a form field event to select a different item. + vcl::ITiledRenderable::StringMap aArguments; + aArguments["type"] = "drop-down"; + aArguments["cmd"] = "selected"; + aArguments["data"] = "3"; + pXTextDocument->executeFromFieldEvent(aArguments); + + // Do a tile rendering to trigger the button message. + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/10000, /*nTileHeight=*/4000); + + CPPUNIT_ASSERT(!m_aFormFieldButton.isEmpty()); + { + std::stringstream aStream((std::string(m_aFormFieldButton))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + OString sSelected( aTree.get_child("params").get_child("selected").get_value() ); + CPPUNIT_ASSERT_EQUAL("3"_ostr, sSelected); + } +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDropDownFormFieldButtonNoSelection) +{ + SwXTextDocument* pXTextDocument = createDoc("drop_down_form_field_noselection.odt"); + pXTextDocument->setClientVisibleArea(tools::Rectangle(0, 0, 10000, 4000)); + + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // Move the cursor to trigger displaying of the field button. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + CPPUNIT_ASSERT(m_aFormFieldButton.isEmpty()); + + // Do a tile rendering to trigger the button message with a valid text area + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/10000, /*nTileHeight=*/4000); + + // None of the items is selected + CPPUNIT_ASSERT(!m_aFormFieldButton.isEmpty()); + { + std::stringstream aStream((std::string(m_aFormFieldButton))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + OString sSelected( aTree.get_child("params").get_child("selected").get_value() ); + CPPUNIT_ASSERT_EQUAL("-1"_ostr, sSelected); + } +} + +static void lcl_extractHandleParameters(std::string_view selection, sal_Int32& id, sal_Int32& x, sal_Int32& y) +{ + OString extraInfo( selection.substr(selection.find("{")) ); + std::stringstream aStream((std::string(extraInfo))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + boost::property_tree::ptree + handle0 = aTree + .get_child("handles") + .get_child("kinds") + .get_child("rectangle") + .get_child("1") + .begin()->second; + id = handle0.get_child("id").get_value(); + x = handle0.get_child("point").get_child("x").get_value(); + y = handle0.get_child("point").get_child("y").get_value(); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testMoveShapeHandle) +{ + comphelper::LibreOfficeKit::setActive(); + SwXTextDocument* pXTextDocument = createDoc("shape.fodt"); + + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + SdrPage* pPage = pWrtShell->GetDoc()->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0); + SdrObject* pObject = pPage->GetObj(0); + pWrtShell->SelectObj(Point(), 0, pObject); + Scheduler::ProcessEventsToIdle(); + + CPPUNIT_ASSERT(!m_ShapeSelection.isEmpty()); + { + sal_Int32 id, x, y; + lcl_extractHandleParameters(m_ShapeSelection, id, x ,y); + sal_Int32 oldX = x; + sal_Int32 oldY = y; + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {"HandleNum", uno::Any(id)}, + {"NewPosX", uno::Any(x+1)}, + {"NewPosY", uno::Any(y+1)} + })); + comphelper::dispatchCommand(".uno:MoveShapeHandle", aPropertyValues); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT(!m_ShapeSelection.isEmpty()); + lcl_extractHandleParameters(m_ShapeSelection, id, x ,y); + CPPUNIT_ASSERT_EQUAL(x-1, oldX); + CPPUNIT_ASSERT_EQUAL(y-1, oldY); + } +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDropDownFormFieldButtonNoItem) +{ + SwXTextDocument* pXTextDocument = createDoc("drop_down_form_field_noitem.odt"); + pXTextDocument->setClientVisibleArea(tools::Rectangle(0, 0, 10000, 4000)); + + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + + // Move the cursor to trigger displaying of the field button. + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, 1, /*bBasicCall=*/false); + CPPUNIT_ASSERT(m_aFormFieldButton.isEmpty()); + + // Do a tile rendering to trigger the button message with a valid text area + size_t nCanvasWidth = 1024; + size_t nCanvasHeight = 512; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, + /*nTilePosY=*/0, /*nTileWidth=*/10000, /*nTileHeight=*/4000); + + // There is not item specified for the field + CPPUNIT_ASSERT(!m_aFormFieldButton.isEmpty()); + { + std::stringstream aStream((std::string(m_aFormFieldButton))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + + boost::property_tree::ptree aItems = aTree.get_child("params").get_child("items"); + CPPUNIT_ASSERT_EQUAL(size_t(0), aItems.size()); + + OString sSelected( aTree.get_child("params").get_child("selected").get_value() ); + CPPUNIT_ASSERT_EQUAL("-1"_ostr, sSelected); + } +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testTablePaintInvalidate) +{ + // Load a document with a table in it. + SwXTextDocument* pXTextDocument = createDoc("table-paint-invalidate.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + // Enter the table. + pWrtShell->Down(/*bSelect=*/false); + Scheduler::ProcessEventsToIdle(); + m_nInvalidations = 0; + + // Paint a tile. + size_t nCanvasWidth = 256; + size_t nCanvasHeight = 256; + std::vector aPixmap(nCanvasWidth * nCanvasHeight * 4, 0); + ScopedVclPtrInstance pDevice(DeviceFormat::WITHOUT_ALPHA); + pDevice->SetBackground(Wallpaper(COL_TRANSPARENT)); + pDevice->SetOutputSizePixelScaleOffsetAndLOKBuffer(Size(nCanvasWidth, nCanvasHeight), + Fraction(1.0), Point(), aPixmap.data()); + pXTextDocument->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, m_aInvalidation.Left(), + m_aInvalidation.Top(), /*nTileWidth=*/1000, + /*nTileHeight=*/1000); + Scheduler::ProcessEventsToIdle(); + + // Without the accompanying fix in place, this test would have failed with + // - Expected: 0 + // - Actual : 5 + // i.e. paint generated an invalidation, which caused a loop. + CPPUNIT_ASSERT_EQUAL(0, m_nInvalidations); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testTableCommentRemoveCallback) +{ + comphelper::LibreOfficeKit::setActive(); + comphelper::LibreOfficeKit::setTiledAnnotations(false); + + // Load a document with a comment in a table. + SwXTextDocument* pXTextDocument = createDoc("testTableCommentRemoveCallback.odt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + ViewCallback aView; + + // delete all characters + comphelper::dispatchCommand(".uno:SelectAll", uno::Sequence()); + Scheduler::ProcessEventsToIdle(); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYINPUT, 0, KEY_DELETE); + pXTextDocument->postKeyEvent(LOK_KEYEVENT_KEYUP, 0, KEY_DELETE); + Scheduler::ProcessEventsToIdle(); + + //check for comment remove callback + OString sAction(aView.m_aComment.get_child("action").get_value()); + CPPUNIT_ASSERT_EQUAL("Remove"_ostr, sAction); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSpellOnlineRenderParameter) +{ + SwXTextDocument* pXTextDocument = createDoc("dummy.fodt"); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + const SwViewOption* pOpt = pWrtShell->GetViewOptions(); + bool bSet = pOpt->IsOnlineSpell(); + + uno::Sequence aPropertyValues(comphelper::InitPropertySequence( + { + {".uno:SpellOnline", uno::Any(!bSet)}, + })); + pXTextDocument->initializeForTiledRendering(aPropertyValues); + CPPUNIT_ASSERT_EQUAL(!bSet, pOpt->IsOnlineSpell()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testExtTextInputReadOnly) +{ + // Create a document with a protected section + a normal paragraph after it. + SwXTextDocument* pXTextDocument = createDoc(); + uno::Reference xController( + pXTextDocument->getCurrentController(), uno::UNO_QUERY); + uno::Reference xCursor = xController->getViewCursor(); + uno::Reference xText = xCursor->getText(); + uno::Reference xSection( + pXTextDocument->createInstance("com.sun.star.text.TextSection"), uno::UNO_QUERY); + uno::Reference xSectionProps(xSection, uno::UNO_QUERY); + xSectionProps->setPropertyValue("IsProtected", uno::Any(true)); + xText->insertTextContent(xCursor, xSection, /*bAbsorb=*/true); + + // First paragraph is the protected section, is it empty? + VclPtr pEditWin = pXTextDocument->getDocWindow(); + CPPUNIT_ASSERT(pEditWin); + CPPUNIT_ASSERT(getParagraph(1)->getString().isEmpty()); + + // Try to type into the protected section, is it still empty? + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->SttEndDoc(/*bStt=*/true); + SfxLokHelper::postExtTextEventAsync(pEditWin, LOK_EXT_TEXTINPUT, "x"); + SfxLokHelper::postExtTextEventAsync(pEditWin, LOK_EXT_TEXTINPUT_END, "x"); + Scheduler::ProcessEventsToIdle(); + // Without the accompanying fix in place, this test would have failed, as it was possible to + // type into the protected section. + CPPUNIT_ASSERT(getParagraph(1)->getString().isEmpty()); + + // Second paragraph is a normal paragraph, is it empty? + pWrtShell->Down(/*bSelect=*/false); + CPPUNIT_ASSERT(getParagraph(2)->getString().isEmpty()); + + // Try to type into the protected section, does it have the typed content? + SfxLokHelper::postExtTextEventAsync(pEditWin, LOK_EXT_TEXTINPUT, "x"); + SfxLokHelper::postExtTextEventAsync(pEditWin, LOK_EXT_TEXTINPUT_END, "x"); + Scheduler::ProcessEventsToIdle(); + CPPUNIT_ASSERT_EQUAL(OUString("x"), getParagraph(2)->getString()); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testBulletDeleteInvalidation) +{ + // Given a document with 3 paragraphs: first 2 is bulleted, the last is not. + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->SplitNode(); + pWrtShell->Up(/*bSelect=*/false); + pWrtShell->StartAllAction(); + pWrtShell->BulletOn(); + pWrtShell->EndAllAction(); + pWrtShell->Insert2("a"); + pWrtShell->SplitNode(); + pWrtShell->Insert2("b"); + pWrtShell->Down(/*bSelect=*/false); + pWrtShell->GetLayout()->PaintSwFrame(*pWrtShell->GetOut(), + pWrtShell->GetLayout()->getFrameArea()); + Scheduler::ProcessEventsToIdle(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + m_aInvalidations = tools::Rectangle(); + + // When pressing backspace in the last paragraph. + pWrtShell->DelLeft(); + + // Then the first paragraph should not be invalidated. + SwRootFrame* pRoot = pWrtShell->GetLayout(); + SwFrame* pPage = pRoot->GetLower(); + SwFrame* pBody = pPage->GetLower(); + SwFrame* pFirstText = pBody->GetLower(); + tools::Rectangle aFirstTextRect = pFirstText->getFrameArea().SVRect(); + CPPUNIT_ASSERT(!aFirstTextRect.Overlaps(m_aInvalidations)); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testTdf155349) +{ + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + Scheduler::ProcessEventsToIdle(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + pWrtShell->Insert2("a"); + Scheduler::ProcessEventsToIdle(); + pWrtShell->Insert2("b"); + m_bFullInvalidateSeen = false; + Scheduler::ProcessEventsToIdle(); + // before fix for tdf#155349 the total area got invalidated when changing one line + CPPUNIT_ASSERT(!m_bFullInvalidateSeen); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testBulletNoNumInvalidation) +{ + // Given a document with 3 paragraphs: all are bulleted. + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->StartAllAction(); + pWrtShell->BulletOn(); + pWrtShell->EndAllAction(); + pWrtShell->Insert2("a"); + pWrtShell->SplitNode(); + pWrtShell->Insert2("b"); + pWrtShell->SplitNode(); + pWrtShell->GetLayout()->PaintSwFrame(*pWrtShell->GetOut(), + pWrtShell->GetLayout()->getFrameArea()); + Scheduler::ProcessEventsToIdle(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + m_aInvalidations = tools::Rectangle(); + + // When pressing backspace in the last paragraph to turn bullets off. + pWrtShell->StartAllAction(); + pWrtShell->NumOrNoNum(/*bDelete=*/false); + pWrtShell->EndAllAction(); + + // Then the first paragraph should not be invalidated. + SwRootFrame* pRoot = pWrtShell->GetLayout(); + SwFrame* pPage = pRoot->GetLower(); + SwFrame* pBody = pPage->GetLower(); + SwFrame* pFirstText = pBody->GetLower(); + tools::Rectangle aFirstTextRect = pFirstText->getFrameArea().SVRect(); + CPPUNIT_ASSERT(!aFirstTextRect.Overlaps(m_aInvalidations)); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testBulletMultiDeleteInvalidation) +{ + // Given a document with 5 paragraphs: all are bulleted. + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->StartAllAction(); + pWrtShell->BulletOn(); + pWrtShell->EndAllAction(); + // There is already an initial text node, so type 5 times, but split 4 times. + for (int i = 0; i < 4; ++i) + { + pWrtShell->Insert2("a"); + pWrtShell->SplitNode(); + } + pWrtShell->Insert2("a"); + // Go to the end of the 4th para. + pWrtShell->Up(/*bSelect=*/false); + pWrtShell->GetLayout()->PaintSwFrame(*pWrtShell->GetOut(), + pWrtShell->GetLayout()->getFrameArea()); + Scheduler::ProcessEventsToIdle(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + m_aInvalidations = tools::Rectangle(); + + // When selecting and deleting several bullets: select till the end of the 2nd para and delete. + pWrtShell->Up(/*bSelect=*/true, /*nCount=*/2); + pWrtShell->DelRight(); + + // Then the first paragraph should not be invalidated. + SwRootFrame* pRoot = pWrtShell->GetLayout(); + SwFrame* pPage = pRoot->GetLower(); + SwFrame* pBody = pPage->GetLower(); + SwFrame* pFirstText = pBody->GetLower(); + tools::Rectangle aFirstTextRect = pFirstText->getFrameArea().SVRect(); + CPPUNIT_ASSERT(!aFirstTextRect.Overlaps(m_aInvalidations)); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testCondCollCopy) +{ + // Given a document with a custom Text Body cond style: + SwXTextDocument* pXTextDocument = createDoc("cond-coll-copy.odt"); + uno::Sequence aPropertyValues + = { comphelper::makePropertyValue("Style", OUString("Text body")), + comphelper::makePropertyValue("FamilyName", OUString("ParagraphStyles")) }; + dispatchCommand(mxComponent, ".uno:StyleApply", aPropertyValues); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + pWrtShell->SelAll(); + + // When getting the text selection, then make sure it doesn't crash: + uno::Reference xTransferable(pXTextDocument->getSelection(), + css::uno::UNO_QUERY); + datatransfer::DataFlavor aFlavor; + aFlavor.MimeType = "text/plain;charset=utf-16"; + aFlavor.DataType = cppu::UnoType::get(); + CPPUNIT_ASSERT(xTransferable->isDataFlavorSupported(aFlavor)); + // Without the accompanying fix in place, this test would have crashed. + xTransferable->getTransferData(aFlavor); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testRedlinePortions) +{ + // Given a document with 3 portions: before insert redline (foo), the insert redline (ins) and after insert + // redline (bar): + SwXTextDocument* pXTextDocument = createDoc(); + SwDocShell* pDocShell = pXTextDocument->GetDocShell(); + SwView* pView = pDocShell->GetView(); + pView->SetRedlineAuthor("first"); + pDocShell->SetView(pView); + SwWrtShell* pWrtShell = pDocShell->GetWrtShell(); + pWrtShell->Insert("foo"); + pDocShell->SetChangeRecording(true); + pWrtShell->Insert("ins"); + pDocShell->SetChangeRecording(false); + pWrtShell->Insert("bar after"); + + // When deleting "fooinsbar": + pView->SetRedlineAuthor("second"); + pDocShell->SetView(pView); + pWrtShell->SttEndDoc(/*bStt*/true); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/true, /*nCount=*/9, /*bBasicCall=*/false); + pDocShell->SetChangeRecording(true); + pWrtShell->Delete(); + + // Then make sure that the portion list is updated, so "bar" can be marked as deleted without + // marking " after" as well: + xmlDocUniquePtr pXmlDoc = parseLayoutDump(); + assertXPath(pXmlDoc, "//SwParaPortion/SwLineLayout/SwLinePortion[1]"_ostr, "portion"_ostr, "foo"); + assertXPath(pXmlDoc, "//SwParaPortion/SwLineLayout/SwLinePortion[2]"_ostr, "portion"_ostr, "ins"); + // Without the accompanying fix in place, this test would have failed width: + // - Expected: bar + // - Actual : bar after + // i.e. the portion list was outdated, even " after" was marked as deleted. + assertXPath(pXmlDoc, "//SwParaPortion/SwLineLayout/SwLinePortion[3]"_ostr, "portion"_ostr, "bar"); + assertXPath(pXmlDoc, "//SwParaPortion/SwLineLayout/SwLinePortion[4]"_ostr, "portion"_ostr, " after"); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testContentControl) +{ + // Given a document with a content control: + SwXTextDocument* pXTextDocument = createDoc(); + uno::Reference xText = pXTextDocument->getText(); + uno::Reference xCursor = xText->createTextCursor(); + xText->insertString(xCursor, "test", /*bAbsorb=*/false); + xCursor->gotoStart(/*bExpand=*/false); + xCursor->gotoEnd(/*bExpand=*/true); + uno::Reference xContentControl( + pXTextDocument->createInstance("com.sun.star.text.ContentControl"), uno::UNO_QUERY); + uno::Reference xContentControlProps(xContentControl, uno::UNO_QUERY); + xContentControlProps->setPropertyValue("Alias", uno::Any(OUString("my alias"))); + xText->insertTextContent(xCursor, xContentControl, /*bAbsorb=*/true); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + pWrtShell->SttEndDoc(/*bStt=*/true); + m_aContentControl.clear(); + + // When entering that content control (chars 2-7 are the content control): + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, /*nCount=*/5, /*bBasicCall=*/false); + + // Then make sure that the callback is emitted: + // Without the accompanying fix in place, this test would have failed, no callback was emitted. + CPPUNIT_ASSERT(!m_aContentControl.isEmpty()); + { + std::stringstream aStream((std::string(m_aContentControl))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + OString sAction( aTree.get_child("action").get_value() ); + CPPUNIT_ASSERT_EQUAL("show"_ostr, sAction); + OString sRectangles( aTree.get_child("rectangles").get_value() ); + CPPUNIT_ASSERT(!sRectangles.isEmpty()); + // Without the accompanying fix in place, this test would have failed width: + // uncaught exception of type std::exception (or derived). + // - No such node (alias) + OString sAlias( aTree.get_child("alias").get_value() ); + CPPUNIT_ASSERT_EQUAL("my alias"_ostr, sAlias); + } + + // And when leaving that content control: + pWrtShell->SttEndDoc(/*bStt=*/true); + + // Then make sure that the callback is emitted again: + std::stringstream aStream((std::string(m_aContentControl))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + OString sAction( aTree.get_child("action").get_value() ); + CPPUNIT_ASSERT_EQUAL("hide"_ostr, sAction); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDropDownContentControl) +{ + // Given a document with a dropdown content control: + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + uno::Reference xMSF(mxComponent, uno::UNO_QUERY); + uno::Reference xTextDocument(mxComponent, uno::UNO_QUERY); + uno::Reference xText = xTextDocument->getText(); + uno::Reference xCursor = xText->createTextCursor(); + xText->insertString(xCursor, "choose an item", /*bAbsorb=*/false); + xCursor->gotoStart(/*bExpand=*/false); + xCursor->gotoEnd(/*bExpand=*/true); + uno::Reference xContentControl( + xMSF->createInstance("com.sun.star.text.ContentControl"), uno::UNO_QUERY); + uno::Reference xContentControlProps(xContentControl, uno::UNO_QUERY); + { + uno::Sequence aListItems = { + { + comphelper::makePropertyValue("DisplayText", uno::Any(OUString("red"))), + comphelper::makePropertyValue("Value", uno::Any(OUString("R"))), + }, + { + comphelper::makePropertyValue("DisplayText", uno::Any(OUString("green"))), + comphelper::makePropertyValue("Value", uno::Any(OUString("G"))), + }, + { + comphelper::makePropertyValue("DisplayText", uno::Any(OUString("blue"))), + comphelper::makePropertyValue("Value", uno::Any(OUString("B"))), + }, + }; + xContentControlProps->setPropertyValue("ListItems", uno::Any(aListItems)); + } + xText->insertTextContent(xCursor, xContentControl, /*bAbsorb=*/true); + pWrtShell->SttEndDoc(/*bStt=*/true); + m_aContentControl.clear(); + + // When entering that content control: + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, /*nCount=*/1, /*bBasicCall=*/false); + + // Then make sure that the callback is emitted: + CPPUNIT_ASSERT(!m_aContentControl.isEmpty()); + { + std::stringstream aStream((std::string(m_aContentControl))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + OString sAction( aTree.get_child("action").get_value() ); + CPPUNIT_ASSERT_EQUAL("show"_ostr, sAction); + OString sRectangles( aTree.get_child("rectangles").get_value() ); + CPPUNIT_ASSERT(!sRectangles.isEmpty()); + boost::optional oItems = aTree.get_child_optional("items"); + CPPUNIT_ASSERT(oItems); + static const std::vector vExpected = { "red", "green", "blue" }; + size_t i = 0; + for (const auto& rItem : *oItems) + { + CPPUNIT_ASSERT_EQUAL(vExpected[i++], rItem.second.get_value()); + } + } + + // And when selecting the 2nd item (green): + std::map aArguments; + aArguments.emplace("type", "drop-down"); + aArguments.emplace("selected", "1"); + pXTextDocument->executeContentControlEvent(aArguments); + + // Then make sure that the document is updated accordingly: + SwTextNode* pTextNode = pWrtShell->GetCursor()->GetPointNode().GetTextNode(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: green + // - Actual : choose an item + // i.e. the document text was not updated. + CPPUNIT_ASSERT_EQUAL(OUString("green"), pTextNode->GetExpandText(pWrtShell->GetLayout())); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testPictureContentControl) +{ + // Given a document with a picture content control: + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + uno::Reference xMSF(mxComponent, uno::UNO_QUERY); + uno::Reference xTextDocument(mxComponent, uno::UNO_QUERY); + uno::Reference xText = xTextDocument->getText(); + uno::Reference xCursor = xText->createTextCursor(); + uno::Reference xTextGraphic( + xMSF->createInstance("com.sun.star.text.TextGraphicObject"), uno::UNO_QUERY); + xTextGraphic->setPropertyValue("AnchorType", + uno::Any(text::TextContentAnchorType_AS_CHARACTER)); + uno::Reference xTextContent(xTextGraphic, uno::UNO_QUERY); + xText->insertTextContent(xCursor, xTextContent, false); + xCursor->gotoStart(/*bExpand=*/false); + xCursor->gotoEnd(/*bExpand=*/true); + uno::Reference xContentControl( + xMSF->createInstance("com.sun.star.text.ContentControl"), uno::UNO_QUERY); + uno::Reference xContentControlProps(xContentControl, uno::UNO_QUERY); + xContentControlProps->setPropertyValue("ShowingPlaceHolder", uno::Any(true)); + xContentControlProps->setPropertyValue("Picture", uno::Any(true)); + xText->insertTextContent(xCursor, xContentControl, /*bAbsorb=*/true); + pWrtShell->SttEndDoc(/*bStt=*/true); + m_aContentControl.clear(); + + // When clicking on that content control: + pWrtShell->GotoObj(/*bNext=*/true, GotoObjFlags::Any); + pWrtShell->EnterSelFrameMode(); + const SwFrameFormat* pFlyFormat = pWrtShell->GetFlyFrameFormat(); + const SwFormatAnchor& rFormatAnchor = pFlyFormat->GetAnchor(); + const SwNode* pAnchorNode = rFormatAnchor.GetAnchorNode(); + const SwTextNode* pTextNode = pAnchorNode->GetTextNode(); + SwTextAttr* pAttr = pTextNode->GetTextAttrForCharAt(0, RES_TXTATR_CONTENTCONTROL); + auto pTextContentControl = static_txtattr_cast(pAttr); + auto& rFormatContentControl + = static_cast(pTextContentControl->GetAttr()); + pWrtShell->GotoContentControl(rFormatContentControl); + + // Then make sure that the callback is emitted: + // Without the accompanying fix in place, this test would have failed, no callback was emitted. + CPPUNIT_ASSERT(!m_aContentControl.isEmpty()); + std::stringstream aStream((std::string(m_aContentControl))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + OString sAction( aTree.get_child("action").get_value() ); + CPPUNIT_ASSERT_EQUAL("change-picture"_ostr, sAction); + + // And when replacing the image: + std::map aArguments; + aArguments.emplace("type", "picture"); + OUString aURL = m_directories.getURLFromSrc(u"sw/qa/extras/uiwriter/data/ole2.png"); + aArguments.emplace("changed", aURL); + pXTextDocument->executeContentControlEvent(aArguments); + + // Then make sure that the document is updated accordingly: + uno::Reference xShape = getShape(1); + auto xGraphic = getProperty>(xShape, "Graphic"); + // Without the accompanying fix in place, this test would have failed, xGraphic was empty after + // executeContentControlEvent(). + CPPUNIT_ASSERT(xGraphic.is()); + CPPUNIT_ASSERT_EQUAL(OUString("image/png"), getProperty(xGraphic, "MimeType")); + +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testDateContentControl) +{ + // Given a document with a date content control: + SwXTextDocument* pXTextDocument = createDoc(); + SwWrtShell* pWrtShell = pXTextDocument->GetDocShell()->GetWrtShell(); + setupLibreOfficeKitViewCallback(pWrtShell->GetSfxViewShell()); + uno::Reference xMSF(mxComponent, uno::UNO_QUERY); + uno::Reference xTextDocument(mxComponent, uno::UNO_QUERY); + uno::Reference xText = xTextDocument->getText(); + uno::Reference xCursor = xText->createTextCursor(); + xText->insertString(xCursor, "choose a date", /*bAbsorb=*/false); + xCursor->gotoStart(/*bExpand=*/false); + xCursor->gotoEnd(/*bExpand=*/true); + uno::Reference xContentControl( + xMSF->createInstance("com.sun.star.text.ContentControl"), uno::UNO_QUERY); + uno::Reference xContentControlProps(xContentControl, uno::UNO_QUERY); + xContentControlProps->setPropertyValue("Date", uno::Any(true)); + xContentControlProps->setPropertyValue("DateFormat", uno::Any(OUString("YYYY-MM-DD"))); + xContentControlProps->setPropertyValue("DateLanguage", uno::Any(OUString("en-US"))); + xText->insertTextContent(xCursor, xContentControl, /*bAbsorb=*/true); + pWrtShell->SttEndDoc(/*bStt=*/true); + m_aContentControl.clear(); + + // When entering that content control: + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/false, /*nCount=*/1, /*bBasicCall=*/false); + + // Then make sure that the callback is emitted: + CPPUNIT_ASSERT(!m_aContentControl.isEmpty()); + { + std::stringstream aStream((std::string(m_aContentControl))); + boost::property_tree::ptree aTree; + boost::property_tree::read_json(aStream, aTree); + OString sAction( aTree.get_child("action").get_value() ); + CPPUNIT_ASSERT_EQUAL("show"_ostr, sAction); + OString sRectangles( aTree.get_child("rectangles").get_value() ); + CPPUNIT_ASSERT(!sRectangles.isEmpty()); + boost::optional oDate = aTree.get_child_optional("date"); + CPPUNIT_ASSERT(oDate); + } + + // And when selecting a date: + std::map aArguments; + aArguments.emplace("type", "date"); + aArguments.emplace("selected", "2022-05-30T00:00:00Z"); + pXTextDocument->executeContentControlEvent(aArguments); + + // Then make sure that the document is updated accordingly: + SwTextNode* pTextNode = pWrtShell->GetCursor()->GetPointNode().GetTextNode(); + // Without the accompanying fix in place, this test would have failed with: + // - Expected: 2022-05-30 + // - Actual : choose a date + // i.e. the document text was not updated. + CPPUNIT_ASSERT_EQUAL(OUString("2022-05-30"), pTextNode->GetExpandText(pWrtShell->GetLayout())); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testAuthorField) +{ + SwXTextDocument* pXTextDocument = createDoc(); + static constexpr OUString sAuthor(u"Abcd Xyz"_ustr); + + uno::Sequence aPropertyValues1(comphelper::InitPropertySequence( + { + {".uno:Author", uno::Any(sAuthor)}, + })); + pXTextDocument->initializeForTiledRendering(aPropertyValues1); + + auto insertAuthorField = [this]() + { + uno::Reference const xMSF(mxComponent, uno::UNO_QUERY_THROW); + uno::Reference const xTD(mxComponent, uno::UNO_QUERY_THROW); + + auto const xText = xTD->getText(); + auto const xTextCursor = xText->createTextCursor(); + CPPUNIT_ASSERT(xTextCursor.is()); + + xTextCursor->gotoEnd(false); + + uno::Reference const xTextField( + xMSF->createInstance("com.sun.star.text.textfield.Author"), uno::UNO_QUERY_THROW); + + uno::Reference xTextFieldProps(xTextField, uno::UNO_QUERY_THROW); + xTextFieldProps->setPropertyValue("FullName", uno::Any(true)); + + xText->insertTextContent(xTextCursor, xTextField, false); + }; + + insertAuthorField(); + Scheduler::ProcessEventsToIdle(); + + xmlDocUniquePtr pXmlDoc = parseLayoutDump(); + + assertXPath(pXmlDoc, "/root/page[1]/body/txt[1]/SwParaPortion[1]/SwLineLayout[1]/SwFieldPortion[1]"_ostr, "expand"_ostr, sAuthor); +} + +CPPUNIT_TEST_FIXTURE(SwTiledRenderingTest, testSavedAuthorField) +{ + SwXTextDocument* pXTextDocument = createDoc("savedauthorfield.odt"); + static constexpr OUString sAuthor(u"XYZ ABCD"_ustr); + uno::Sequence aPropertyValues1(comphelper::InitPropertySequence( + { + {".uno:Author", uno::Any(sAuthor)}, + })); + pXTextDocument->initializeForTiledRendering(aPropertyValues1); + + Scheduler::ProcessEventsToIdle(); + + xmlDocUniquePtr pXmlDoc = parseLayoutDump(); + assertXPath(pXmlDoc, "/root/page[1]/body/txt[1]/SwParaPortion[1]/SwLineLayout[1]/SwFieldPortion[1]"_ostr, "expand"_ostr, sAuthor); +} + +CPPUNIT_PLUGIN_IMPLEMENT(); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ -- cgit v1.2.3