1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include <sal/types.h>
21 #include <boost/ptr_container/ptr_vector.hpp>
22 #include "tools/debug.hxx"
24 #include "ctfonts.hxx"
25 #include "CTRunData.hxx"
28 class CTLayout
: public SalLayout
31 explicit CTLayout( const CoreTextStyle
* );
32 virtual ~CTLayout( void );
34 virtual bool LayoutText( ImplLayoutArgs
& ) SAL_OVERRIDE
;
35 virtual void AdjustLayout( ImplLayoutArgs
& ) SAL_OVERRIDE
;
36 virtual void DrawText( SalGraphics
& ) const SAL_OVERRIDE
;
37 virtual bool DrawTextSpecial( SalGraphics
& rGraphics
, sal_uInt32 flags
) const SAL_OVERRIDE
;
39 virtual int GetNextGlyphs( int nLen
, sal_GlyphId
* pOutGlyphIds
, Point
& rPos
, int&,
40 sal_Int32
* pGlyphAdvances
, int* pCharIndexes
,
41 const PhysicalFontFace
** pFallbackFonts
) const SAL_OVERRIDE
;
43 virtual long GetTextWidth() const SAL_OVERRIDE
;
44 virtual long FillDXArray( sal_Int32
* pDXArray
) const SAL_OVERRIDE
;
45 virtual sal_Int32
GetTextBreak(long nMaxWidth
, long nCharExtra
, int nFactor
) const SAL_OVERRIDE
;
46 virtual void GetCaretPositions( int nArraySize
, sal_Int32
* pCaretXArray
) const SAL_OVERRIDE
;
47 virtual bool GetBoundRect( SalGraphics
&, Rectangle
& ) const SAL_OVERRIDE
;
49 virtual void InitFont( void) const SAL_OVERRIDE
;
50 virtual void MoveGlyph( int nStart
, long nNewXPos
) SAL_OVERRIDE
;
51 virtual void DropGlyph( int nStart
) SAL_OVERRIDE
;
52 virtual void Simplify( bool bIsBase
) SAL_OVERRIDE
;
55 void drawCTLine(AquaSalGraphics
& rAquaGraphics
, CTLineRef ctline
, const CoreTextStyle
* const pStyle
) const;
56 CGPoint
GetTextDrawPosition(void) const;
57 double GetWidth(void) const;
58 bool CacheGlyphLayout(void) const;
60 const CoreTextStyle
* const mpTextStyle
;
62 // CoreText specific objects
63 CFAttributedStringRef mpAttrString
;
66 int mnCharCount
; // ==mnEndCharPos-mnMinCharPos
67 int mnTrailingSpaceCount
;
68 double mfTrailingSpaceWidth
;
70 // cached details about the resulting layout
71 // mutable members since these details are all lazy initialized
72 mutable double mfCachedWidth
; // cached value of resulting typographical width
74 // x-offset relative to layout origin
75 // currently only used in RTL-layouts
76 mutable double mfBaseAdv
;
78 mutable bool bLayouted
; // true if the glyph layout information are cached and current;
79 mutable boost::ptr_vector
<CTRunData
> m_vRunData
;
83 CTLayout::CTLayout( const CoreTextStyle
* pTextStyle
)
84 : mpTextStyle( pTextStyle
)
85 , mpAttrString( NULL
)
88 , mnTrailingSpaceCount( 0 )
89 , mfTrailingSpaceWidth( 0.0 )
99 CFRelease( mpCTLine
);
101 CFRelease( mpAttrString
);
104 bool CTLayout::LayoutText( ImplLayoutArgs
& rArgs
)
106 m_vRunData
.release();
110 CFRelease( mpAttrString
);
113 CFRelease( mpCTLine
);
116 SalLayout::AdjustLayout( rArgs
);
117 mnCharCount
= mnEndCharPos
- mnMinCharPos
;
119 // short circuit if there is nothing to do
120 if( mnCharCount
<= 0 )
123 // create the CoreText line layout
124 CFStringRef aCFText
= CFStringCreateWithCharactersNoCopy( NULL
,
125 rArgs
.mpStr
+ mnMinCharPos
,
128 // CFAttributedStringCreate copies the attribues parameter
129 mpAttrString
= CFAttributedStringCreate( NULL
, aCFText
, mpTextStyle
->GetStyleDict() );
130 mpCTLine
= CTLineCreateWithAttributedString( mpAttrString
);
133 mnTrailingSpaceCount
= 0;
134 // reverse search for first 'non-space'...
135 for( int i
= mnEndCharPos
- 1; i
>= mnMinCharPos
; i
--)
137 sal_Unicode nChar
= rArgs
.mpStr
[i
];
138 if ((nChar
<= 0x0020) || // blank
139 (nChar
== 0x00A0) || // non breaking space
140 (nChar
>= 0x2000 && nChar
<= 0x200F) || // whitespace
141 (nChar
== 0x3000)) // ideographic space
143 mnTrailingSpaceCount
+= 1;
153 void CTLayout::AdjustLayout( ImplLayoutArgs
& rArgs
)
158 int nPixelWidth
= rArgs
.mpDXArray
? rArgs
.mpDXArray
[ mnCharCount
- 1 ] : rArgs
.mnLayoutWidth
;
159 if( nPixelWidth
<= 0)
162 // HACK: justification requests which change the width by just one pixel are probably
163 // #i86038# introduced by lossy conversions between integer based coordinate system
164 int fuzz
= (nPixelWidth
- GetTextWidth()) / 2;
170 // if the text to be justified has whitespace in it then
171 // - Writer goes crazy with its HalfSpace magic
172 // - CoreText handles spaces specially (in particular at the text end)
173 if( mnTrailingSpaceCount
)
175 #if MAC_OS_X_VERSION_MAX_ALLOWED < 1060
176 // don't recreate line layout here, because this can lead to problems
177 // (looks like internal issues inside early CoreText versions)
178 mfTrailingSpaceWidth
= CTLineGetTrailingWhitespaceWidth( mpCTLine
);
182 int nFullPixelWidth
= nPixelWidth
;
183 nPixelWidth
= mnTrailingSpaceCount
== mnCharCount
184 ? 0 : rArgs
.mpDXArray
[ mnCharCount
- mnTrailingSpaceCount
- 1];
185 mfTrailingSpaceWidth
= nFullPixelWidth
- nPixelWidth
;
189 if(mfTrailingSpaceWidth
<= 0.0)
191 mfTrailingSpaceWidth
= CTLineGetTrailingWhitespaceWidth( mpCTLine
);
192 nPixelWidth
-= rint(mfTrailingSpaceWidth
);
199 // recreate the CoreText line layout without trailing spaces
200 CFRelease( mpCTLine
);
201 CFStringRef aCFText
= CFStringCreateWithCharactersNoCopy( NULL
,
202 rArgs
.mpStr
+ mnMinCharPos
,
203 mnCharCount
- mnTrailingSpaceCount
,
205 CFAttributedStringRef pAttrStr
= CFAttributedStringCreate( NULL
,
207 mpTextStyle
->GetStyleDict() );
208 mpCTLine
= CTLineCreateWithAttributedString( pAttrStr
);
209 CFRelease( pAttrStr
);
210 CFRelease( aCFText
);
213 // in RTL-layouts trailing spaces are leftmost
214 // TODO: use BiDi-algorithm to thoroughly check this assumption
215 if( rArgs
.mnFlags
& SAL_LAYOUT_BIDI_RTL
)
217 mfBaseAdv
= mfTrailingSpaceWidth
;
221 #if MAC_OS_X_VERSION_MAX_ALLOWED < 1060
222 CTLineRef pNewCTLine
= CTLineCreateJustifiedLine( mpCTLine
, 1.0, nPixelWidth
- mfTrailingSpaceWidth
);
224 CTLineRef pNewCTLine
= CTLineCreateJustifiedLine( mpCTLine
, 1.0, nPixelWidth
);
228 // CTLineCreateJustifiedLine can and does fail
229 // handle failure by keeping the unjustified layout
230 // TODO: a better solution such as
231 // - forcing glyph overlap
232 // - changing the font size
233 // - changing the CTM matrix
236 CFRelease( mpCTLine
);
237 mpCTLine
= pNewCTLine
;
238 #if MAC_OS_X_VERSION_MAX_ALLOWED < 1060
239 mfCachedWidth
= nPixelWidth
;
241 mfCachedWidth
= nPixelWidth
+ mfTrailingSpaceWidth
;
245 // When drawing right aligned text, rounding errors in the position returned by
246 // GetDrawPosition() cause the right margin of the text to change whenever text
247 // width changes causing "jumping letters" effect. So here we calculate the
248 // drawing position relative to the right margin on our own to avoid the
249 // rounding errors. That is basically a hack, and it should go away if one day
250 // we managed to get rid of those rounding errors.
252 // We continue using GetDrawPosition() for non-right aligned text, to minimize
253 // any unforeseen side effects.
254 CGPoint
CTLayout::GetTextDrawPosition(void) const
256 CGFloat fPosX
, fPosY
;
258 if (mnLayoutFlags
& SAL_LAYOUT_RIGHT_ALIGN
)
260 // text is always drawn at its leftmost point
261 const Point aPos
= DrawBase();
262 fPosX
= aPos
.X() + mfBaseAdv
- GetWidth();
267 const Point aPos
= GetDrawPosition(Point(mfBaseAdv
, 0));
272 CGPoint aTextPos
= { +fPosX
, -fPosY
};
276 /* use to deal with special font decoration like 'outline' drawing
277 * return true if it was able to handle the drawing
278 * false if not, in which case the caller
279 * is supposed to fallback to 'generic' method
281 bool CTLayout::DrawTextSpecial( SalGraphics
& rGraphics
, sal_uInt32 flags
) const
283 AquaSalGraphics
& rAquaGraphics
= static_cast<AquaSalGraphics
&>(rGraphics
);
285 // short circuit if there is nothing to do
286 if( (mnCharCount
<= 0) || !rAquaGraphics
.CheckContext() )
289 if (flags
& DRAWTEXT_F_OUTLINE
)
291 CFMutableDictionaryRef styledict
= CFDictionaryCreateMutableCopy(
292 CFAllocatorGetDefault(),
293 CFDictionaryGetCount(mpTextStyle
->GetStyleDict()),
294 mpTextStyle
->GetStyleDict());
296 #if MAC_OS_X_VERSION_MAX_ALLOWED < 1060
297 /* just don't do 'kCTStrokeWidthAttributeName' */
300 CFNumberRef rStroke
= CFNumberCreate(NULL
, kCFNumberSInt32Type
, &nStroke
);
301 CFDictionarySetValue(styledict
, kCTStrokeWidthAttributeName
, rStroke
);
304 CFAttributedStringRef pAttrStr
= CFAttributedStringCreate(
306 CFAttributedStringGetString(mpAttrString
),
308 CTLineRef pCTLine
= CTLineCreateWithAttributedString( pAttrStr
);
309 CFRelease( pAttrStr
);
311 /* draw the text in 'outline' */
312 drawCTLine(rAquaGraphics
, pCTLine
, mpTextStyle
);
318 return SalLayout::DrawTextSpecial(rGraphics
, flags
);
322 void CTLayout::drawCTLine(AquaSalGraphics
& rAquaGraphics
, CTLineRef ctline
, const CoreTextStyle
* const pStyle
) const
324 // the view is vertically flipped => flipped glyphs
325 // so apply a temporary transformation that it flips back
326 // also compensate if the font was size limited
327 CGContextSaveGState( rAquaGraphics
.mrContext
);
328 CGContextScaleCTM( rAquaGraphics
.mrContext
, 1.0, -1.0 );
329 CGContextSetShouldAntialias( rAquaGraphics
.mrContext
, !rAquaGraphics
.mbNonAntialiasedText
);
332 CGPoint aTextPos
= GetTextDrawPosition();
334 if( pStyle
->mfFontRotation
!= 0.0 )
336 const CGFloat fRadians
= pStyle
->mfFontRotation
;
337 CGContextRotateCTM( rAquaGraphics
.mrContext
, +fRadians
);
339 const CGAffineTransform aInvMatrix
= CGAffineTransformMakeRotation( -fRadians
);
340 aTextPos
= CGPointApplyAffineTransform( aTextPos
, aInvMatrix
);
343 CGContextSetTextPosition( rAquaGraphics
.mrContext
, aTextPos
.x
, aTextPos
.y
);
344 CTLineDraw( ctline
, rAquaGraphics
.mrContext
);
346 // request an update of the changed window area
347 if( rAquaGraphics
.IsWindowGraphics() )
349 const CGRect aInkRect
= CTLineGetImageBounds( mpCTLine
, rAquaGraphics
.mrContext
);
350 const CGRect aRefreshRect
= CGContextConvertRectToDeviceSpace( rAquaGraphics
.mrContext
, aInkRect
);
351 rAquaGraphics
.RefreshRect( aRefreshRect
);
354 // restore the original graphic context transformations
355 CGContextRestoreGState( rAquaGraphics
.mrContext
);
358 void CTLayout::DrawText( SalGraphics
& rGraphics
) const
360 AquaSalGraphics
& rAquaGraphics
= static_cast<AquaSalGraphics
&>(rGraphics
);
362 // short circuit if there is nothing to do
363 if( (mnCharCount
<= 0) || !rAquaGraphics
.CheckContext() )
366 drawCTLine(rAquaGraphics
, mpCTLine
, mpTextStyle
);
369 bool CTLayout::CacheGlyphLayout(void) const // eew!
371 m_vRunData
.release();
376 CFArrayRef aRuns
= CTLineGetGlyphRuns( mpCTLine
);
377 const int nRun
= CFArrayGetCount( aRuns
);
380 for( int i
= 0; i
< nRun
; ++i
)
382 CTRunRef pRun
= (CTRunRef
)CFArrayGetValueAtIndex( aRuns
, i
);
383 CTRunData
* pRunData
= new CTRunData(pRun
, nPos
);
384 m_vRunData
.push_back(pRunData
);
385 nPos
+= pRunData
->m_nGlyphs
;
391 int CTLayout::GetNextGlyphs( int nLen
, sal_GlyphId
* pOutGlyphIds
, Point
& rPos
, int& nStart
,
392 sal_Int32
* pGlyphAdvances
, int* pCharIndexes
,
393 const PhysicalFontFace
** pFallbackFonts
) const
404 if( nStart
< 0 ) // first glyph requested?
408 const PhysicalFontFace
* pFallbackFont
= NULL
;
409 CTFontRef pFont
= NULL
;
410 CTFontDescriptorRef pFontDesc
= NULL
;
411 ImplDevFontAttributes rDevFontAttr
;
413 boost::ptr_vector
<CTRunData
>::const_iterator iter
= m_vRunData
.begin();
415 while(iter
!= m_vRunData
.end() && iter
->m_EndPos
<= nStart
)
419 if(iter
== m_vRunData
.end())
427 pFont
= (CTFontRef
)CFDictionaryGetValue( mpTextStyle
->GetStyleDict(), kCTFontAttributeName
);
428 pFontDesc
= CTFontCopyFontDescriptor( iter
->m_pFont
);
429 rDevFontAttr
= DevFontFromCTFontDescriptor( pFontDesc
, NULL
);
434 while( i
< nStart
+ nLen
)
436 // convert glyph details for VCL
437 int j
= i
- iter
->m_StartPos
;
438 *(pOutGlyphIds
++) = iter
->m_pGlyphs
[ j
];
441 *(pGlyphAdvances
++) = lrint(iter
->m_pAdvances
[ j
].width
);
445 *(pCharIndexes
++) = iter
->m_pStringIndices
[ j
] + mnMinCharPos
;
449 if ( !CFEqual( iter
->m_pFont
, pFont
) )
451 pFallbackFont
= new CoreTextFontData( rDevFontAttr
, (sal_IntPtr
)pFontDesc
);
452 *(pFallbackFonts
++) = pFallbackFont
;
456 *(pFallbackFonts
++) = NULL
;
461 const CGPoint
& rFirstPos
= iter
->m_pPositions
[ j
];
462 rPos
= GetDrawPosition( Point( rFirstPos
.x
, rFirstPos
.y
) );
466 if(i
== iter
->m_EndPos
)
468 // note: we assume that we do not have empty runs in the middle of things
470 if( iter
== m_vRunData
.end())
476 pFont
= (CTFontRef
)CFDictionaryGetValue( mpTextStyle
->GetStyleDict(), kCTFontAttributeName
);
477 pFontDesc
= CTFontCopyFontDescriptor( iter
->m_pFont
);
478 rDevFontAttr
= DevFontFromCTFontDescriptor( pFontDesc
, NULL
);
488 double CTLayout::GetWidth() const
490 if( (mnCharCount
<= 0) || !mpCTLine
)
493 if( mfCachedWidth
< 0.0 )
495 mfCachedWidth
= CTLineGetTypographicBounds( mpCTLine
, NULL
, NULL
, NULL
);
498 return mfCachedWidth
;
501 long CTLayout::GetTextWidth() const
503 return lrint(GetWidth());
506 long CTLayout::FillDXArray( sal_Int32
* pDXArray
) const
508 long nPixWidth
= GetTextWidth();
509 // short circuit requests which don't need full details
513 // prepare the sub-pixel accurate logical-width array
514 ::std::vector
<double> aWidthVector( mnCharCount
);
515 if( mnTrailingSpaceCount
&& (mfTrailingSpaceWidth
> 0.0) )
517 const double fOneWidth
= mfTrailingSpaceWidth
/ mnTrailingSpaceCount
;
518 ::std::fill_n(aWidthVector
.begin() + (mnCharCount
- mnTrailingSpaceCount
),
519 mnTrailingSpaceCount
,
523 // handle each glyph run
524 CFArrayRef aGlyphRuns
= CTLineGetGlyphRuns( mpCTLine
);
525 const int nRunCount
= CFArrayGetCount( aGlyphRuns
);
526 typedef std::vector
<CGSize
> CGSizeVector
;
527 CGSizeVector aSizeVec
;
528 typedef std::vector
<CFIndex
> CFIndexVector
;
529 CFIndexVector aIndexVec
;
531 for( int nRunIndex
= 0; nRunIndex
< nRunCount
; ++nRunIndex
)
533 CTRunRef pGlyphRun
= (CTRunRef
)CFArrayGetValueAtIndex( aGlyphRuns
, nRunIndex
);
534 const CFIndex nGlyphCount
= CTRunGetGlyphCount( pGlyphRun
);
535 const CFRange aFullRange
= CFRangeMake( 0, nGlyphCount
);
537 aSizeVec
.resize( nGlyphCount
);
538 aIndexVec
.resize( nGlyphCount
);
539 CTRunGetAdvances( pGlyphRun
, aFullRange
, &aSizeVec
[0] );
540 CTRunGetStringIndices( pGlyphRun
, aFullRange
, &aIndexVec
[0] );
542 for( int i
= 0; i
!= nGlyphCount
; ++i
)
544 const int nRelIdx
= aIndexVec
[i
];
545 aWidthVector
[nRelIdx
] += aSizeVec
[i
].width
;
549 // convert the sub-pixel accurate array into classic pDXArray integers
550 double fWidthSum
= 0.0;
551 sal_Int32 nOldDX
= 0;
552 for( int i
= 0; i
< mnCharCount
; ++i
)
554 const sal_Int32 nNewDX
= rint( fWidthSum
+= aWidthVector
[i
]);
555 pDXArray
[i
] = nNewDX
- nOldDX
;
562 sal_Int32
CTLayout::GetTextBreak( long nMaxWidth
, long /*nCharExtra*/, int nFactor
) const
567 CTTypesetterRef aCTTypeSetter
= CTTypesetterCreateWithAttributedString( mpAttrString
);
568 const double fCTMaxWidth
= (double)nMaxWidth
/ nFactor
;
569 CFIndex nIndex
= CTTypesetterSuggestClusterBreak( aCTTypeSetter
, 0, fCTMaxWidth
);
570 CFRelease( aCTTypeSetter
);
572 if( nIndex
>= mnCharCount
)
575 nIndex
+= mnMinCharPos
;
579 void CTLayout::GetCaretPositions( int nMaxIndex
, sal_Int32
* pCaretXArray
) const
581 DBG_ASSERT( ((nMaxIndex
>0)&&!(nMaxIndex
&1)),
582 "CTLayout::GetCaretPositions() : invalid number of caret pairs requested");
584 // initialize the caret positions
585 for( int i
= 0; i
< nMaxIndex
; ++i
)
587 pCaretXArray
[ i
] = -1;
589 for( int n
= 0; n
<= mnCharCount
; ++n
)
591 // measure the characters cursor position
593 const CGFloat fPos1
= CTLineGetOffsetForStringIndex( mpCTLine
, n
, &fPos2
);
594 (void)fPos2
; // TODO: split cursor at line direction change
596 // update previous trailing position
598 pCaretXArray
[ 2*n
-1 ] = lrint( fPos1
);
600 // update current leading position
601 if( 2*n
>= nMaxIndex
)
603 pCaretXArray
[ 2*n
+0 ] = lrint( fPos1
);
607 bool CTLayout::GetBoundRect( SalGraphics
& rGraphics
, Rectangle
& rVCLRect
) const
609 // Closely mimic DrawText(), except that instead of calling
610 // CTLineDraw() to draw the line, we call CTLineGetImageBounds()
611 // to get its bounds. But all the coordinate system manipulation
612 // before that is the same => should be factored out?
614 AquaSalGraphics
& rAquaGraphics
= static_cast<AquaSalGraphics
&>(rGraphics
);
616 if( !rAquaGraphics
.CheckContext() )
619 CGContextSaveGState( rAquaGraphics
.mrContext
);
620 CGContextScaleCTM( rAquaGraphics
.mrContext
, 1.0, -1.0 );
621 CGContextSetShouldAntialias( rAquaGraphics
.mrContext
, !rAquaGraphics
.mbNonAntialiasedText
);
623 const CGPoint aVclPos
= GetTextDrawPosition();
624 CGPoint aTextPos
= GetTextDrawPosition();
626 if( mpTextStyle
->mfFontRotation
!= 0.0 )
628 const CGFloat fRadians
= mpTextStyle
->mfFontRotation
;
629 CGContextRotateCTM( rAquaGraphics
.mrContext
, +fRadians
);
631 const CGAffineTransform aInvMatrix
= CGAffineTransformMakeRotation( -fRadians
);
632 aTextPos
= CGPointApplyAffineTransform( aTextPos
, aInvMatrix
);
635 CGContextSetTextPosition( rAquaGraphics
.mrContext
, aTextPos
.x
, aTextPos
.y
);
636 CGRect aMacRect
= CTLineGetImageBounds( mpCTLine
, rAquaGraphics
.mrContext
);
638 if( mpTextStyle
->mfFontRotation
!= 0.0 )
640 const CGFloat fRadians
= mpTextStyle
->mfFontRotation
;
641 const CGAffineTransform aMatrix
= CGAffineTransformMakeRotation( +fRadians
);
642 aMacRect
= CGRectApplyAffineTransform( aMacRect
, aMatrix
);
645 CGContextRestoreGState( rAquaGraphics
.mrContext
);
647 rVCLRect
.Left() = aVclPos
.x
+ lrint(aMacRect
.origin
.x
);
648 rVCLRect
.Right() = aVclPos
.x
+ lrint(aMacRect
.origin
.x
+ aMacRect
.size
.width
);
649 rVCLRect
.Bottom() = aVclPos
.x
- lrint(aMacRect
.origin
.y
);
650 rVCLRect
.Top() = aVclPos
.x
- lrint(aMacRect
.origin
.y
+ aMacRect
.size
.height
);
655 // glyph fallback is supported directly by Aqua
656 // so methods used only by MultiSalLayout can be dummy implementated
657 void CTLayout::InitFont() const {}
658 void CTLayout::MoveGlyph( int /*nStart*/, long /*nNewXPos*/ ) {}
659 void CTLayout::DropGlyph( int /*nStart*/ ) {}
660 void CTLayout::Simplify( bool /*bIsBase*/ ) {}
662 SalLayout
* CoreTextStyle::GetTextLayout( void ) const
664 return new CTLayout( this);
667 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */