1 /************************************************************** 2 * 3 * Licensed to the Apache Software Foundation (ASF) under one 4 * or more contributor license agreements. See the NOTICE file 5 * distributed with this work for additional information 6 * regarding copyright ownership. The ASF licenses this file 7 * to you under the Apache License, Version 2.0 (the 8 * "License"); you may not use this file except in compliance 9 * with the License. You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, 14 * software distributed under the License is distributed on an 15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 * KIND, either express or implied. See the License for the 17 * specific language governing permissions and limitations 18 * under the License. 19 * 20 *************************************************************/ 21 22 #include "ctfonts.hxx" 23 24 // ======================================================================= 25 26 class CTLayout 27 : public SalLayout 28 { 29 public: 30 explicit CTLayout( const CTTextStyle* ); 31 virtual ~CTLayout( void ); 32 33 virtual bool LayoutText( ImplLayoutArgs& ); 34 virtual void AdjustLayout( ImplLayoutArgs& ); 35 virtual void DrawText( SalGraphics& ) const; 36 37 virtual int GetNextGlyphs( int nLen, sal_GlyphId* pOutGlyphIds, Point& rPos, int&, 38 sal_Int32* pGlyphAdvances, int* pCharIndexes ) const; 39 40 virtual long GetTextWidth() const; 41 virtual long FillDXArray( sal_Int32* pDXArray ) const; 42 virtual int GetTextBreak( long nMaxWidth, long nCharExtra, int nFactor ) const; 43 virtual void GetCaretPositions( int nArraySize, sal_Int32* pCaretXArray ) const; 44 virtual bool GetGlyphOutlines( SalGraphics&, PolyPolyVector& ) const; 45 virtual bool GetBoundRect( SalGraphics&, Rectangle& ) const; 46 47 const ImplFontData* GetFallbackFontData( sal_GlyphId ) const; 48 49 virtual void InitFont( void) const; 50 virtual void MoveGlyph( int nStart, long nNewXPos ); 51 virtual void DropGlyph( int nStart ); 52 virtual void Simplify( bool bIsBase ); 53 54 private: 55 const CTTextStyle* const mpTextStyle; 56 57 // CoreText specific objects 58 CFAttributedStringRef mpAttrString; 59 CTLineRef mpCTLine; 60 61 int mnCharCount; // ==mnEndCharPos-mnMinCharPos 62 int mnTrailingSpaceCount; 63 double mfTrailingSpaceWidth; // preserves the width of stripped-off trailing space 64 65 // to prevent overflows 66 // font requests get size limited by downscaling huge fonts 67 // in these cases the font scale becomes something bigger than 1.0 68 float mfFontScale; // TODO: does CoreText have a font size limit? 69 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 73 74 // x-offset relative to layout origin 75 // currently only used in RTL-layouts 76 mutable long mnBaseAdv; 77 }; 78 79 // ======================================================================= 80 81 CTLayout::CTLayout( const CTTextStyle* pTextStyle ) 82 : mpTextStyle( pTextStyle ) 83 , mpAttrString( NULL ) 84 , mpCTLine( NULL ) 85 , mnCharCount( 0 ) 86 , mnTrailingSpaceCount( 0 ) 87 , mfTrailingSpaceWidth( 0.0 ) 88 , mfFontScale( pTextStyle->mfFontScale ) 89 , mfCachedWidth( -1 ) 90 , mnBaseAdv( 0 ) 91 { 92 CFRetain( mpTextStyle->GetStyleDict() ); 93 } 94 95 // ----------------------------------------------------------------------- 96 97 CTLayout::~CTLayout() 98 { 99 if( mpCTLine ) 100 CFRelease( mpCTLine ); 101 if( mpAttrString ) 102 CFRelease( mpAttrString ); 103 CFRelease( mpTextStyle->GetStyleDict() ); 104 } 105 106 // ----------------------------------------------------------------------- 107 108 bool CTLayout::LayoutText( ImplLayoutArgs& rArgs ) 109 { 110 if( mpAttrString ) 111 CFRelease( mpAttrString ); 112 mpAttrString = NULL; 113 if( mpCTLine ) 114 CFRelease( mpCTLine ); 115 mpCTLine = NULL; 116 117 SalLayout::AdjustLayout( rArgs ); 118 mnCharCount = mnEndCharPos - mnMinCharPos; 119 120 // short circuit if there is nothing to do 121 if( mnCharCount <= 0 ) 122 return false; 123 124 // prepare the string to be layouted by CoreText 125 CFStringRef aCFText = CFStringCreateWithCharactersNoCopy( NULL, rArgs.mpStr + mnMinCharPos, mnCharCount, kCFAllocatorNull ); 126 // #i124375# force soft-hyphen visibility to meet the expectations of Writer+EditEngine 127 if( CFStringFind( aCFText, (CFStringRef)@"\u00AD", 0).length > 0 ) 128 { 129 NSString* pDashStr = [(NSString*)aCFText stringByReplacingOccurrencesOfString: @"\u00AD" withString: @"-"]; 130 aCFText = CFStringCreateCopy( NULL, (CFStringRef)pDashStr ); 131 } 132 133 // create the CoreText line layout using the requested text style 134 mpAttrString = CFAttributedStringCreate( NULL, aCFText, mpTextStyle->GetStyleDict() ); 135 mpCTLine = CTLineCreateWithAttributedString( mpAttrString ); 136 CFRelease( aCFText); 137 138 // get info about trailing whitespace to prepare for text justification in AdjustLayout() 139 mnTrailingSpaceCount = 0; 140 for( int i = mnEndCharPos; --i >= mnMinCharPos; ++mnTrailingSpaceCount ) 141 if( !IsSpacingGlyph( rArgs.mpStr[i] | GF_ISCHAR ) 142 && (rArgs.mpStr[i] != 0x00A0) ) 143 break; 144 return true; 145 } 146 147 // ----------------------------------------------------------------------- 148 149 void CTLayout::AdjustLayout( ImplLayoutArgs& rArgs ) 150 { 151 if( !mpCTLine) 152 return; 153 154 int nPixelWidth = rArgs.mnLayoutWidth; 155 if( rArgs.mpDXArray ) 156 { 157 // for now we are only interested in the layout width 158 // TODO: use all mpDXArray elements for layouting 159 nPixelWidth = rArgs.mpDXArray[ mnCharCount-1 ]; 160 } 161 else if( !nPixelWidth ) // short-circuit if there is nothing to adjust 162 return; 163 164 // short-circuit when justifying an all-whitespace string 165 if( mnTrailingSpaceCount >= mnCharCount) 166 { 167 mfCachedWidth = nPixelWidth / mfFontScale; 168 return; 169 } 170 171 // return early if there is nothing to do 172 if( nPixelWidth <= 0 ) 173 return; 174 175 // HACK: justification requests which change the width by just one pixel are probably 176 // #i86038# introduced by lossy conversions between integer based coordinate system 177 const int nOrigWidth = GetTextWidth(); 178 if( (nOrigWidth >= nPixelWidth-1) && (nOrigWidth <= nPixelWidth+1) ) 179 return; 180 181 // if the text to be justified has whitespace in it then 182 // - Writer goes crazy with its HalfSpace magic 183 // - CoreText handles spaces specially (in particular at the text end) 184 if( mnTrailingSpaceCount ) { 185 int nTrailingSpaceWidth = 0; 186 if( rArgs.mpDXArray) { 187 const int nFullPixWidth = nPixelWidth; 188 nPixelWidth = rArgs.mpDXArray[ mnCharCount-1-mnTrailingSpaceCount ]; 189 nTrailingSpaceWidth = nFullPixWidth - nPixelWidth; 190 mfTrailingSpaceWidth = nTrailingSpaceWidth; 191 } else { 192 if( mfTrailingSpaceWidth <= 0.0 ) 193 mfTrailingSpaceWidth = CTLineGetTrailingWhitespaceWidth( mpCTLine ); 194 nTrailingSpaceWidth = rint( mfTrailingSpaceWidth ); 195 nPixelWidth -= nTrailingSpaceWidth; 196 } 197 if( nPixelWidth <= 0 ) 198 return; 199 200 // recreate the CoreText line layout without trailing spaces 201 CFRelease( mpCTLine ); 202 CFStringRef aCFText = CFStringCreateWithCharactersNoCopy( NULL, rArgs.mpStr + mnMinCharPos, 203 mnCharCount - mnTrailingSpaceCount, kCFAllocatorNull ); 204 CFAttributedStringRef pAttrStr = CFAttributedStringCreate( NULL, aCFText, mpTextStyle->GetStyleDict() ); 205 mpCTLine = CTLineCreateWithAttributedString( pAttrStr ); 206 CFRelease( aCFText); 207 CFRelease( pAttrStr ); 208 209 // in RTL-layouts trailing spaces are leftmost 210 // TODO: use BiDi-algorithm to thoroughly check this assumption 211 if( rArgs.mnFlags & SAL_LAYOUT_BIDI_RTL) 212 mnBaseAdv = nTrailingSpaceWidth; 213 } 214 215 const double fAdjustedWidth = nPixelWidth / mfFontScale; 216 CTLineRef pNewCTLine = CTLineCreateJustifiedLine( mpCTLine, 1.0, fAdjustedWidth ); 217 if( !pNewCTLine ) { // CTLineCreateJustifiedLine can and does fail 218 // handle failure by keeping the unjustified layout 219 // TODO: a better solution such as 220 // - forcing glyph overlap 221 // - changing the font size 222 // - changing the CTM matrix 223 return; 224 } 225 CFRelease( mpCTLine ); 226 mpCTLine = pNewCTLine; 227 mfCachedWidth = fAdjustedWidth + mfTrailingSpaceWidth; 228 } 229 230 // ----------------------------------------------------------------------- 231 232 void CTLayout::DrawText( SalGraphics& rGraphics ) const 233 { 234 AquaSalGraphics& rAquaGraphics = static_cast<AquaSalGraphics&>(rGraphics); 235 236 // short circuit if there is nothing to do 237 if( (mnCharCount <= 0) 238 || !rAquaGraphics.CheckContext() ) 239 return; 240 241 // the view is vertically flipped => flipped glyphs 242 // so apply a temporary transformation that it flips back 243 // also compensate if the font was size limited 244 CGContextSaveGState( rAquaGraphics.mrContext ); 245 CGContextScaleCTM( rAquaGraphics.mrContext, +mfFontScale, -mfFontScale ); 246 CGContextSetShouldAntialias( rAquaGraphics.mrContext, !rAquaGraphics.mbNonAntialiasedText ); 247 248 // Draw the text 249 const Point aVclPos = GetDrawPosition( Point(mnBaseAdv,0) ); 250 CGPoint aTextPos = { +aVclPos.X()/mfFontScale, -aVclPos.Y()/mfFontScale }; 251 252 if( mpTextStyle->mfFontRotation != 0.0 ) 253 { 254 const CGFloat fRadians = mpTextStyle->mfFontRotation; 255 CGContextRotateCTM( rAquaGraphics.mrContext, +fRadians ); 256 257 const CGAffineTransform aInvMatrix = CGAffineTransformMakeRotation( -fRadians ); 258 aTextPos = CGPointApplyAffineTransform( aTextPos, aInvMatrix ); 259 } 260 261 CGContextSetTextPosition( rAquaGraphics.mrContext, aTextPos.x, aTextPos.y ); 262 CTLineDraw( mpCTLine, rAquaGraphics.mrContext ); 263 264 // request an update of the changed window area 265 if( rAquaGraphics.IsWindowGraphics() ) 266 { 267 const CGRect aInkRect = CTLineGetImageBounds( mpCTLine, rAquaGraphics.mrContext ); 268 const CGRect aRefreshRect = CGContextConvertRectToDeviceSpace( rAquaGraphics.mrContext, aInkRect ); 269 rAquaGraphics.RefreshRect( aRefreshRect ); 270 } 271 272 // restore the original graphic context transformations 273 CGContextRestoreGState( rAquaGraphics.mrContext ); 274 } 275 276 // ----------------------------------------------------------------------- 277 278 int CTLayout::GetNextGlyphs( int nLen, sal_GlyphId* pOutGlyphIds, Point& rPos, int& nStart, 279 sal_Int32* pGlyphAdvances, int* pCharIndexes ) const 280 { 281 if( !mpCTLine ) 282 return 0; 283 284 if( nStart < 0 ) // first glyph requested? 285 nStart = 0; 286 nLen = 1; // TODO: handle nLen>1 below 287 288 // prepare to iterate over the glyph runs 289 int nCount = 0; 290 int nSubIndex = nStart; 291 292 const DynCoreTextSyms& rCT = DynCoreTextSyms::get(); 293 typedef std::vector<CGGlyph> CGGlyphVector; 294 typedef std::vector<CGPoint> CGPointVector; 295 typedef std::vector<CGSize> CGSizeVector; 296 typedef std::vector<CFIndex> CFIndexVector; 297 CGGlyphVector aCGGlyphVec; 298 CGPointVector aCGPointVec; 299 CGSizeVector aCGSizeVec; 300 CFIndexVector aCFIndexVec; 301 302 // TODO: iterate over cached layout 303 CFArrayRef aGlyphRuns = rCT.LineGetGlyphRuns( mpCTLine ); 304 const int nRunCount = CFArrayGetCount( aGlyphRuns ); 305 for( int nRunIndex = 0; nRunIndex < nRunCount; ++nRunIndex ) { 306 CTRunRef pGlyphRun = (CTRunRef)CFArrayGetValueAtIndex( aGlyphRuns, nRunIndex ); 307 const CFIndex nGlyphsInRun = rCT.RunGetGlyphCount( pGlyphRun ); 308 // skip to the first glyph run of interest 309 if( nSubIndex >= nGlyphsInRun ) { 310 nSubIndex -= nGlyphsInRun; 311 continue; 312 } 313 const CFRange aFullRange = CFRangeMake( 0, nGlyphsInRun ); 314 315 // get glyph run details 316 const CGGlyph* pCGGlyphIdx = rCT.RunGetGlyphsPtr( pGlyphRun ); 317 if( !pCGGlyphIdx ) { 318 aCGGlyphVec.reserve( nGlyphsInRun ); 319 CTRunGetGlyphs( pGlyphRun, aFullRange, &aCGGlyphVec[0] ); 320 pCGGlyphIdx = &aCGGlyphVec[0]; 321 } 322 const CGPoint* pCGGlyphPos = rCT.RunGetPositionsPtr( pGlyphRun ); 323 if( !pCGGlyphPos ) { 324 aCGPointVec.reserve( nGlyphsInRun ); 325 CTRunGetPositions( pGlyphRun, aFullRange, &aCGPointVec[0] ); 326 pCGGlyphPos = &aCGPointVec[0]; 327 } 328 329 const CGSize* pCGGlyphAdvs = NULL; 330 if( pGlyphAdvances) { 331 pCGGlyphAdvs = rCT.RunGetAdvancesPtr( pGlyphRun ); 332 if( !pCGGlyphAdvs) { 333 aCGSizeVec.reserve( nGlyphsInRun ); 334 CTRunGetAdvances( pGlyphRun, aFullRange, &aCGSizeVec[0] ); 335 pCGGlyphAdvs = &aCGSizeVec[0]; 336 } 337 } 338 339 const CFIndex* pCGGlyphStrIdx = NULL; 340 if( pCharIndexes) { 341 pCGGlyphStrIdx = rCT.RunGetStringIndicesPtr( pGlyphRun ); 342 if( !pCGGlyphStrIdx) { 343 aCFIndexVec.reserve( nGlyphsInRun ); 344 CTRunGetStringIndices( pGlyphRun, aFullRange, &aCFIndexVec[0] ); 345 pCGGlyphStrIdx = &aCFIndexVec[0]; 346 } 347 } 348 349 // get the details for each interesting glyph 350 // TODO: handle nLen>1 351 for(; (--nLen >= 0) && (nSubIndex < nGlyphsInRun); ++nSubIndex, ++nStart ) 352 { 353 // convert glyph details for VCL 354 *(pOutGlyphIds++) = pCGGlyphIdx[ nSubIndex ]; 355 if( pGlyphAdvances ) 356 *(pGlyphAdvances++) = pCGGlyphAdvs[ nSubIndex ].width; 357 if( pCharIndexes ) 358 *(pCharIndexes++) = pCGGlyphStrIdx[ nSubIndex] + mnMinCharPos; 359 if( !nCount++ ) { 360 const CGPoint& rCurPos = pCGGlyphPos[ nSubIndex ]; 361 rPos = GetDrawPosition( Point( mfFontScale * rCurPos.x, mfFontScale * rCurPos.y) ); 362 } 363 } 364 nSubIndex = 0; // prepare for the next glyph run 365 break; // TODO: handle nLen>1 366 } 367 368 return nCount; 369 } 370 371 // ----------------------------------------------------------------------- 372 373 long CTLayout::GetTextWidth() const 374 { 375 if( (mnCharCount <= 0) || !mpCTLine ) 376 return 0; 377 378 if( mfCachedWidth < 0.0 ) 379 mfCachedWidth = CTLineGetTypographicBounds( mpCTLine, NULL, NULL, NULL ); 380 381 const long nScaledWidth = lrint( mfFontScale * mfCachedWidth ); 382 return nScaledWidth; 383 } 384 385 // ----------------------------------------------------------------------- 386 387 long CTLayout::FillDXArray( sal_Int32* pDXArray ) const 388 { 389 // short circuit requests which don't need full details 390 if( !pDXArray ) 391 return GetTextWidth(); 392 393 long nPixWidth = GetTextWidth(); 394 if( pDXArray ) { 395 // prepare the sub-pixel accurate logical-width array 396 ::std::vector<float> aWidthVector( mnCharCount ); 397 if( mnTrailingSpaceCount && (mfTrailingSpaceWidth > 0.0) ) { 398 const double fOneWidth = mfTrailingSpaceWidth / mnTrailingSpaceCount; 399 for( int i = 1; i <= mnTrailingSpaceCount; ++i) 400 aWidthVector[ mnCharCount - i ] = fOneWidth; 401 } 402 // measure advances in each glyph run 403 CFArrayRef aGlyphRuns = CTLineGetGlyphRuns( mpCTLine ); 404 const int nRunCount = CFArrayGetCount( aGlyphRuns ); 405 typedef std::vector<CGSize> CGSizeVector; 406 CGSizeVector aSizeVec; 407 typedef std::vector<CFIndex> CFIndexVector; 408 CFIndexVector aIndexVec; 409 for( int nRunIndex = 0; nRunIndex < nRunCount; ++nRunIndex ) { 410 CTRunRef pGlyphRun = (CTRunRef)CFArrayGetValueAtIndex( aGlyphRuns, nRunIndex ); 411 const CFIndex nGlyphCount = CTRunGetGlyphCount( pGlyphRun ); 412 const CFRange aFullRange = CFRangeMake( 0, nGlyphCount ); 413 aSizeVec.resize( nGlyphCount ); 414 aIndexVec.resize( nGlyphCount ); 415 CTRunGetAdvances( pGlyphRun, aFullRange, &aSizeVec[0] ); 416 CTRunGetStringIndices( pGlyphRun, aFullRange, &aIndexVec[0] ); 417 for( int i = 0; i != nGlyphCount; ++i ) { 418 const int nRelIdx = aIndexVec[i]; 419 aWidthVector[nRelIdx] += aSizeVec[i].width; 420 } 421 } 422 423 // convert the sub-pixel accurate array into classic pDXArray integers 424 float fWidthSum = 0.0; 425 sal_Int32 nOldDX = 0; 426 for( int i = 0; i < mnCharCount; ++i) { 427 const sal_Int32 nNewDX = rint( fWidthSum += aWidthVector[i]); 428 pDXArray[i] = nNewDX - nOldDX; 429 nOldDX = nNewDX; 430 } 431 } 432 433 return nPixWidth; 434 } 435 436 // ----------------------------------------------------------------------- 437 438 int CTLayout::GetTextBreak( long nMaxWidth, long nCharExtra, int nFactor ) const 439 { 440 if( !mpCTLine ) 441 return STRING_LEN; 442 443 CTTypesetterRef aCTTypeSetter = CTTypesetterCreateWithAttributedString( mpAttrString ); 444 const double fCTMaxWidth = (double)nMaxWidth / (nFactor * mfFontScale); 445 CFIndex nIndex = CTTypesetterSuggestClusterBreak( aCTTypeSetter, 0, fCTMaxWidth ); 446 if( nIndex >= mnCharCount ) 447 return STRING_LEN; 448 449 nIndex += mnMinCharPos; 450 return (int)nIndex; 451 } 452 453 // ----------------------------------------------------------------------- 454 455 void CTLayout::GetCaretPositions( int nMaxIndex, sal_Int32* pCaretXArray ) const 456 { 457 DBG_ASSERT( ((nMaxIndex>0)&&!(nMaxIndex&1)), 458 "CTLayout::GetCaretPositions() : invalid number of caret pairs requested"); 459 460 // initialize the caret positions 461 for( int i = 0; i < nMaxIndex; ++i ) 462 pCaretXArray[ i ] = -1; 463 464 const DynCoreTextSyms& rCT = DynCoreTextSyms::get(); 465 for( int n = 0; n <= mnCharCount; ++n ) 466 { 467 // measure the characters cursor position 468 CGFloat fPos2 = -1; 469 const CGFloat fPos1 = rCT.LineGetOffsetForStringIndex( mpCTLine, n, &fPos2 ); 470 (void)fPos2; // TODO: split cursor at line direction change 471 // update previous trailing position 472 if( n > 0 ) 473 pCaretXArray[ 2*n-1 ] = lrint( fPos1 * mfFontScale ); 474 // update current leading position 475 if( 2*n >= nMaxIndex ) 476 break; 477 pCaretXArray[ 2*n+0 ] = lrint( fPos1 * mfFontScale ); 478 } 479 } 480 481 // ----------------------------------------------------------------------- 482 483 bool CTLayout::GetBoundRect( SalGraphics& rGraphics, Rectangle& rVCLRect ) const 484 { 485 AquaSalGraphics& rAquaGraphics = static_cast<AquaSalGraphics&>(rGraphics); 486 CGRect aMacRect = CTLineGetImageBounds( mpCTLine, rAquaGraphics.mrContext ); 487 CGPoint aMacPos = CGContextGetTextPosition( rAquaGraphics.mrContext ); 488 aMacRect.origin.x -= aMacPos.x; 489 aMacRect.origin.y -= aMacPos.y; 490 491 const Point aPos = GetDrawPosition( Point(mnBaseAdv, 0) ); 492 493 // CoreText top-bottom are vertically flipped from a VCL aspect 494 rVCLRect.Left() = aPos.X() + mfFontScale * aMacRect.origin.x; 495 rVCLRect.Right() = aPos.X() + mfFontScale * (aMacRect.origin.x + aMacRect.size.width); 496 rVCLRect.Bottom() = aPos.Y() - mfFontScale * aMacRect.origin.y; 497 rVCLRect.Top() = aPos.Y() - mfFontScale * (aMacRect.origin.y + aMacRect.size.height); 498 return true; 499 } 500 501 // ======================================================================= 502 503 // glyph fallback is supported directly by Aqua 504 // so methods used only by MultiSalLayout can be dummy implementated 505 bool CTLayout::GetGlyphOutlines( SalGraphics&, PolyPolyVector& ) const { return false; } 506 void CTLayout::InitFont() const {} 507 void CTLayout::MoveGlyph( int /*nStart*/, long /*nNewXPos*/ ) {} 508 void CTLayout::DropGlyph( int /*nStart*/ ) {} 509 void CTLayout::Simplify( bool /*bIsBase*/ ) {} 510 511 // get the ImplFontData for a glyph fallback font 512 // for a glyphid that was returned by CTLayout::GetNextGlyphs() 513 const ImplFontData* CTLayout::GetFallbackFontData( sal_GlyphId /*aGlyphId*/ ) const 514 { 515 #if 0 516 // check if any fallback fonts were needed 517 if( !mpFallbackInfo ) 518 return NULL; 519 // check if the current glyph needs a fallback font 520 int nFallbackLevel = (aGlyphId & GF_FONTMASK) >> GF_FONTSHIFT; 521 if( !nFallbackLevel ) 522 return NULL; 523 pFallbackFont = mpFallbackInfo->GetFallbackFontData( nFallbackLevel ); 524 #else 525 // let CoreText's font cascading handle glyph fallback 526 const ImplFontData* pFallbackFont = NULL; 527 #endif 528 return pFallbackFont; 529 } 530 531 // ======================================================================= 532 533 SalLayout* CTTextStyle::GetTextLayout( void ) const 534 { 535 return new CTLayout( this); 536 } 537 538 // ======================================================================= 539 540