sw/inc/crsrsh.hxx | 3 + sw/inc/docsh.hxx | 3 + sw/qa/extras/uiwriter/uiwriter3.cxx | 44 ++++++++++++++++++++ sw/source/core/crsr/crsrsh.cxx | 39 ++++++++++++++++++ sw/source/uibase/app/docst.cxx | 78 +++++++++++++++++++++++++++++++++++- 5 files changed, 166 insertions(+), 1 deletion(-)
New commits: commit 7a35f3dc7419d833b8f47069c4df63e900ccb880 Author: László Németh <nem...@numbertext.org> AuthorDate: Fri Oct 25 00:38:35 2024 +0200 Commit: László Németh <nem...@numbertext.org> CommitDate: Sat Oct 26 17:35:09 2024 +0200 tdf#48459 sw inline heading: apply it on the selected words Selected text at the beginning of a paragraph (<= 75 characters) become text frame based inline heading at applying a paragraph style (using Formatting toolbar, context menu, Ctrl-1...Ctrl-5 or Styles sidebar panel). If the whole paragraph is selected, or if no or multiple paragraphs are selected, formatting is still applied on the whole paragraphs. Using text frames for inline heading is ODF 1.0 compliant and fully back-compatible with the older Writer versions. The new inline heading frame contains direct formatting to zero the upper and bottom paragraph margin to solve interoperability issues: in MSO, margins of heading styles are zeroed by using the style separators. Note: lack of inline heading was a showstopper for creating APA-, IEEE-, MIL-STD-961E-format, legal etc. documents. Note: recent Formula frame style will be replaced by the planned Inline Heading, which will be used by the DOCX filter to export OOXML style separators instead of text frames. Change-Id: I6722dcaef046bdbca2fe044d175806fa8c65278c Reviewed-on: https://gerrit.libreoffice.org/c/core/+/175580 Tested-by: Jenkins Reviewed-by: László Németh <nem...@numbertext.org> diff --git a/sw/inc/crsrsh.hxx b/sw/inc/crsrsh.hxx index 02a08289a889..806b1b7d2472 100644 --- a/sw/inc/crsrsh.hxx +++ b/sw/inc/crsrsh.hxx @@ -552,6 +552,9 @@ public: // Check if selection is within one paragraph. bool IsSelOnePara() const; + // Check if selection starts a paragraph. + bool IsSelStartPara() const; + /* * Returns SRectangle, at which the cursor is located. */ diff --git a/sw/inc/docsh.hxx b/sw/inc/docsh.hxx index f29c4ed78134..858c778ac784 100644 --- a/sw/inc/docsh.hxx +++ b/sw/inc/docsh.hxx @@ -54,6 +54,7 @@ class SwDrawModel; class SwViewShell; class SwDocStyleSheetPool; class SwXTextDocument; +class SwTextFormatColl; namespace svt { class EmbeddedObjectRef; @@ -143,6 +144,8 @@ class SW_DLLPUBLIC SwDocShell SAL_DLLPRIVATE void Delete(const OUString &rName, SfxStyleFamily nFamily); SAL_DLLPRIVATE void Hide(const OUString &rName, SfxStyleFamily nFamily, bool bHidden); + SAL_DLLPRIVATE bool MakeInlineHeading(SwWrtShell *pSh, SwTextFormatColl* pColl, + const sal_uInt16 nMode); SAL_DLLPRIVATE SfxStyleFamily ApplyStyles(const OUString &rName, const SfxStyleFamily nFamily, SwWrtShell* pShell, diff --git a/sw/qa/extras/uiwriter/uiwriter3.cxx b/sw/qa/extras/uiwriter/uiwriter3.cxx index f7c31f4e76d8..cbd860f0aabf 100644 --- a/sw/qa/extras/uiwriter/uiwriter3.cxx +++ b/sw/qa/extras/uiwriter/uiwriter3.cxx @@ -1315,6 +1315,50 @@ CPPUNIT_TEST_FIXTURE(SwUiWriterTest3, testTdf147206) u"HyperLinkURL"_ustr)); } +CPPUNIT_TEST_FIXTURE(SwUiWriterTest3, testTdf48459) +{ + createSwDoc(); + SwDoc* pDoc = getSwDoc(); + SwWrtShell* pWrtShell = pDoc->GetDocShell()->GetWrtShell(); + + // insert paragraph text + pWrtShell->Insert(u"Heading and normal text"_ustr); + + // select the first word (proposed for inline heading) + pWrtShell->SttEndDoc(/*bStart=*/true); + pWrtShell->Right(SwCursorSkipMode::Chars, /*bSelect=*/true, 7, /*bBasicCall=*/false); + + // apply styles only on the selected word -> create inline heading + uno::Sequence<beans::PropertyValue> aPropertyValues = comphelper::InitPropertySequence({ + { "Style", uno::Any(u"Heading 1"_ustr) }, + { "FamilyName", uno::Any(u"ParagraphStyles"_ustr) }, + }); + dispatchCommand(mxComponent, u".uno:StyleApply"_ustr, aPropertyValues); + + uno::Reference<frame::XFrames> xFrames = mxDesktop->getFrames(); + + // inline heading frame + CPPUNIT_ASSERT_EQUAL(sal_Int32(1), xFrames->getCount()); + + pWrtShell->EndOfSection(false); + + // insert table of contents to check ToC content (containing only the inline heading) + SwTOXMgr mgr(pWrtShell); + SwTOXDescription desc{ TOX_CONTENT }; + mgr.UpdateOrInsertTOX(desc, nullptr, nullptr); + + CPPUNIT_ASSERT_EQUAL(int(3), getParagraphs()); + + // first paragraph: selected text moved to the inline heading frame + CPPUNIT_ASSERT_EQUAL(u" and normal text"_ustr, getParagraph(1)->getString()); + + // ToC title + CPPUNIT_ASSERT_EQUAL(u"Table of Contents"_ustr, getParagraph(2)->getString()); + + // ToC contains only the inline heading + CPPUNIT_ASSERT_EQUAL(u"Heading 1"_ustr, getParagraph(3)->getString()); +} + CPPUNIT_TEST_FIXTURE(SwUiWriterTest3, testTdf144840) { createSwDoc("tdf144840.odt"); diff --git a/sw/source/core/crsr/crsrsh.cxx b/sw/source/core/crsr/crsrsh.cxx index fce43323b19b..6386b8673856 100644 --- a/sw/source/core/crsr/crsrsh.cxx +++ b/sw/source/core/crsr/crsrsh.cxx @@ -1394,6 +1394,45 @@ bool SwCursorShell::IsSelOnePara() const return false; } +bool SwCursorShell::IsSelStartPara() const +{ + if (m_pCurrentCursor->IsMultiSelection()) + { + return false; + } + if (m_pCurrentCursor->GetPoint()->GetContentIndex() == 0 || + m_pCurrentCursor->GetMark()->GetContentIndex() == 0) + { + return true; + } + if (GetLayout()->HasMergedParas()) + { + SwTextNode const*const pNode(m_pCurrentCursor->GetPoint()->GetNode().GetTextNode()); + if (pNode) + { + SwTextFrame const*const pFrame(static_cast<SwTextFrame*>( + pNode->getLayoutFrame(GetLayout()))); + if (pFrame) + { + return pFrame->MapModelToViewPos(*m_pCurrentCursor->GetPoint()) + == TextFrameIndex(0); + } + } + SwTextNode const*const pNode2(m_pCurrentCursor->GetMark()->GetNode().GetTextNode()); + if (pNode2) + { + SwTextFrame const*const pFrame(static_cast<SwTextFrame*>( + pNode2->getLayoutFrame(GetLayout()))); + if (pFrame) + { + return pFrame->MapModelToViewPos(*m_pCurrentCursor->GetMark()) + == TextFrameIndex(0); + } + } + } + return false; +} + bool SwCursorShell::IsSttPara() const { if (GetLayout()->HasMergedParas()) diff --git a/sw/source/uibase/app/docst.cxx b/sw/source/uibase/app/docst.cxx index d6be439d4259..e81ccb9aff8d 100644 --- a/sw/source/uibase/app/docst.cxx +++ b/sw/source/uibase/app/docst.cxx @@ -85,6 +85,10 @@ #include <docmodel/theme/Theme.hxx> #include <svx/svdpage.hxx> #include <officecfg/Office/Common.hxx> +#include <fmtfsize.hxx> +#include <svl/ptitem.hxx> +#include <editeng/sizeitem.hxx> +#include <editeng/ulspitem.hxx> using namespace ::com::sun::star; @@ -1170,6 +1174,71 @@ void SwDocShell::Hide(const OUString &rName, SfxStyleFamily nFamily, bool bHidde } } +#define MAX_CHAR_IN_INLINE_HEADING 75 +bool SwDocShell::MakeInlineHeading(SwWrtShell *pSh, SwTextFormatColl* pColl, const sal_uInt16 nMode) +{ + // insert an inline heading frame, if only MAX_CHAR_IN_INLINE_HEADING or less + // characters are selected beginning of a single paragraph, but not the full paragraph + // TODO extend it for multiple selections + if ( pSh->IsSelOnePara() && !pSh->IsSelFullPara() && pSh->IsSelStartPara() && + GetView()->GetSelectionText().getLength() < MAX_CHAR_IN_INLINE_HEADING && + 0 < GetView()->GetSelectionText().getLength() ) + { + SwTextFormatColl *pLocal = pColl? pColl: (*GetDoc()->GetTextFormatColls())[0]; + + // put inside a single Undo + SwRewriter aRewriter; + aRewriter.AddRule(UndoArg1, pLocal->GetName()); + GetWrtShell()->StartUndo(SwUndoId::SETFMTCOLL, &aRewriter); + + // anchor as character + SfxUInt16Item aAnchor(FN_INSERT_FRAME, static_cast<sal_uInt16>(1)); + SvxSizeItem aSizeItem(FN_PARAM_2, Size(1, 1)); + GetView()->GetViewFrame().GetDispatcher()->ExecuteList(FN_INSERT_FRAME, + SfxCallMode::SYNCHRON|SfxCallMode::RECORD, { &aAnchor, &aSizeItem }); + if ( pSh->IsFrameSelected() ) + { + // use the borderless frame style "Formula" + // TODO add a new frame style "Inline Heading" + SwDocStyleSheet* pStyle2 = static_cast<SwDocStyleSheet*>( + m_xBasePool->Find( "Formula", SfxStyleFamily::Frame)); + pSh->SetFrameFormat( pStyle2->GetFrameFormat() ); + + // set variable width frame to extend for the width of the text content + SfxItemSetFixed<RES_FRMATR_BEGIN, RES_FRMATR_END - 1> aSet(pSh->GetAttrPool()); + pSh->GetFlyFrameAttr( aSet ); + SwTwips nMinWidth = 100; + SwFormatFrameSize aSize(SwFrameSize::Variable, nMinWidth, nMinWidth); + aSize.SetWidthSizeType(SwFrameSize::Variable); + aSet.Put(aSize); + pSh->SetFlyFrameAttr( aSet ); + + // select the text content of the frame, and apply the paragraph style + pSh->UnSelectFrame(); + pSh->LeaveSelFrameMode(); + pSh->MoveSection( GoCurrSection, fnSectionEnd ); + pSh->SelAll(); + + pSh->SetTextFormatColl( pColl, true, (nMode & KEY_MOD1) ? SetAttrMode::REMOVE_ALL_ATTR : SetAttrMode::DEFAULT); + + // zero the upper and lower margins of the paragraph (also an interoperability issue) + SfxItemSetFixed<RES_UL_SPACE, RES_UL_SPACE> aSet2(pSh->GetAttrPool()); + pSh->GetCurAttr( aSet2 ); + SvxULSpaceItem aUL( 0, 0, RES_UL_SPACE ); + pSh->SetAttrItem( aUL ); + + // leave the inline heading frame + GetView()->GetViewFrame().GetDispatcher()->Execute(FN_ESCAPE, SfxCallMode::ASYNCHRON); + GetView()->GetViewFrame().GetDispatcher()->Execute(FN_ESCAPE, SfxCallMode::ASYNCHRON); + GetView()->GetViewFrame().GetDispatcher()->Execute(FN_ESCAPE, SfxCallMode::SYNCHRON); + + GetWrtShell()->EndUndo(); + return true; + } + } + return false; +} + // apply template SfxStyleFamily SwDocShell::ApplyStyles(const OUString &rName, SfxStyleFamily nFamily, SwWrtShell* pShell, const sal_uInt16 nMode ) @@ -1211,10 +1280,17 @@ SfxStyleFamily SwDocShell::ApplyStyles(const OUString &rName, SfxStyleFamily nFa // outline node become folded content of the previous outline node if the previous // outline node's content is folded. MakeAllOutlineContentTemporarilyVisible a(GetDoc()); + + // if the first 75 or less characters are selected, but not the full paragraph, + // create an inline heading from the selected text + SwTextFormatColl* pColl = pStyle->GetCollection(); + if ( MakeInlineHeading( pSh, pColl, nMode ) ) + break; + // #i62675# // clear also list attributes at affected text nodes, if paragraph // style has the list style attribute set. - pSh->SetTextFormatColl( pStyle->GetCollection(), true, (nMode & KEY_MOD1) ? SetAttrMode::REMOVE_ALL_ATTR : SetAttrMode::DEFAULT); + pSh->SetTextFormatColl( pColl, true, (nMode & KEY_MOD1) ? SetAttrMode::REMOVE_ALL_ATTR : SetAttrMode::DEFAULT); } break; }