Related: tdf#89178 Add an option to avoid converting some fields into text
... during mail merge.
In some modes (generating individual documents; creating PDF) the mail merge
process converts all fields into text. But sometimes it is undesirable for
fields not involved into mail merge itself:
* It is inconsistent with how MS Word behaves;
* The generated editable documents could benefit from having other fields
kept as fields;
* Some fields, when exported to PDF, produce different results: e.g.,
placeholder fields are output as empty spaces, not as placeholder text.
An expert boolean configuration option is added:
Office/Writer/FormLetter/ConvertToTextOnlyMMFields; it is false by default,
in which case, the behavior is unchanged. When true, all fields in the mail
merge document, except for database fields and hidden text fields, are not
converted to text during mail merge process.
Change-Id: Ibdb505ed3f2762db063bb0a91b674d27ecbc2e7f
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/158306
Tested-by: Jenkins
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/158476
Reviewed-by: Michael Stahl <michael.stahl@allotropia.de>
diff --git a/officecfg/registry/schema/org/openoffice/Office/Writer.xcs b/officecfg/registry/schema/org/openoffice/Office/Writer.xcs
index cfa36e9..d09e31d 100644
--- a/officecfg/registry/schema/org/openoffice/Office/Writer.xcs
+++ b/officecfg/registry/schema/org/openoffice/Office/Writer.xcs
@@ -6000,6 +6000,12 @@
</prop>
</group>
</group>
<prop oor:name="ConvertToTextOnlyMMFields" oor:type="xs:boolean" oor:nillable="false">
<info>
<desc>When true, only fields that can be used in mail merge will be converted to text; all other fields will be kept as fields in the Mail Merge output</desc>
</info>
<value>false</value>
</prop>
</group>
<group oor:name="Misc">
<info>
diff --git a/sw/inc/doc.hxx b/sw/inc/doc.hxx
index a08638d..bbf0451 100644
--- a/sw/inc/doc.hxx
+++ b/sw/inc/doc.hxx
@@ -1445,6 +1445,7 @@ public:
// restore the invisible content if it's available on the undo stack
bool RestoreInvisibleContent();
// Replace fields by text - mailmerge support
SAL_DLLPRIVATE bool ConvertFieldsToText(SwRootFrame const& rLayout);
// Create sub-documents according to given collection.
diff --git a/sw/source/core/doc/doc.cxx b/sw/source/core/doc/doc.cxx
index 18d9fa2..e3b606c 100644
--- a/sw/source/core/doc/doc.cxx
+++ b/sw/source/core/doc/doc.cxx
@@ -53,6 +53,8 @@
#include <editeng/pbinitem.hxx>
#include <unotools/localedatawrapper.hxx>
#include <officecfg/Office/Writer.hxx>
#include <swatrset.hxx>
#include <swmodule.hxx>
#include <fmtrfmrk.hxx>
@@ -1608,12 +1610,32 @@ bool SwDoc::RestoreInvisibleContent()
return false;
}
static bool IsMailMergeField(SwFieldIds fieldId)
{
switch (fieldId)
{
case SwFieldIds::Database: // Mail merge fields
case SwFieldIds::DatabaseName: // Database name
case SwFieldIds::HiddenText: // Hidden text may use database fields in condition
case SwFieldIds::HiddenPara: // Hidden paragraph may use database fields in condition
case SwFieldIds::DbNextSet: // Moving to next mail merge record
case SwFieldIds::DbNumSet: // Moving to a specific mail merge record
case SwFieldIds::DbSetNumber: // Number of current mail merge record
return true;
default:
return false;
}
}
bool SwDoc::ConvertFieldsToText(SwRootFrame const& rLayout)
{
bool bRet = false;
getIDocumentFieldsAccess().LockExpFields();
GetIDocumentUndoRedo().StartUndo( SwUndoId::UI_REPLACE, nullptr );
const bool bOnlyConvertDBFields
= officecfg::Office::Writer::FormLetter::ConvertToTextOnlyMMFields::get();
const SwFieldTypes* pMyFieldTypes = getIDocumentFieldsAccess().GetFieldTypes();
const SwFieldTypes::size_type nCount = pMyFieldTypes->size();
//go backward, field types are removed
@@ -1624,6 +1646,9 @@ bool SwDoc::ConvertFieldsToText(SwRootFrame const& rLayout)
if ( SwFieldIds::Postit == pCurType->Which() )
continue;
if (bOnlyConvertDBFields && !IsMailMergeField(pCurType->Which()))
continue;
std::vector<SwFormatField*> vFieldFormats;
pCurType->GatherFields(vFieldFormats, false);
for(const auto& rpFieldFormat : vFieldFormats)
@@ -1634,67 +1659,66 @@ bool SwDoc::ConvertFieldsToText(SwRootFrame const& rLayout)
bool bSkip = !pTextField ||
!pTextField->GetpTextNode()->GetNodes().IsDocNodes();
if (bSkip)
continue;
if (!bSkip)
bool bInHeaderFooter = IsInHeaderFooter(*pTextField->GetpTextNode());
const SwFormatField& rFormatField = pTextField->GetFormatField();
const SwField* pField = rFormatField.GetField();
//#i55595# some fields have to be excluded in headers/footers
SwFieldIds nWhich = pField->GetTyp()->Which();
if(!bInHeaderFooter ||
(nWhich != SwFieldIds::PageNumber &&
nWhich != SwFieldIds::Chapter &&
nWhich != SwFieldIds::GetExp&&
nWhich != SwFieldIds::SetExp&&
nWhich != SwFieldIds::Input&&
nWhich != SwFieldIds::RefPageGet&&
nWhich != SwFieldIds::RefPageSet))
{
bool bInHeaderFooter = IsInHeaderFooter(*pTextField->GetpTextNode());
const SwFormatField& rFormatField = pTextField->GetFormatField();
const SwField* pField = rFormatField.GetField();
OUString sText = pField->ExpandField(true, &rLayout);
//#i55595# some fields have to be excluded in headers/footers
SwFieldIds nWhich = pField->GetTyp()->Which();
if(!bInHeaderFooter ||
(nWhich != SwFieldIds::PageNumber &&
nWhich != SwFieldIds::Chapter &&
nWhich != SwFieldIds::GetExp&&
nWhich != SwFieldIds::SetExp&&
nWhich != SwFieldIds::Input&&
nWhich != SwFieldIds::RefPageGet&&
nWhich != SwFieldIds::RefPageSet))
// database fields should not convert their command into text
if( SwFieldIds::Database == pCurType->Which() && !static_cast<const SwDBField*>(pField)->IsInitialized())
sText.clear();
SwPaM aInsertPam(*pTextField->GetpTextNode(), pTextField->GetStart());
aInsertPam.SetMark();
// go to the end of the field
const SwTextField *pFieldAtEnd = sw::DocumentFieldsManager::GetTextFieldAtPos(*aInsertPam.End());
if (pFieldAtEnd && pFieldAtEnd->Which() == RES_TXTATR_INPUTFIELD)
{
OUString sText = pField->ExpandField(true, &rLayout);
// database fields should not convert their command into text
if( SwFieldIds::Database == pCurType->Which() && !static_cast<const SwDBField*>(pField)->IsInitialized())
sText.clear();
SwPaM aInsertPam(*pTextField->GetpTextNode(), pTextField->GetStart());
aInsertPam.SetMark();
// go to the end of the field
const SwTextField *pFieldAtEnd = sw::DocumentFieldsManager::GetTextFieldAtPos(*aInsertPam.End());
if (pFieldAtEnd && pFieldAtEnd->Which() == RES_TXTATR_INPUTFIELD)
{
SwPosition &rEndPos = *aInsertPam.GetPoint();
rEndPos.SetContent( SwCursorShell::EndOfInputFieldAtPos( *aInsertPam.End() ) );
}
else
{
aInsertPam.Move();
}
// first insert the text after field to keep the field's attributes,
// then delete the field
if (!sText.isEmpty())
{
// to keep the position after insert
SwPaM aDelPam( *aInsertPam.GetMark(), *aInsertPam.GetPoint() );
aDelPam.Move( fnMoveBackward );
aInsertPam.DeleteMark();
getIDocumentContentOperations().InsertString( aInsertPam, sText );
aDelPam.Move();
// finally remove the field
getIDocumentContentOperations().DeleteAndJoin( aDelPam );
}
else
{
getIDocumentContentOperations().DeleteAndJoin( aInsertPam );
}
bRet = true;
SwPosition &rEndPos = *aInsertPam.GetPoint();
rEndPos.SetContent( SwCursorShell::EndOfInputFieldAtPos( *aInsertPam.End() ) );
}
else
{
aInsertPam.Move();
}
// first insert the text after field to keep the field's attributes,
// then delete the field
if (!sText.isEmpty())
{
// to keep the position after insert
SwPaM aDelPam( *aInsertPam.GetMark(), *aInsertPam.GetPoint() );
aDelPam.Move( fnMoveBackward );
aInsertPam.DeleteMark();
getIDocumentContentOperations().InsertString( aInsertPam, sText );
aDelPam.Move();
// finally remove the field
getIDocumentContentOperations().DeleteAndJoin( aDelPam );
}
else
{
getIDocumentContentOperations().DeleteAndJoin( aInsertPam );
}
bRet = true;
}
}
}