Line data Source code
1 : /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 : /*
3 : * This file is part of the LibreOffice project.
4 : *
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/.
8 : *
9 : * This file incorporates work covered by the following license notice:
10 : *
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 .
18 : */
19 :
20 : #include <svx/sdr/primitive2d/sdrdecompositiontools.hxx>
21 : #include <drawinglayer/primitive2d/baseprimitive2d.hxx>
22 : #include <drawinglayer/primitive2d/polypolygonprimitive2d.hxx>
23 : #include <drawinglayer/primitive2d/unifiedtransparenceprimitive2d.hxx>
24 : #include <drawinglayer/primitive2d/transparenceprimitive2d.hxx>
25 : #include <basegfx/polygon/b2dpolypolygontools.hxx>
26 : #include <drawinglayer/primitive2d/fillgradientprimitive2d.hxx>
27 : #include <drawinglayer/attribute/strokeattribute.hxx>
28 : #include <drawinglayer/attribute/linestartendattribute.hxx>
29 : #include <drawinglayer/primitive2d/polygonprimitive2d.hxx>
30 : #include <drawinglayer/attribute/sdrfillgraphicattribute.hxx>
31 : #include <basegfx/matrix/b2dhommatrix.hxx>
32 : #include <drawinglayer/primitive2d/shadowprimitive2d.hxx>
33 : #include <svx/sdr/attribute/sdrtextattribute.hxx>
34 : #include <sdr/primitive2d/sdrtextprimitive2d.hxx>
35 : #include <svx/svdotext.hxx>
36 : #include <basegfx/polygon/b2dpolygontools.hxx>
37 : #include <drawinglayer/primitive2d/animatedprimitive2d.hxx>
38 : #include <drawinglayer/animation/animationtiming.hxx>
39 : #include <drawinglayer/primitive2d/maskprimitive2d.hxx>
40 : #include <basegfx/tools/canvastools.hxx>
41 : #include <drawinglayer/geometry/viewinformation2d.hxx>
42 : #include <drawinglayer/primitive2d/texthierarchyprimitive2d.hxx>
43 : #include <drawinglayer/attribute/sdrfillattribute.hxx>
44 : #include <drawinglayer/attribute/sdrlineattribute.hxx>
45 : #include <drawinglayer/attribute/sdrlinestartendattribute.hxx>
46 : #include <drawinglayer/attribute/sdrshadowattribute.hxx>
47 :
48 :
49 :
50 : using namespace com::sun::star;
51 :
52 :
53 :
54 : namespace drawinglayer
55 : {
56 : namespace primitive2d
57 : {
58 18327 : Primitive2DReference createPolyPolygonFillPrimitive(
59 : const basegfx::B2DPolyPolygon& rPolyPolygon,
60 : const attribute::SdrFillAttribute& rFill,
61 : const attribute::FillGradientAttribute& rFillGradient)
62 : {
63 : // when we have no given definition range, use the range of the given geometry
64 : // also for definition (simplest case)
65 18327 : const basegfx::B2DRange aRange(basegfx::tools::getRange(rPolyPolygon));
66 :
67 : return createPolyPolygonFillPrimitive(
68 : rPolyPolygon,
69 : aRange,
70 : rFill,
71 18327 : rFillGradient);
72 : }
73 :
74 18713 : Primitive2DReference createPolyPolygonFillPrimitive(
75 : const basegfx::B2DPolyPolygon& rPolyPolygon,
76 : const basegfx::B2DRange& rDefinitionRange,
77 : const attribute::SdrFillAttribute& rFill,
78 : const attribute::FillGradientAttribute& rFillGradient)
79 : {
80 18713 : if(basegfx::fTools::moreOrEqual(rFill.getTransparence(), 1.0))
81 : {
82 170 : return Primitive2DReference();
83 : }
84 :
85 : // prepare fully scaled polygon
86 18543 : BasePrimitive2D* pNewFillPrimitive = 0;
87 :
88 18543 : if(!rFill.getGradient().isDefault())
89 : {
90 : pNewFillPrimitive = new PolyPolygonGradientPrimitive2D(
91 : rPolyPolygon,
92 : rDefinitionRange,
93 674 : rFill.getGradient());
94 : }
95 17869 : else if(!rFill.getHatch().isDefault())
96 : {
97 : pNewFillPrimitive = new PolyPolygonHatchPrimitive2D(
98 : rPolyPolygon,
99 : rDefinitionRange,
100 : rFill.getColor(),
101 1497 : rFill.getHatch());
102 : }
103 16372 : else if(!rFill.getFillGraphic().isDefault())
104 : {
105 : pNewFillPrimitive = new PolyPolygonGraphicPrimitive2D(
106 : rPolyPolygon,
107 : rDefinitionRange,
108 410 : rFill.getFillGraphic().createFillGraphicAttribute(rDefinitionRange));
109 : }
110 : else
111 : {
112 : pNewFillPrimitive = new PolyPolygonColorPrimitive2D(
113 : rPolyPolygon,
114 15962 : rFill.getColor());
115 : }
116 :
117 18543 : if(0.0 != rFill.getTransparence())
118 : {
119 : // create simpleTransparencePrimitive, add created fill primitive
120 486 : const Primitive2DReference xRefA(pNewFillPrimitive);
121 972 : const Primitive2DSequence aContent(&xRefA, 1L);
122 972 : return Primitive2DReference(new UnifiedTransparencePrimitive2D(aContent, rFill.getTransparence()));
123 : }
124 18057 : else if(!rFillGradient.isDefault())
125 : {
126 : // create sequence with created fill primitive
127 2629 : const Primitive2DReference xRefA(pNewFillPrimitive);
128 5258 : const Primitive2DSequence aContent(&xRefA, 1L);
129 :
130 : // create FillGradientPrimitive2D for transparence and add to new sequence
131 : // fillGradientPrimitive is enough here (compared to PolyPolygonGradientPrimitive2D) since float transparence will be masked anyways
132 2629 : const basegfx::B2DRange aRange(basegfx::tools::getRange(rPolyPolygon));
133 : const Primitive2DReference xRefB(
134 : new FillGradientPrimitive2D(
135 : aRange,
136 : rDefinitionRange,
137 5258 : rFillGradient));
138 5258 : const Primitive2DSequence aAlpha(&xRefB, 1L);
139 :
140 : // create TransparencePrimitive2D using alpha and content
141 5258 : return Primitive2DReference(new TransparencePrimitive2D(aContent, aAlpha));
142 : }
143 : else
144 : {
145 : // add to decomposition
146 15428 : return Primitive2DReference(pNewFillPrimitive);
147 : }
148 : }
149 :
150 35842 : Primitive2DReference createPolygonLinePrimitive(
151 : const basegfx::B2DPolygon& rPolygon,
152 : const attribute::SdrLineAttribute& rLine,
153 : const attribute::SdrLineStartEndAttribute& rStroke)
154 : {
155 : // create line and stroke attribute
156 35842 : const attribute::LineAttribute aLineAttribute(rLine.getColor(), rLine.getWidth(), rLine.getJoin(), rLine.getCap());
157 71684 : const attribute::StrokeAttribute aStrokeAttribute(rLine.getDotDashArray(), rLine.getFullDotDashLen());
158 35842 : BasePrimitive2D* pNewLinePrimitive = 0L;
159 :
160 35842 : if(!rPolygon.isClosed() && !rStroke.isDefault())
161 : {
162 411 : attribute::LineStartEndAttribute aStart(rStroke.getStartWidth(), rStroke.getStartPolyPolygon(), rStroke.isStartCentered());
163 822 : attribute::LineStartEndAttribute aEnd(rStroke.getEndWidth(), rStroke.getEndPolyPolygon(), rStroke.isEndCentered());
164 :
165 : // create data
166 822 : pNewLinePrimitive = new PolygonStrokeArrowPrimitive2D(rPolygon, aLineAttribute, aStrokeAttribute, aStart, aEnd);
167 : }
168 : else
169 : {
170 : // create data
171 35431 : pNewLinePrimitive = new PolygonStrokePrimitive2D(rPolygon, aLineAttribute, aStrokeAttribute);
172 : }
173 :
174 35842 : if(0.0 != rLine.getTransparence())
175 : {
176 : // create simpleTransparencePrimitive, add created fill primitive
177 598 : const Primitive2DReference xRefA(pNewLinePrimitive);
178 1196 : const Primitive2DSequence aContent(&xRefA, 1L);
179 1196 : return Primitive2DReference(new UnifiedTransparencePrimitive2D(aContent, rLine.getTransparence()));
180 : }
181 : else
182 : {
183 : // add to decomposition
184 35244 : return Primitive2DReference(pNewLinePrimitive);
185 35842 : }
186 : }
187 :
188 16841 : Primitive2DReference createTextPrimitive(
189 : const basegfx::B2DPolyPolygon& rUnitPolyPolygon,
190 : const basegfx::B2DHomMatrix& rObjectTransform,
191 : const attribute::SdrTextAttribute& rText,
192 : const attribute::SdrLineAttribute& rStroke,
193 : bool bCellText,
194 : bool bWordWrap,
195 : bool bClipOnBounds)
196 : {
197 16841 : basegfx::B2DHomMatrix aAnchorTransform(rObjectTransform);
198 16841 : SdrTextPrimitive2D* pNew = 0;
199 :
200 16841 : if(rText.isContour())
201 : {
202 : // contour text
203 0 : if(!rStroke.isDefault() && 0.0 != rStroke.getWidth())
204 : {
205 : // take line width into account and shrink contour polygon accordingly
206 : // decompose to get scale
207 0 : basegfx::B2DVector aScale, aTranslate;
208 : double fRotate, fShearX;
209 0 : rObjectTransform.decompose(aScale, aTranslate, fRotate, fShearX);
210 :
211 : // scale outline to object's size to allow growing with value relative to that size
212 : // and also to keep aspect ratio
213 0 : basegfx::B2DPolyPolygon aScaledUnitPolyPolygon(rUnitPolyPolygon);
214 : aScaledUnitPolyPolygon.transform(basegfx::tools::createScaleB2DHomMatrix(
215 0 : fabs(aScale.getX()), fabs(aScale.getY())));
216 :
217 : // grow the polygon. To shrink, use negative value (half width)
218 0 : aScaledUnitPolyPolygon = basegfx::tools::growInNormalDirection(aScaledUnitPolyPolygon, -(rStroke.getWidth() * 0.5));
219 :
220 : // scale back to unit polygon
221 : aScaledUnitPolyPolygon.transform(basegfx::tools::createScaleB2DHomMatrix(
222 0 : 0.0 != aScale.getX() ? 1.0 / aScale.getX() : 1.0,
223 0 : 0.0 != aScale.getY() ? 1.0 / aScale.getY() : 1.0));
224 :
225 : // create with unit polygon
226 : pNew = new SdrContourTextPrimitive2D(
227 : &rText.getSdrText(),
228 : rText.getOutlinerParaObject(),
229 : aScaledUnitPolyPolygon,
230 0 : rObjectTransform);
231 : }
232 : else
233 : {
234 : // create with unit polygon
235 : pNew = new SdrContourTextPrimitive2D(
236 : &rText.getSdrText(),
237 : rText.getOutlinerParaObject(),
238 : rUnitPolyPolygon,
239 0 : rObjectTransform);
240 : }
241 : }
242 16841 : else if(!rText.getSdrFormTextAttribute().isDefault())
243 : {
244 : // text on path, use scaled polygon
245 0 : basegfx::B2DPolyPolygon aScaledPolyPolygon(rUnitPolyPolygon);
246 0 : aScaledPolyPolygon.transform(rObjectTransform);
247 : pNew = new SdrPathTextPrimitive2D(
248 : &rText.getSdrText(),
249 : rText.getOutlinerParaObject(),
250 : aScaledPolyPolygon,
251 0 : rText.getSdrFormTextAttribute());
252 : }
253 : else
254 : {
255 : // rObjectTransform is the whole SdrObject transformation from unit rectangle
256 : // to its size and position. Decompose to allow working with single values.
257 33682 : basegfx::B2DVector aScale, aTranslate;
258 : double fRotate, fShearX;
259 16841 : rObjectTransform.decompose(aScale, aTranslate, fRotate, fShearX);
260 :
261 : // extract mirroring
262 16841 : const bool bMirrorX(basegfx::fTools::less(aScale.getX(), 0.0));
263 16841 : const bool bMirrorY(basegfx::fTools::less(aScale.getY(), 0.0));
264 16841 : aScale = basegfx::absolute(aScale);
265 :
266 : // Get the real size, since polygon ountline and scale
267 : // from the object transformation may vary (e.g. ellipse segments)
268 33682 : basegfx::B2DHomMatrix aJustScaleTransform;
269 16841 : aJustScaleTransform.set(0, 0, aScale.getX());
270 16841 : aJustScaleTransform.set(1, 1, aScale.getY());
271 33682 : basegfx::B2DPolyPolygon aScaledUnitPolyPolygon(rUnitPolyPolygon);
272 16841 : aScaledUnitPolyPolygon.transform(aJustScaleTransform);
273 16841 : const basegfx::B2DRange aSnapRange(basegfx::tools::getRange(aScaledUnitPolyPolygon));
274 :
275 : // create a range describing the wanted text position and size (aTextAnchorRange). This
276 : // means to use the text distance values here
277 33682 : const basegfx::B2DPoint aTopLeft(aSnapRange.getMinX() + rText.getTextLeftDistance(), aSnapRange.getMinY() + rText.getTextUpperDistance());
278 33682 : const basegfx::B2DPoint aBottomRight(aSnapRange.getMaxX() - rText.getTextRightDistance(), aSnapRange.getMaxY() - rText.getTextLowerDistance());
279 16841 : basegfx::B2DRange aTextAnchorRange;
280 16841 : aTextAnchorRange.expand(aTopLeft);
281 16841 : aTextAnchorRange.expand(aBottomRight);
282 :
283 : // now create a transformation from this basic range (aTextAnchorRange)
284 : // #i121494# if we have no scale use at least 1.0 to have a carrier e.g. for
285 : // mirror values, else these will get lost
286 67364 : aAnchorTransform = basegfx::tools::createScaleTranslateB2DHomMatrix(
287 33682 : basegfx::fTools::equalZero(aTextAnchorRange.getWidth()) ? 1.0 : aTextAnchorRange.getWidth(),
288 33682 : basegfx::fTools::equalZero(aTextAnchorRange.getHeight()) ? 1.0 : aTextAnchorRange.getHeight(),
289 16841 : aTextAnchorRange.getMinX(), aTextAnchorRange.getMinY());
290 :
291 : // apply mirroring
292 16841 : aAnchorTransform.scale(bMirrorX ? -1.0 : 1.0, bMirrorY ? -1.0 : 1.0);
293 :
294 : // apply object's other transforms
295 : aAnchorTransform = basegfx::tools::createShearXRotateTranslateB2DHomMatrix(fShearX, fRotate, aTranslate)
296 16841 : * aAnchorTransform;
297 :
298 16841 : if(rText.isFitToSize())
299 : {
300 : // stretched text in range
301 : pNew = new SdrStretchTextPrimitive2D(
302 : &rText.getSdrText(),
303 : rText.getOutlinerParaObject(),
304 : aAnchorTransform,
305 0 : rText.isFixedCellHeight());
306 : }
307 16841 : else if(rText.isAutoFit())
308 : {
309 : // isotrophically scaled text in range
310 68 : pNew = new SdrAutoFitTextPrimitive2D(&rText.getSdrText(), rText.getOutlinerParaObject(), aAnchorTransform, bWordWrap);
311 : }
312 : else // text in range
313 : {
314 : // build new primitive
315 : pNew = new SdrBlockTextPrimitive2D(
316 : &rText.getSdrText(),
317 : rText.getOutlinerParaObject(),
318 : aAnchorTransform,
319 : rText.getSdrTextHorzAdjust(),
320 : rText.getSdrTextVertAdjust(),
321 16773 : rText.isFixedCellHeight(),
322 16773 : rText.isScroll(),
323 : bCellText,
324 : bWordWrap,
325 50319 : bClipOnBounds);
326 16841 : }
327 : }
328 :
329 : OSL_ENSURE(pNew != 0, "createTextPrimitive: no text primitive created (!)");
330 :
331 16841 : if(rText.isBlink())
332 : {
333 : // prepare animation and primitive list
334 0 : drawinglayer::animation::AnimationEntryList aAnimationList;
335 0 : rText.getBlinkTextTiming(aAnimationList);
336 :
337 0 : if(0.0 != aAnimationList.getDuration())
338 : {
339 : // create content sequence
340 0 : const Primitive2DReference xRefA(pNew);
341 0 : const Primitive2DSequence aContent(&xRefA, 1L);
342 :
343 : // create and add animated switch primitive
344 0 : return Primitive2DReference(new AnimatedBlinkPrimitive2D(aAnimationList, aContent, true));
345 : }
346 : else
347 : {
348 : // add to decomposition
349 0 : return Primitive2DReference(pNew);
350 0 : }
351 : }
352 :
353 16841 : if(rText.isScroll())
354 : {
355 : // suppress scroll when FontWork
356 0 : if(rText.getSdrFormTextAttribute().isDefault())
357 : {
358 : // get scroll direction
359 0 : const SdrTextAniDirection eDirection(rText.getSdrText().GetObject().GetTextAniDirection());
360 0 : const bool bHorizontal(SDRTEXTANI_LEFT == eDirection || SDRTEXTANI_RIGHT == eDirection);
361 :
362 : // decompose to get separated values for the scroll box
363 0 : basegfx::B2DVector aScale, aTranslate;
364 : double fRotate, fShearX;
365 0 : aAnchorTransform.decompose(aScale, aTranslate, fRotate, fShearX);
366 :
367 : // build transform from scaled only to full AnchorTransform and inverse
368 : const basegfx::B2DHomMatrix aSRT(basegfx::tools::createShearXRotateTranslateB2DHomMatrix(
369 0 : fShearX, fRotate, aTranslate));
370 0 : basegfx::B2DHomMatrix aISRT(aSRT);
371 0 : aISRT.invert();
372 :
373 : // bring the primitive back to scaled only and get scaled range, create new clone for this
374 0 : SdrTextPrimitive2D* pNew2 = pNew->createTransformedClone(aISRT);
375 : OSL_ENSURE(pNew2, "createTextPrimitive: Could not create transformed clone of text primitive (!)");
376 0 : delete pNew;
377 0 : pNew = pNew2;
378 :
379 : // create neutral geometry::ViewInformation2D for local range and decompose calls. This is okay
380 : // since the decompose is view-independent
381 0 : const uno::Sequence< beans::PropertyValue > xViewParameters;
382 0 : geometry::ViewInformation2D aViewInformation2D(xViewParameters);
383 :
384 : // get range
385 0 : const basegfx::B2DRange aScaledRange(pNew->getB2DRange(aViewInformation2D));
386 :
387 : // create left outside and right outside transformations. Also take care
388 : // of the clip rectangle
389 0 : basegfx::B2DHomMatrix aLeft, aRight;
390 0 : basegfx::B2DPoint aClipTopLeft(0.0, 0.0);
391 0 : basegfx::B2DPoint aClipBottomRight(aScale.getX(), aScale.getY());
392 :
393 0 : if(bHorizontal)
394 : {
395 0 : aClipTopLeft.setY(aScaledRange.getMinY());
396 0 : aClipBottomRight.setY(aScaledRange.getMaxY());
397 0 : aLeft.translate(-aScaledRange.getMaxX(), 0.0);
398 0 : aRight.translate(aScale.getX() - aScaledRange.getMinX(), 0.0);
399 : }
400 : else
401 : {
402 0 : aClipTopLeft.setX(aScaledRange.getMinX());
403 0 : aClipBottomRight.setX(aScaledRange.getMaxX());
404 0 : aLeft.translate(0.0, -aScaledRange.getMaxY());
405 0 : aRight.translate(0.0, aScale.getY() - aScaledRange.getMinY());
406 : }
407 :
408 0 : aLeft *= aSRT;
409 0 : aRight *= aSRT;
410 :
411 : // prepare animation list
412 0 : drawinglayer::animation::AnimationEntryList aAnimationList;
413 :
414 0 : if(bHorizontal)
415 : {
416 0 : rText.getScrollTextTiming(aAnimationList, aScale.getX(), aScaledRange.getWidth());
417 : }
418 : else
419 : {
420 0 : rText.getScrollTextTiming(aAnimationList, aScale.getY(), aScaledRange.getHeight());
421 : }
422 :
423 0 : if(0.0 != aAnimationList.getDuration())
424 : {
425 : // create a new Primitive2DSequence containing the animated text in it's scaled only state.
426 : // use the decomposition to force to simple text primitives, those will no longer
427 : // need the outliner for formatting (alternatively it is also possible to just add
428 : // pNew to aNewPrimitiveSequence)
429 0 : Primitive2DSequence aAnimSequence(pNew->get2DDecomposition(aViewInformation2D));
430 0 : delete pNew;
431 :
432 : // create a new animatedInterpolatePrimitive and add it
433 0 : std::vector< basegfx::B2DHomMatrix > aMatrixStack;
434 0 : aMatrixStack.push_back(aLeft);
435 0 : aMatrixStack.push_back(aRight);
436 0 : const Primitive2DReference xRefA(new AnimatedInterpolatePrimitive2D(aMatrixStack, aAnimationList, aAnimSequence, true));
437 0 : const Primitive2DSequence aContent(&xRefA, 1L);
438 :
439 : // scrolling needs an encapsulating clipping primitive
440 0 : const basegfx::B2DRange aClipRange(aClipTopLeft, aClipBottomRight);
441 0 : basegfx::B2DPolygon aClipPolygon(basegfx::tools::createPolygonFromRect(aClipRange));
442 0 : aClipPolygon.transform(aSRT);
443 0 : return Primitive2DReference(new MaskPrimitive2D(basegfx::B2DPolyPolygon(aClipPolygon), aContent));
444 : }
445 : else
446 : {
447 : // add to decomposition
448 0 : return Primitive2DReference(pNew);
449 0 : }
450 : }
451 : }
452 :
453 16841 : if(rText.isInEditMode())
454 : {
455 : // #i97628#
456 : // encapsulate with TextHierarchyEditPrimitive2D to allow renderers
457 : // to suppress actively edited content if needed
458 7 : const Primitive2DReference xRefA(pNew);
459 14 : const Primitive2DSequence aContent(&xRefA, 1L);
460 :
461 : // create and add TextHierarchyEditPrimitive2D primitive
462 14 : return Primitive2DReference(new TextHierarchyEditPrimitive2D(aContent));
463 : }
464 : else
465 : {
466 : // add to decomposition
467 16834 : return Primitive2DReference(pNew);
468 16841 : }
469 : }
470 :
471 342 : Primitive2DSequence createEmbeddedShadowPrimitive(
472 : const Primitive2DSequence& rContent,
473 : const attribute::SdrShadowAttribute& rShadow)
474 : {
475 342 : if(rContent.hasElements())
476 : {
477 342 : Primitive2DSequence aRetval(2);
478 684 : basegfx::B2DHomMatrix aShadowOffset;
479 :
480 : // prepare shadow offset
481 342 : aShadowOffset.set(0, 2, rShadow.getOffset().getX());
482 342 : aShadowOffset.set(1, 2, rShadow.getOffset().getY());
483 :
484 : // create shadow primitive and add content
485 684 : aRetval[0] = Primitive2DReference(
486 : new ShadowPrimitive2D(
487 : aShadowOffset,
488 : rShadow.getColor(),
489 684 : rContent));
490 :
491 342 : if(0.0 != rShadow.getTransparence())
492 : {
493 : // create SimpleTransparencePrimitive2D
494 129 : const Primitive2DSequence aTempContent(&aRetval[0], 1);
495 :
496 258 : aRetval[0] = Primitive2DReference(
497 : new UnifiedTransparencePrimitive2D(
498 : aTempContent,
499 387 : rShadow.getTransparence()));
500 : }
501 :
502 342 : aRetval[1] = Primitive2DReference(new GroupPrimitive2D(rContent));
503 684 : return aRetval;
504 : }
505 : else
506 : {
507 0 : return rContent;
508 : }
509 : }
510 : } // end of namespace primitive2d
511 435 : } // end of namespace drawinglayer
512 :
513 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|