/* -*- 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/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-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 "itratr.hxx" #include #include #include "redlnitr.hxx" #include #include #include "itrtxt.hxx" #include #include #include #include #include #include using namespace ::com::sun::star::i18n; using namespace ::com::sun::star; static sal_Int32 GetNextAttrImpl(SwTextNode const* pTextNode, size_t nStartIndex, size_t nEndIndex, sal_Int32 nPosition); SwAttrIter::SwAttrIter(SwTextNode const * pTextNode) : m_pViewShell(nullptr) , m_pFont(nullptr) , m_pScriptInfo(nullptr) , m_pLastOut(nullptr) , m_nChgCnt(0) , m_nStartIndex(0) , m_nEndIndex(0) , m_nPosition(0) , m_nPropFont(0) , m_pTextNode(pTextNode) , m_pMergedPara(nullptr) { m_aFontCacheIds[SwFontScript::Latin] = m_aFontCacheIds[SwFontScript::CJK] = m_aFontCacheIds[SwFontScript::CTL] = nullptr; } SwAttrIter::SwAttrIter(SwTextNode& rTextNode, SwScriptInfo& rScrInf, SwTextFrame const*const pFrame) : m_pViewShell(nullptr) , m_pFont(nullptr) , m_pScriptInfo(nullptr) , m_pLastOut(nullptr) , m_nChgCnt(0) , m_nPropFont(0) , m_pTextNode(&rTextNode) , m_pMergedPara(nullptr) { CtorInitAttrIter(rTextNode, rScrInf, pFrame); } void SwAttrIter::Chg( SwTextAttr const *pHt ) { assert(pHt && m_pFont && "No attribute of font available for change"); if( m_pRedline && m_pRedline->IsOn() ) m_pRedline->ChangeTextAttr( m_pFont, *pHt, true ); else m_aAttrHandler.PushAndChg( *pHt, *m_pFont ); m_nChgCnt++; } void SwAttrIter::Rst( SwTextAttr const *pHt ) { assert(pHt && m_pFont && "No attribute of font available for reset"); // get top from stack after removing pHt if( m_pRedline && m_pRedline->IsOn() ) m_pRedline->ChangeTextAttr( m_pFont, *pHt, false ); else m_aAttrHandler.PopAndChg( *pHt, *m_pFont ); m_nChgCnt--; } SwAttrIter::~SwAttrIter() { m_pRedline.reset(); delete m_pFont; } bool SwAttrIter::MaybeHasHints() const { return nullptr != m_pTextNode->GetpSwpHints() || nullptr != m_pMergedPara; } /** * Returns the attribute for a position * * Only if the attribute is exactly at the position @param nPos and * does not have an EndIndex * * We need this function for attributes which should alter formatting without * changing the content of the string. * Such "degenerated" attributes are e.g.: fields which retain expanded text and * line-bound Frames. * In order to avoid ambiguities between different such attributes, we insert a * special character at the start of the string, when creating such an attribute. * The Formatter later on encounters such a special character and retrieves the * degenerate attribute via GetAttr(). */ SwTextAttr *SwAttrIter::GetAttr(TextFrameIndex const nPosition) const { std::pair const pos( m_pMergedPara ? sw::MapViewToModel(*m_pMergedPara, nPosition) : std::make_pair(m_pTextNode, sal_Int32(nPosition))); return pos.first->GetTextAttrForCharAt(pos.second); } bool SwAttrIter::SeekAndChgAttrIter(TextFrameIndex const nNewPos, OutputDevice* pOut) { std::pair const pos( m_pMergedPara ? sw::MapViewToModel(*m_pMergedPara, nNewPos) : std::make_pair(m_pTextNode, sal_Int32(nNewPos))); bool bChg = m_nStartIndex && pos.first == m_pTextNode && pos.second == m_nPosition ? m_pFont->IsFntChg() : Seek( nNewPos ); if ( m_pLastOut.get() != pOut ) { m_pLastOut = pOut; m_pFont->SetFntChg( true ); bChg = true; } if( bChg ) { // if the change counter is zero, we know the cache id of the wanted font if ( !m_nChgCnt && !m_nPropFont ) m_pFont->SetFontCacheId( m_aFontCacheIds[ m_pFont->GetActual() ], m_aFontIdx[ m_pFont->GetActual() ], m_pFont->GetActual() ); m_pFont->ChgPhysFnt( m_pViewShell, *pOut ); } return bChg; } bool SwAttrIter::IsSymbol(TextFrameIndex const nNewPos) { Seek( nNewPos ); if ( !m_nChgCnt && !m_nPropFont ) m_pFont->SetFontCacheId( m_aFontCacheIds[ m_pFont->GetActual() ], m_aFontIdx[ m_pFont->GetActual() ], m_pFont->GetActual() ); return m_pFont->IsSymbol( m_pViewShell ); } bool SwTextFrame::IsSymbolAt(TextFrameIndex const nPos) const { SwTextInfo info(const_cast(this)); SwTextIter iter(const_cast(this), &info); return iter.IsSymbol(nPos); } bool SwAttrIter::SeekStartAndChgAttrIter( OutputDevice* pOut, const bool bParaFont ) { SwTextNode const*const pFirstTextNode(m_pMergedPara ? m_pMergedPara->pFirstNode : m_pTextNode); if ( m_pRedline && m_pRedline->ExtOn() ) m_pRedline->LeaveExtend(*m_pFont, pFirstTextNode->GetIndex(), 0); if (m_pTextNode != pFirstTextNode) { assert(m_pMergedPara); m_pTextNode = m_pMergedPara->pFirstNode; InitFontAndAttrHandler(*m_pMergedPara->pParaPropsNode, *m_pTextNode, m_pMergedPara->mergedText, nullptr, nullptr); } // reset font to its original state m_aAttrHandler.Reset(); m_aAttrHandler.ResetFont( *m_pFont ); m_nStartIndex = 0; m_nEndIndex = 0; m_nPosition = 0; m_nChgCnt = 0; if( m_nPropFont ) m_pFont->SetProportion( m_nPropFont ); if( m_pRedline ) { m_pRedline->Clear( m_pFont ); if( !bParaFont ) m_nChgCnt = m_nChgCnt + m_pRedline->Seek(*m_pFont, pFirstTextNode->GetIndex(), 0, COMPLETE_STRING); else m_pRedline->Reset(); } SwpHints const*const pHints(m_pTextNode->GetpSwpHints()); if (pHints && !bParaFont) { SwTextAttr *pTextAttr; // While we've not reached the end of the StartArray && the TextAttribute starts at position 0... while ((m_nStartIndex < pHints->Count()) && !((pTextAttr = pHints->Get(m_nStartIndex))->GetStart())) { // open the TextAttributes Chg( pTextAttr ); m_nStartIndex++; } } bool bChg = m_pFont->IsFntChg(); if ( m_pLastOut.get() != pOut ) { m_pLastOut = pOut; m_pFont->SetFntChg( true ); bChg = true; } if( bChg ) { // if the application counter is zero, we know the cache id of the wanted font if ( !m_nChgCnt && !m_nPropFont ) m_pFont->SetFontCacheId( m_aFontCacheIds[ m_pFont->GetActual() ], m_aFontIdx[ m_pFont->GetActual() ], m_pFont->GetActual() ); m_pFont->ChgPhysFnt( m_pViewShell, *pOut ); } return bChg; } // AMA: New AttrIter Nov 94 void SwAttrIter::SeekFwd(const sal_Int32 nOldPos, const sal_Int32 nNewPos) { SwpHints const*const pHints(m_pTextNode->GetpSwpHints()); SwTextAttr *pTextAttr; if ( m_nStartIndex ) // If attributes have been opened at all ... { // Close attributes that are currently open, but stop at nNewPos+1 // As long as we've not yet reached the end of EndArray and the // TextAttribute ends before or at the new position ... while ((m_nEndIndex < pHints->Count()) && ((pTextAttr = pHints->GetSortedByEnd(m_nEndIndex))->GetAnyEnd() <= nNewPos)) { // Close the TextAttributes, whose StartPos were before or at // the old nPos and are currently open if (pTextAttr->GetStart() <= nOldPos) Rst( pTextAttr ); m_nEndIndex++; } } else // skip the not opened ends { while ((m_nEndIndex < pHints->Count()) && (pHints->GetSortedByEnd(m_nEndIndex)->GetAnyEnd() <= nNewPos)) { m_nEndIndex++; } } // As long as we've not yet reached the end of EndArray and the // TextAttribute ends before or at the new position... while ((m_nStartIndex < pHints->Count()) && ((pTextAttr = pHints->Get(m_nStartIndex))->GetStart() <= nNewPos)) { // open the TextAttributes, whose ends lie behind the new position if ( pTextAttr->GetAnyEnd() > nNewPos ) Chg( pTextAttr ); m_nStartIndex++; } } bool SwAttrIter::Seek(TextFrameIndex const nNewPos) { // note: nNewPos isn't necessarily an index returned from GetNextAttr std::pair const newPos( m_pMergedPara ? sw::MapViewToModel(*m_pMergedPara, nNewPos) : std::make_pair(m_pTextNode, sal_Int32(nNewPos))); if ( m_pRedline && m_pRedline->ExtOn() ) m_pRedline->LeaveExtend(*m_pFont, newPos.first->GetIndex(), newPos.second); if (m_pTextNode->GetIndex() < newPos.first->GetIndex()) { // Skipping to a different node - first seek until the end of this node // to get rid of all hint items if (m_pTextNode->GetpSwpHints()) { sal_Int32 nPos(m_nPosition); do { sal_Int32 const nOldPos(nPos); nPos = GetNextAttrImpl(m_pTextNode, m_nStartIndex, m_nEndIndex, nPos); if (nPos <= m_pTextNode->Len()) { SeekFwd(nOldPos, nPos); } else { SeekFwd(nOldPos, m_pTextNode->Len()); } } while (nPos < m_pTextNode->Len()); } assert(m_nChgCnt == 0); // should have reset it all? there cannot be ExtOn() inside of a Delete redline, surely? // Unapply current para items: // the SwAttrHandler doesn't appear to be capable of *unapplying* // items at all; it can only apply a previously effective item. // So do this by recreating the font from scratch. // Apply new para items: assert(m_pMergedPara); InitFontAndAttrHandler(*m_pMergedPara->pParaPropsNode, *newPos.first, m_pMergedPara->mergedText, nullptr, nullptr); // reset to next m_pTextNode = newPos.first; m_nStartIndex = 0; m_nEndIndex = 0; m_nPosition = 0; assert(m_pRedline); } // sw_redlinehide: Seek(0) must move before the first character, which // has a special case where the first node starts with delete redline. if ((!nNewPos && !m_pMergedPara) || newPos.first != m_pTextNode || newPos.second < m_nPosition) { if (m_pMergedPara) { if (m_pTextNode != newPos.first) { m_pTextNode = newPos.first; // sw_redlinehide: hope it's okay to use the current text node // here; the AttrHandler shouldn't care about non-char items InitFontAndAttrHandler(*m_pMergedPara->pParaPropsNode, *m_pTextNode, m_pMergedPara->mergedText, nullptr, nullptr); } } if (m_pMergedPara || m_pTextNode->GetpSwpHints()) { if( m_pRedline ) m_pRedline->Clear( nullptr ); // reset font to its original state m_aAttrHandler.Reset(); m_aAttrHandler.ResetFont( *m_pFont ); if( m_nPropFont ) m_pFont->SetProportion( m_nPropFont ); m_nStartIndex = 0; m_nEndIndex = 0; m_nPosition = 0; m_nChgCnt = 0; // Attention! // resetting the font here makes it necessary to apply any // changes for extended input directly to the font if ( m_pRedline && m_pRedline->ExtOn() ) { m_pRedline->UpdateExtFont( *m_pFont ); ++m_nChgCnt; } } } if (m_pTextNode->GetpSwpHints()) { if (m_pMergedPara) { // iterate hint by hint: SeekFwd does not mix ends and starts, // it always applies all the starts last, so it must be called once // per position where hints start/end! sal_Int32 nPos(m_nPosition); do { sal_Int32 const nOldPos(nPos); nPos = GetNextAttrImpl(m_pTextNode, m_nStartIndex, m_nEndIndex, nPos); if (nPos <= newPos.second) { SeekFwd(nOldPos, nPos); } else { SeekFwd(nOldPos, newPos.second); } } while (nPos < newPos.second); } else { SeekFwd(m_nPosition, newPos.second); } } m_pFont->SetActual( m_pScriptInfo->WhichFont(nNewPos) ); if( m_pRedline ) m_nChgCnt = m_nChgCnt + m_pRedline->Seek(*m_pFont, m_pTextNode->GetIndex(), newPos.second, m_nPosition); m_nPosition = newPos.second; if( m_nPropFont ) m_pFont->SetProportion( m_nPropFont ); return m_pFont->IsFntChg(); } static void InsertCharAttrs(SfxPoolItem const** pAttrs, SfxItemSet const& rItems) { SfxItemIter iter(rItems); for (SfxPoolItem const* pItem = iter.GetCurItem(); pItem; pItem = iter.NextItem()) { auto const nWhich(pItem->Which()); if (isCHRATR(nWhich) && RES_CHRATR_RSID != nWhich) { pAttrs[nWhich - RES_CHRATR_BEGIN] = pItem; } else if (nWhich == RES_TXTATR_UNKNOWN_CONTAINER) { pAttrs[RES_CHRATR_END] = pItem; } } } // if return false: portion ends at start of redline, indexes unchanged // if return true: portion end not known (past end of redline), indexes point to first hint past end of redline static bool CanSkipOverRedline( SwTextNode const& rStartNode, sal_Int32 const nStartRedline, SwRangeRedline const& rRedline, size_t & rStartIndex, size_t & rEndIndex, bool const isTheAnswerYes) { size_t nStartIndex(rStartIndex); size_t nEndIndex(rEndIndex); SwPosition const*const pRLEnd(rRedline.End()); if (!pRLEnd->nNode.GetNode().IsTextNode() // if fully deleted... || pRLEnd->nContent == pRLEnd->nNode.GetNode().GetTextNode()->Len()) { // shortcut: nothing follows redline // current state is end state return false; } std::vector activeCharFmts; // can't compare the SwFont that's stored somewhere, it doesn't have compare // operator, so try to recreate the situation with some temp arrays here SfxPoolItem const* activeCharAttrsStart[RES_CHRATR_END - RES_CHRATR_BEGIN + 1] = { nullptr, }; if (&rStartNode != &pRLEnd->nNode.GetNode()) { // nodes' attributes are only needed if there are different nodes InsertCharAttrs(activeCharAttrsStart, rStartNode.GetSwAttrSet()); } if (SwpHints const*const pStartHints = rStartNode.GetpSwpHints()) { // check hint ends of hints that start before and end within sal_Int32 const nRedlineEnd(&rStartNode == &pRLEnd->nNode.GetNode() ? pRLEnd->nContent.GetIndex() : rStartNode.Len()); for ( ; nEndIndex < pStartHints->Count(); ++nEndIndex) { SwTextAttr *const pAttr(pStartHints->GetSortedByEnd(nEndIndex)); if (!pAttr->End()) { continue; } if (nRedlineEnd < *pAttr->End()) { break; } if (nStartRedline <= pAttr->GetStart()) { continue; } if (pAttr->IsFormatIgnoreEnd()) { continue; } switch (pAttr->Which()) { // if any of these ends inside RL then we need a new portion case RES_TXTATR_REFMARK: case RES_TXTATR_TOXMARK: case RES_TXTATR_META: // actually these 2 aren't allowed to overlap ??? case RES_TXTATR_METAFIELD: case RES_TXTATR_INETFMT: case RES_TXTATR_CJK_RUBY: case RES_TXTATR_INPUTFIELD: { if (!isTheAnswerYes) return false; // always break } break; // these are guaranteed not to overlap // and come in order of application case RES_TXTATR_AUTOFMT: case RES_TXTATR_CHARFMT: { if (pAttr->Which() == RES_TXTATR_CHARFMT) { activeCharFmts.push_back(pAttr); } // pure formatting hints may end inside the redline & // start again inside the redline, which must not cause // a new text portion if they have the same items - so // store the effective items & compare all at the end SfxItemSet const& rSet((pAttr->Which() == RES_TXTATR_CHARFMT) ? static_cast(pAttr->GetCharFormat().GetCharFormat()->GetAttrSet()) : *pAttr->GetAutoFormat().GetStyleHandle()); InsertCharAttrs(activeCharAttrsStart, rSet); } break; // SwTextNode::SetAttr puts it into AUTOFMT which is quite // sensible so it doesn't actually exist as a hint case RES_TXTATR_UNKNOWN_CONTAINER: default: assert(false); } } assert(nEndIndex == pStartHints->Count() || pRLEnd->nContent.GetIndex() < pStartHints->GetSortedByEnd(nEndIndex)->GetAnyEnd()); } if (&rStartNode != &pRLEnd->nNode.GetNode()) { nStartIndex = 0; nEndIndex = 0; } // treat para properties as text properties // ... with the FormatToTextAttr we get autofmts that correspond to the *effective* attr set difference // effective attr set: para props + charfmts + autofmt *in that order* // ... and the charfmt must be *nominally* the same SfxPoolItem const* activeCharAttrsEnd[RES_CHRATR_END - RES_CHRATR_BEGIN + 1] = { nullptr, }; if (&rStartNode != &pRLEnd->nNode.GetNode()) { // nodes' attributes are only needed if there are different nodes InsertCharAttrs(activeCharAttrsEnd, pRLEnd->nNode.GetNode().GetTextNode()->GetSwAttrSet()); } if (SwpHints *const pEndHints = pRLEnd->nNode.GetNode().GetTextNode()->GetpSwpHints()) { // check hint starts of hints that start within and end after #ifndef NDEBUG sal_Int32 const nRedlineStart(&rStartNode == &pRLEnd->nNode.GetNode() ? nStartRedline : 0); #endif for ( ; nStartIndex < pEndHints->Count(); ++nStartIndex) { SwTextAttr *const pAttr(pEndHints->Get(nStartIndex)); // compare with < here, not <=, to get the effective formatting // of the 1st char after the redline; should not cause problems // with consecutive delete redlines because those are handed by // GetNextRedln() and here we have the last end pos. if (pRLEnd->nContent.GetIndex() < pAttr->GetStart()) { break; } if (!pAttr->End()) continue; if (pAttr->IsFormatIgnoreStart()) { continue; } assert(nRedlineStart <= pAttr->GetStart()); // we wouldn't be here otherwise? if (*pAttr->End() <= pRLEnd->nContent.GetIndex()) { continue; } switch (pAttr->Which()) { case RES_TXTATR_REFMARK: case RES_TXTATR_TOXMARK: case RES_TXTATR_META: // actually these 2 aren't allowed to overlap ??? case RES_TXTATR_METAFIELD: case RES_TXTATR_INETFMT: case RES_TXTATR_CJK_RUBY: case RES_TXTATR_INPUTFIELD: { if (!isTheAnswerYes) return false; } break; case RES_TXTATR_AUTOFMT: case RES_TXTATR_CHARFMT: { // char formats must be *nominally* the same if (pAttr->Which() == RES_TXTATR_CHARFMT) { auto iter = std::find_if(activeCharFmts.begin(), activeCharFmts.end(), [&pAttr](const SwTextAttr* pCharFmt) { return *pCharFmt == *pAttr; }); if (iter != activeCharFmts.end()) activeCharFmts.erase(iter); else if (!isTheAnswerYes) return false; } SfxItemSet const& rSet((pAttr->Which() == RES_TXTATR_CHARFMT) ? static_cast(pAttr->GetCharFormat().GetCharFormat()->GetAttrSet()) : *pAttr->GetAutoFormat().GetStyleHandle()); InsertCharAttrs(activeCharAttrsEnd, rSet); } break; // SwTextNode::SetAttr puts it into AUTOFMT which is quite // sensible so it doesn't actually exist as a hint case RES_TXTATR_UNKNOWN_CONTAINER: default: assert(false); } } if (&rStartNode != &pRLEnd->nNode.GetNode()) { // need to iterate the nEndIndex forward too so the loop in the // caller can look for the right ends in the next iteration for (nEndIndex = 0; nEndIndex < pEndHints->Count(); ++nEndIndex) { SwTextAttr *const pAttr(pEndHints->GetSortedByEnd(nEndIndex)); if (!pAttr->End()) continue; if (pRLEnd->nContent.GetIndex() < *pAttr->End()) { break; } } } } // if we didn't find a matching start for any end, then it really ends inside if (!activeCharFmts.empty()) { if (!isTheAnswerYes) return false; } for (size_t i = 0; i < SAL_N_ELEMENTS(activeCharAttrsStart); ++i) { // all of these are poolable // assert(!activeCharAttrsStart[i] || activeCharAttrsStart[i]->GetItemPool()->IsItemPoolable(*activeCharAttrsStart[i])); if (activeCharAttrsStart[i] != activeCharAttrsEnd[i]) { if (!isTheAnswerYes) return false; } } rStartIndex = nStartIndex; rEndIndex = nEndIndex; return true; } static sal_Int32 GetNextAttrImpl(SwTextNode const*const pTextNode, size_t const nStartIndex, size_t const nEndIndex, sal_Int32 const nPosition) { // note: this used to be COMPLETE_STRING, but was set to Len() + 1 below, // which is rather silly, so set it to Len() instead sal_Int32 nNext = pTextNode->Len(); if (SwpHints const*const pHints = pTextNode->GetpSwpHints()) { // are there attribute starts left? for (size_t i = nStartIndex; i < pHints->Count(); ++i) { SwTextAttr *const pAttr(pHints->Get(i)); if (!pAttr->IsFormatIgnoreStart()) { nNext = pAttr->GetStart(); break; } } // are there attribute ends left? for (size_t i = nEndIndex; i < pHints->Count(); ++i) { SwTextAttr *const pAttr(pHints->GetSortedByEnd(i)); if (!pAttr->IsFormatIgnoreEnd()) { sal_Int32 const nNextEnd = pAttr->GetAnyEnd(); nNext = std::min(nNext, nNextEnd); // pick nearest one break; } } } // TODO: maybe use hints like FieldHints for this instead of looking at the text... const sal_Int32 l = std::min(nNext, pTextNode->Len()); sal_Int32 p = nPosition; const sal_Unicode* pStr = pTextNode->GetText().getStr(); while (p < l) { sal_Unicode aChar = pStr[p]; switch (aChar) { case CH_TXT_ATR_FORMELEMENT: case CH_TXT_ATR_FIELDSTART: case CH_TXT_ATR_FIELDSEP: case CH_TXT_ATR_FIELDEND: goto break_; // sigh... default: ++p; } } break_: assert(p <= nNext); if (p < l) { // found a CH_TXT_ATR_FIELD*: if it's same as current position, // skip behind it so that both before- and after-positions are returned nNext = (nPosition < p) ? p : p + 1; } return nNext; } TextFrameIndex SwAttrIter::GetNextAttr() const { size_t nStartIndex(m_nStartIndex); size_t nEndIndex(m_nEndIndex); size_t nPosition(m_nPosition); SwTextNode const* pTextNode(m_pTextNode); SwRedlineTable::size_type nActRedline(m_pRedline ? m_pRedline->GetAct() : SwRedlineTable::npos); while (true) { sal_Int32 nNext = GetNextAttrImpl(pTextNode, nStartIndex, nEndIndex, nPosition); if( m_pRedline ) { std::pair> const redline( m_pRedline->GetNextRedln(nNext, pTextNode, nActRedline)); if (redline.second.first) { assert(m_pMergedPara); assert(redline.second.first->End()->nNode.GetIndex() <= m_pMergedPara->pLastNode->GetIndex() || !redline.second.first->End()->nNode.GetNode().IsTextNode()); if (CanSkipOverRedline(*pTextNode, redline.first, *redline.second.first, nStartIndex, nEndIndex, m_nPosition == redline.first)) { // if current position is start of the redline, must skip! nActRedline += redline.second.second; if (&redline.second.first->End()->nNode.GetNode() != pTextNode) { pTextNode = redline.second.first->End()->nNode.GetNode().GetTextNode(); nPosition = redline.second.first->End()->nContent.GetIndex(); } else { nPosition = redline.second.first->End()->nContent.GetIndex(); } } else { return sw::MapModelToView(*m_pMergedPara, pTextNode, redline.first); } } else { return m_pMergedPara ? sw::MapModelToView(*m_pMergedPara, pTextNode, redline.first) : TextFrameIndex(redline.first); } } else { return TextFrameIndex(nNext); } } } namespace { class SwMinMaxArgs { public: VclPtr pOut; SwViewShell const * pSh; sal_uLong &rMin; sal_uLong &rAbsMin; long nRowWidth; long nWordWidth; long nWordAdd; sal_Int32 nNoLineBreak; SwMinMaxArgs( OutputDevice* pOutI, SwViewShell const * pShI, sal_uLong& rMinI, sal_uLong &rAbsI ) : pOut( pOutI ), pSh( pShI ), rMin( rMinI ), rAbsMin( rAbsI ), nRowWidth(0), nWordWidth(0), nWordAdd(0), nNoLineBreak(COMPLETE_STRING) { } void Minimum( long nNew ) const { if( static_cast(rMin) < nNew ) rMin = nNew; } void NewWord() { nWordAdd = nWordWidth = 0; } }; } static bool lcl_MinMaxString( SwMinMaxArgs& rArg, SwFont* pFnt, const OUString &rText, sal_Int32 nIdx, sal_Int32 nEnd ) { bool bRet = false; while( nIdx < nEnd ) { sal_Int32 nStop = nIdx; LanguageType eLang = pFnt->GetLanguage(); assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); bool bClear = CH_BLANK == rText[ nStop ]; Boundary aBndry( g_pBreakIt->GetBreakIter()->getWordBoundary( rText, nIdx, g_pBreakIt->GetLocale( eLang ), WordType::DICTIONARY_WORD, true ) ); nStop = aBndry.endPos; if( nIdx <= aBndry.startPos && nIdx && nIdx-1 != rArg.nNoLineBreak ) rArg.NewWord(); if( nStop == nIdx ) ++nStop; if( nStop > nEnd ) nStop = nEnd; SwDrawTextInfo aDrawInf(rArg.pSh, *rArg.pOut, rText, nIdx, nStop - nIdx); long nCurrentWidth = pFnt->GetTextSize_( aDrawInf ).Width(); rArg.nRowWidth += nCurrentWidth; if( bClear ) rArg.NewWord(); else { rArg.nWordWidth += nCurrentWidth; if( static_cast(rArg.rAbsMin) < rArg.nWordWidth ) rArg.rAbsMin = rArg.nWordWidth; rArg.Minimum( rArg.nWordWidth + rArg.nWordAdd ); bRet = true; } nIdx = nStop; } return bRet; } bool SwTextNode::IsSymbolAt(const sal_Int32 nBegin) const { SwScriptInfo aScriptInfo; SwAttrIter aIter( *const_cast(this), aScriptInfo ); aIter.Seek( TextFrameIndex(nBegin) ); return aIter.GetFnt()->IsSymbol( getIDocumentLayoutAccess().GetCurrentViewShell() ); } namespace { class SwMinMaxNodeArgs { public: sal_uLong nMaxWidth; // sum of all frame widths long nMinWidth; // biggest frame long nLeftRest; // space not already covered by frames in the left margin long nRightRest; // space not already covered by frames in the right margin long nLeftDiff; // Min/Max-difference of the frame in the left margin long nRightDiff; // Min/Max-difference of the frame in the right margin sal_uLong nIndx; // index of the node void Minimum( long nNew ) { if( nNew > nMinWidth ) nMinWidth = nNew; } }; } static void lcl_MinMaxNode( SwFrameFormat* pNd, SwMinMaxNodeArgs* pIn ) { const SwFormatAnchor& rFormatA = pNd->GetAnchor(); if ((RndStdIds::FLY_AT_PARA != rFormatA.GetAnchorId()) && (RndStdIds::FLY_AT_CHAR != rFormatA.GetAnchorId())) { return; } const SwPosition *pPos = rFormatA.GetContentAnchor(); OSL_ENSURE(pPos && pIn, "Unexpected NULL arguments"); if (!pPos || !pIn || pIn->nIndx != pPos->nNode.GetIndex()) return; long nMin, nMax; SwHTMLTableLayout *pLayout = nullptr; const bool bIsDrawFrameFormat = pNd->Which()==RES_DRAWFRMFMT; if( !bIsDrawFrameFormat ) { // Does the frame contain a table at the start or the end? const SwNodes& rNodes = pNd->GetDoc()->GetNodes(); const SwFormatContent& rFlyContent = pNd->GetContent(); sal_uLong nStt = rFlyContent.GetContentIdx()->GetIndex(); SwTableNode* pTableNd = rNodes[nStt+1]->GetTableNode(); if( !pTableNd ) { SwNode *pNd2 = rNodes[nStt]; pNd2 = rNodes[pNd2->EndOfSectionIndex()-1]; if( pNd2->IsEndNode() ) pTableNd = pNd2->StartOfSectionNode()->GetTableNode(); } if( pTableNd ) pLayout = pTableNd->GetTable().GetHTMLTableLayout(); } const SwFormatHoriOrient& rOrient = pNd->GetHoriOrient(); sal_Int16 eHoriOri = rOrient.GetHoriOrient(); long nDiff; if( pLayout ) { nMin = pLayout->GetMin(); nMax = pLayout->GetMax(); nDiff = nMax - nMin; } else { if( bIsDrawFrameFormat ) { const SdrObject* pSObj = pNd->FindSdrObject(); if( pSObj ) nMin = pSObj->GetCurrentBoundRect().GetWidth(); else nMin = 0; } else { const SwFormatFrameSize &rSz = pNd->GetFrameSize(); nMin = rSz.GetWidth(); } nMax = nMin; nDiff = 0; } const SvxLRSpaceItem &rLR = pNd->GetLRSpace(); nMin += rLR.GetLeft(); nMin += rLR.GetRight(); nMax += rLR.GetLeft(); nMax += rLR.GetRight(); if( css::text::WrapTextMode_THROUGH == pNd->GetSurround().GetSurround() ) { pIn->Minimum( nMin ); return; } // Frames, which are left- or right-aligned are only party considered // when calculating the maximum, since the border is already being considered. // Only if the frame extends into the text body, this part is being added switch( eHoriOri ) { case text::HoriOrientation::RIGHT: { if( nDiff ) { pIn->nRightRest -= pIn->nRightDiff; pIn->nRightDiff = nDiff; } if( text::RelOrientation::FRAME != rOrient.GetRelationOrient() ) { if( pIn->nRightRest > 0 ) pIn->nRightRest = 0; } pIn->nRightRest -= nMin; break; } case text::HoriOrientation::LEFT: { if( nDiff ) { pIn->nLeftRest -= pIn->nLeftDiff; pIn->nLeftDiff = nDiff; } if( text::RelOrientation::FRAME != rOrient.GetRelationOrient() && pIn->nLeftRest < 0 ) pIn->nLeftRest = 0; pIn->nLeftRest -= nMin; break; } default: { pIn->nMaxWidth += nMax; pIn->Minimum( nMin ); } } } #define FLYINCNT_MIN_WIDTH 284 /** * Changing this method very likely requires changing of GetScalingOfSelectedText * This one is called exclusively from import filters, so there is no layout. */ void SwTextNode::GetMinMaxSize( sal_uLong nIndex, sal_uLong& rMin, sal_uLong &rMax, sal_uLong& rAbsMin ) const { SwViewShell const * pSh = GetDoc()->getIDocumentLayoutAccess().GetCurrentViewShell(); OutputDevice* pOut = nullptr; if( pSh ) pOut = pSh->GetWin(); if( !pOut ) pOut = Application::GetDefaultDevice(); MapMode aOldMap( pOut->GetMapMode() ); pOut->SetMapMode( MapMode( MapUnit::MapTwip ) ); rMin = 0; rMax = 0; rAbsMin = 0; const SvxLRSpaceItem &rSpace = GetSwAttrSet().GetLRSpace(); long nLROffset = rSpace.GetTextLeft() + GetLeftMarginWithNum( true ); short nFLOffs; // For enumerations a negative first line indentation is probably filled already if( !GetFirstLineOfsWithNum( nFLOffs ) || nFLOffs > nLROffset ) nLROffset = nFLOffs; SwMinMaxNodeArgs aNodeArgs; aNodeArgs.nMinWidth = 0; aNodeArgs.nMaxWidth = 0; aNodeArgs.nLeftRest = nLROffset; aNodeArgs.nRightRest = rSpace.GetRight(); aNodeArgs.nLeftDiff = 0; aNodeArgs.nRightDiff = 0; if( nIndex ) { SwFrameFormats* pTmp = const_cast(GetDoc()->GetSpzFrameFormats()); if( pTmp ) { aNodeArgs.nIndx = nIndex; for( SwFrameFormat *pFormat : *pTmp ) lcl_MinMaxNode( pFormat, &aNodeArgs ); } } if( aNodeArgs.nLeftRest < 0 ) aNodeArgs.Minimum( nLROffset - aNodeArgs.nLeftRest ); aNodeArgs.nLeftRest -= aNodeArgs.nLeftDiff; if( aNodeArgs.nLeftRest < 0 ) aNodeArgs.nMaxWidth -= aNodeArgs.nLeftRest; if( aNodeArgs.nRightRest < 0 ) aNodeArgs.Minimum( rSpace.GetRight() - aNodeArgs.nRightRest ); aNodeArgs.nRightRest -= aNodeArgs.nRightDiff; if( aNodeArgs.nRightRest < 0 ) aNodeArgs.nMaxWidth -= aNodeArgs.nRightRest; SwScriptInfo aScriptInfo; SwAttrIter aIter( *const_cast(this), aScriptInfo ); TextFrameIndex nIdx(0); aIter.SeekAndChgAttrIter( nIdx, pOut ); TextFrameIndex nLen(m_Text.getLength()); long nCurrentWidth = 0; long nAdd = 0; SwMinMaxArgs aArg( pOut, pSh, rMin, rAbsMin ); while( nIdx < nLen ) { TextFrameIndex nNextChg = aIter.GetNextAttr(); TextFrameIndex nStop = aScriptInfo.NextScriptChg( nIdx ); if( nNextChg > nStop ) nNextChg = nStop; SwTextAttr *pHint = nullptr; sal_Unicode cChar = CH_BLANK; nStop = nIdx; while( nStop < nLen && nStop < nNextChg && CH_TAB != (cChar = m_Text[sal_Int32(nStop)]) && CH_BREAK != cChar && CHAR_HARDBLANK != cChar && CHAR_HARDHYPHEN != cChar && CHAR_SOFTHYPHEN != cChar && CH_TXT_ATR_INPUTFIELDSTART != cChar && CH_TXT_ATR_INPUTFIELDEND != cChar && CH_TXT_ATR_FORMELEMENT != cChar && CH_TXT_ATR_FIELDSTART != cChar && CH_TXT_ATR_FIELDSEP != cChar && CH_TXT_ATR_FIELDEND != cChar && !pHint ) { // this looks like some defensive programming to handle dummy char // with missing hint? but it's rather silly because it may pass the // dummy char to lcl_MinMaxString in that case... if( ( CH_TXTATR_BREAKWORD != cChar && CH_TXTATR_INWORD != cChar ) || ( nullptr == ( pHint = aIter.GetAttr( nStop ) ) ) ) ++nStop; } if (lcl_MinMaxString(aArg, aIter.GetFnt(), m_Text, sal_Int32(nIdx), sal_Int32(nStop))) { nAdd = 20; } nIdx = nStop; aIter.SeekAndChgAttrIter( nIdx, pOut ); switch( cChar ) { case CH_BREAK : { if( static_cast(rMax) < aArg.nRowWidth ) rMax = aArg.nRowWidth; aArg.nRowWidth = 0; aArg.NewWord(); aIter.SeekAndChgAttrIter( ++nIdx, pOut ); } break; case CH_TAB : { aArg.NewWord(); aIter.SeekAndChgAttrIter( ++nIdx, pOut ); } break; case CHAR_SOFTHYPHEN: ++nIdx; break; case CHAR_HARDBLANK: case CHAR_HARDHYPHEN: { OUString sTmp( cChar ); SwDrawTextInfo aDrawInf( pSh, *pOut, sTmp, 0, 1, 0, false ); nCurrentWidth = aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); aArg.nWordWidth += nCurrentWidth; aArg.nRowWidth += nCurrentWidth; if( static_cast(rAbsMin) < aArg.nWordWidth ) rAbsMin = aArg.nWordWidth; aArg.Minimum( aArg.nWordWidth + aArg.nWordAdd ); aArg.nNoLineBreak = sal_Int32(nIdx++); } break; case CH_TXTATR_BREAKWORD: case CH_TXTATR_INWORD: { if( !pHint ) break; long nOldWidth = aArg.nWordWidth; long nOldAdd = aArg.nWordAdd; aArg.NewWord(); switch( pHint->Which() ) { case RES_TXTATR_FLYCNT : { SwFrameFormat *pFrameFormat = pHint->GetFlyCnt().GetFrameFormat(); const SvxLRSpaceItem &rLR = pFrameFormat->GetLRSpace(); if( RES_DRAWFRMFMT == pFrameFormat->Which() ) { const SdrObject* pSObj = pFrameFormat->FindSdrObject(); if( pSObj ) nCurrentWidth = pSObj->GetCurrentBoundRect().GetWidth(); else nCurrentWidth = 0; } else { const SwFormatFrameSize& rTmpSize = pFrameFormat->GetFrameSize(); if( RES_FLYFRMFMT == pFrameFormat->Which() && rTmpSize.GetWidthPercent() ) { // This is a hack for the following situation: In the paragraph there's a // text frame with relative size. Then let's take 0.5 cm as minimum width // and USHRT_MAX as maximum width // It were cleaner and maybe necessary later on to iterate over the content // of the text frame and call GetMinMaxSize recursively nCurrentWidth = FLYINCNT_MIN_WIDTH; // 0.5 cm rMax = std::max(rMax, sal_uLong(USHRT_MAX)); } else nCurrentWidth = pFrameFormat->GetFrameSize().GetWidth(); } nCurrentWidth += rLR.GetLeft(); nCurrentWidth += rLR.GetRight(); aArg.nWordAdd = nOldWidth + nOldAdd; aArg.nWordWidth = nCurrentWidth; aArg.nRowWidth += nCurrentWidth; if( static_cast(rAbsMin) < aArg.nWordWidth ) rAbsMin = aArg.nWordWidth; aArg.Minimum( aArg.nWordWidth + aArg.nWordAdd ); break; } case RES_TXTATR_FTN : { const OUString aText = pHint->GetFootnote().GetNumStr(); if( lcl_MinMaxString( aArg, aIter.GetFnt(), aText, 0, aText.getLength() ) ) nAdd = 20; break; } case RES_TXTATR_FIELD : case RES_TXTATR_ANNOTATION : { SwField *pField = const_cast(pHint->GetFormatField().GetField()); const OUString aText = pField->ExpandField(true, nullptr); if( lcl_MinMaxString( aArg, aIter.GetFnt(), aText, 0, aText.getLength() ) ) nAdd = 20; break; } default: aArg.nWordWidth = nOldWidth; aArg.nWordAdd = nOldAdd; } aIter.SeekAndChgAttrIter( ++nIdx, pOut ); } break; case CH_TXT_ATR_INPUTFIELDSTART: case CH_TXT_ATR_INPUTFIELDEND: case CH_TXT_ATR_FORMELEMENT: case CH_TXT_ATR_FIELDSTART: case CH_TXT_ATR_FIELDSEP: case CH_TXT_ATR_FIELDEND: { // just skip it and continue with the content... aIter.SeekAndChgAttrIter( ++nIdx, pOut ); } break; } } if( static_cast(rMax) < aArg.nRowWidth ) rMax = aArg.nRowWidth; nLROffset += rSpace.GetRight(); rAbsMin += nLROffset; rAbsMin += nAdd; rMin += nLROffset; rMin += nAdd; if( static_cast(rMin) < aNodeArgs.nMinWidth ) rMin = aNodeArgs.nMinWidth; if( static_cast(rAbsMin) < aNodeArgs.nMinWidth ) rAbsMin = aNodeArgs.nMinWidth; rMax += aNodeArgs.nMaxWidth; rMax += nLROffset; rMax += nAdd; if( rMax < rMin ) // e.g. Frames with flow through only contribute to the minimum rMax = rMin; pOut->SetMapMode( aOldMap ); } /** * Calculates the width of the text part specified by nStart and nEnd, * the height of the line containing nStart is divided by this width, * indicating the scaling factor, if the text part is rotated. * Having CH_BREAKs in the text part, this method returns the scaling * factor for the longest of the text parts separated by the CH_BREAK * * Changing this method very likely requires changing of "GetMinMaxSize" */ sal_uInt16 SwTextFrame::GetScalingOfSelectedText( TextFrameIndex nStart, TextFrameIndex nEnd) { assert(GetOffset() <= nStart && (!GetFollow() || nStart < GetFollow()->GetOffset())); SwViewShell const*const pSh = getRootFrame()->GetCurrShell(); assert(pSh); OutputDevice *const pOut = &pSh->GetRefDev(); assert(pOut); MapMode aOldMap( pOut->GetMapMode() ); pOut->SetMapMode( MapMode( MapUnit::MapTwip ) ); if (nStart == nEnd) { assert(g_pBreakIt && g_pBreakIt->GetBreakIter().is()); SwScriptInfo aScriptInfo; SwAttrIter aIter(*GetTextNodeFirst(), aScriptInfo, this); aIter.SeekAndChgAttrIter( nStart, pOut ); Boundary aBound = g_pBreakIt->GetBreakIter()->getWordBoundary( GetText(), sal_Int32(nStart), g_pBreakIt->GetLocale( aIter.GetFnt()->GetLanguage() ), WordType::DICTIONARY_WORD, true ); if (sal_Int32(nStart) == aBound.startPos) { // cursor is at left or right border of word pOut->SetMapMode( aOldMap ); return 100; } nStart = TextFrameIndex(aBound.startPos); nEnd = TextFrameIndex(aBound.endPos); if (nStart == nEnd) { pOut->SetMapMode( aOldMap ); return 100; } } SwScriptInfo aScriptInfo; SwAttrIter aIter(*GetTextNodeFirst(), aScriptInfo, this); // We do not want scaling attributes to be considered during this // calculation. For this, we push a temporary scaling attribute with // scaling value 100 and priority flag on top of the scaling stack SwAttrHandler& rAH = aIter.GetAttrHandler(); SvxCharScaleWidthItem aItem(100, RES_CHRATR_SCALEW); SwTextAttrEnd aAttr( aItem, 0, COMPLETE_STRING ); aAttr.SetPriorityAttr( true ); rAH.PushAndChg( aAttr, *(aIter.GetFnt()) ); TextFrameIndex nIdx = nStart; sal_uLong nWidth = 0; sal_uLong nProWidth = 0; while( nIdx < nEnd ) { aIter.SeekAndChgAttrIter( nIdx, pOut ); // scan for end of portion TextFrameIndex const nNextChg = std::min(aIter.GetNextAttr(), aScriptInfo.NextScriptChg(nIdx)); TextFrameIndex nStop = nIdx; sal_Unicode cChar = CH_BLANK; SwTextAttr* pHint = nullptr; // stop at special characters in [ nIdx, nNextChg ] while( nStop < nEnd && nStop < nNextChg ) { cChar = GetText()[sal_Int32(nStop)]; if ( CH_TAB == cChar || CH_BREAK == cChar || CHAR_HARDBLANK == cChar || CHAR_HARDHYPHEN == cChar || CHAR_SOFTHYPHEN == cChar || CH_TXT_ATR_INPUTFIELDSTART == cChar || CH_TXT_ATR_INPUTFIELDEND == cChar || CH_TXT_ATR_FORMELEMENT == cChar || CH_TXT_ATR_FIELDSTART == cChar || CH_TXT_ATR_FIELDSEP == cChar || CH_TXT_ATR_FIELDEND == cChar || ( (CH_TXTATR_BREAKWORD == cChar || CH_TXTATR_INWORD == cChar) && (nullptr == (pHint = aIter.GetAttr(nStop))) ) ) { break; } else ++nStop; } // calculate text widths up to cChar if ( nStop > nIdx ) { SwDrawTextInfo aDrawInf(pSh, *pOut, GetText(), sal_Int32(nIdx), sal_Int32(nStop - nIdx)); nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); } nIdx = nStop; aIter.SeekAndChgAttrIter( nIdx, pOut ); if ( cChar == CH_BREAK ) { nWidth = std::max( nWidth, nProWidth ); nProWidth = 0; nIdx++; } else if ( cChar == CH_TAB ) { // tab receives width of one space OUString sTmp( CH_BLANK ); SwDrawTextInfo aDrawInf(pSh, *pOut, sTmp, 0, 1); nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); nIdx++; } else if ( cChar == CHAR_SOFTHYPHEN ) ++nIdx; else if ( cChar == CHAR_HARDBLANK || cChar == CHAR_HARDHYPHEN ) { OUString sTmp( cChar ); SwDrawTextInfo aDrawInf(pSh, *pOut, sTmp, 0, 1); nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); nIdx++; } else if ( pHint && ( cChar == CH_TXTATR_BREAKWORD || cChar == CH_TXTATR_INWORD ) ) { switch( pHint->Which() ) { case RES_TXTATR_FTN : { const OUString aText = pHint->GetFootnote().GetNumStr(); SwDrawTextInfo aDrawInf(pSh, *pOut, aText, 0, aText.getLength()); nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); break; } case RES_TXTATR_FIELD : case RES_TXTATR_ANNOTATION : { SwField *pField = const_cast(pHint->GetFormatField().GetField()); OUString const aText = pField->ExpandField(true, getRootFrame()); SwDrawTextInfo aDrawInf(pSh, *pOut, aText, 0, aText.getLength()); nProWidth += aIter.GetFnt()->GetTextSize_( aDrawInf ).Width(); break; } default: { // any suggestions for a default action? } } // end of switch nIdx++; } else if (CH_TXT_ATR_INPUTFIELDSTART == cChar || CH_TXT_ATR_INPUTFIELDEND == cChar || CH_TXT_ATR_FORMELEMENT == cChar || CH_TXT_ATR_FIELDSTART == cChar || CH_TXT_ATR_FIELDSEP == cChar || CH_TXT_ATR_FIELDEND == cChar) { // just skip it and continue with the content... ++nIdx; } } // end of while nWidth = std::max( nWidth, nProWidth ); // search for the line containing nStart if (HasPara()) { SwTextInfo aInf(this); SwTextIter aLine(this, &aInf); aLine.CharToLine( nStart ); pOut->SetMapMode( aOldMap ); return static_cast( nWidth ? ( ( 100 * aLine.GetCurr()->Height() ) / nWidth ) : 0 ); } // no frame or no paragraph, we take the height of the character // at nStart as line height aIter.SeekAndChgAttrIter( nStart, pOut ); pOut->SetMapMode( aOldMap ); SwDrawTextInfo aDrawInf(pSh, *pOut, GetText(), sal_Int32(nStart), 1); return static_cast( nWidth ? ((100 * aIter.GetFnt()->GetTextSize_( aDrawInf ).Height()) / nWidth ) : 0 ); } SwTwips SwTextNode::GetWidthOfLeadingTabs() const { SwTwips nRet = 0; sal_Int32 nIdx = 0; while ( nIdx < GetText().getLength() ) { const sal_Unicode cCh = GetText()[nIdx]; if ( cCh!='\t' && cCh!=' ' ) { break; } ++nIdx; } if ( nIdx > 0 ) { SwPosition aPos( *this ); aPos.nContent += nIdx; // Find the non-follow text frame: SwIterator aIter(*this); for( SwTextFrame* pFrame = aIter.First(); pFrame; pFrame = aIter.Next() ) { // Only consider master frames: if (!pFrame->IsFollow() && pFrame->GetTextNodeForFirstText() == this) { SwRectFnSet aRectFnSet(pFrame); SwRect aRect; pFrame->GetCharRect( aRect, aPos ); nRet = pFrame->IsRightToLeft() ? aRectFnSet.GetPrtRight(*pFrame) - aRectFnSet.GetRight(aRect) : aRectFnSet.GetLeft(aRect) - aRectFnSet.GetPrtLeft(*pFrame); break; } } } return nRet; } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */