/src/libreoffice/svx/source/sdr/contact/viewcontactofgraphic.cxx
Line | Count | Source |
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 <sdr/contact/viewcontactofgraphic.hxx> |
21 | | #include <sdr/contact/viewobjectcontactofgraphic.hxx> |
22 | | #include <svx/svdograf.hxx> |
23 | | #include <sdgtritm.hxx> |
24 | | #include <svx/sdgluitm.hxx> |
25 | | #include <sdgcoitm.hxx> |
26 | | #include <svx/sdggaitm.hxx> |
27 | | #include <sdginitm.hxx> |
28 | | #include <svx/sdgmoitm.hxx> |
29 | | #include <sdr/primitive2d/sdrattributecreator.hxx> |
30 | | #include <svl/itemset.hxx> |
31 | | #include <tools/debug.hxx> |
32 | | |
33 | | #include <svx/sdgcpitm.hxx> |
34 | | #include <svx/sdr/contact/viewobjectcontact.hxx> |
35 | | #include <svx/sdr/contact/objectcontact.hxx> |
36 | | #include <basegfx/matrix/b2dhommatrix.hxx> |
37 | | #include <sdr/primitive2d/sdrgrafprimitive2d.hxx> |
38 | | #include <vcl/canvastools.hxx> |
39 | | #include <vcl/svapp.hxx> |
40 | | #include <vcl/settings.hxx> |
41 | | #include <basegfx/polygon/b2dpolygontools.hxx> |
42 | | #include <drawinglayer/primitive2d/PolygonHairlinePrimitive2D.hxx> |
43 | | #include <drawinglayer/primitive2d/bitmapprimitive2d.hxx> |
44 | | #include <sdr/primitive2d/sdrtextprimitive2d.hxx> |
45 | | #include <editeng/eeitem.hxx> |
46 | | #include <editeng/colritem.hxx> |
47 | | #include <basegfx/matrix/b2dhommatrixtools.hxx> |
48 | | #include <drawinglayer/primitive2d/sdrdecompositiontools2d.hxx> |
49 | | #include <drawinglayer/primitive2d/exclusiveeditviewprimitive2d.hxx> |
50 | | |
51 | | #include <bitmaps.hlst> |
52 | | |
53 | | namespace sdr::contact |
54 | | { |
55 | | // Create an Object-Specific ViewObjectContact, set ViewContact and |
56 | | // ObjectContact. Always needs to return something. |
57 | | ViewObjectContact& ViewContactOfGraphic::CreateObjectSpecificViewObjectContact(ObjectContact& rObjectContact) |
58 | 0 | { |
59 | 0 | ViewObjectContact* pRetval = new ViewObjectContactOfGraphic(rObjectContact, *this); |
60 | 0 | DBG_ASSERT(pRetval, "ViewContact::CreateObjectSpecificViewObjectContact() failed (!)"); |
61 | |
|
62 | 0 | return *pRetval; |
63 | 0 | } |
64 | | |
65 | | ViewContactOfGraphic::ViewContactOfGraphic(SdrGrafObj& rGrafObj) |
66 | 15.9k | : ViewContactOfTextObj(rGrafObj) |
67 | 15.9k | { |
68 | 15.9k | } |
69 | | |
70 | | ViewContactOfGraphic::~ViewContactOfGraphic() |
71 | 15.9k | { |
72 | 15.9k | } |
73 | | |
74 | | drawinglayer::primitive2d::Primitive2DContainer ViewContactOfGraphic::createVIP2DSForPresObj( |
75 | | const basegfx::B2DHomMatrix& rObjectMatrix, |
76 | | const drawinglayer::attribute::SdrLineFillEffectsTextAttribute& rAttribute) const |
77 | 0 | { |
78 | 0 | drawinglayer::primitive2d::Primitive2DContainer xRetval; |
79 | 0 | GraphicObject aEmptyGraphicObject; |
80 | 0 | GraphicAttr aEmptyGraphicAttr; |
81 | | |
82 | | // SdrGrafPrimitive2D without content in original size which carries all eventual attributes and texts |
83 | 0 | const drawinglayer::primitive2d::Primitive2DReference xReferenceA(new drawinglayer::primitive2d::SdrGrafPrimitive2D( |
84 | 0 | rObjectMatrix, |
85 | 0 | rAttribute, |
86 | 0 | aEmptyGraphicObject, |
87 | 0 | aEmptyGraphicAttr, |
88 | 0 | true)); |
89 | 0 | xRetval = drawinglayer::primitive2d::Primitive2DContainer { xReferenceA }; |
90 | | |
91 | | // SdrGrafPrimitive2D with content (which is the preview graphic) scaled to smaller size and |
92 | | // without attributes |
93 | 0 | basegfx::B2DHomMatrix aSmallerMatrix; |
94 | | |
95 | | // #i94431# for some reason, i forgot to take the PrefMapMode of the graphic |
96 | | // into account. Since EmptyPresObj's are only used in Draw/Impress, it is |
97 | | // safe to assume 100th mm as target. |
98 | 0 | Size aPrefSize(GetGrafObject().GetGrafPrefSize()); |
99 | |
|
100 | 0 | if(MapUnit::MapPixel == GetGrafObject().GetGrafPrefMapMode().GetMapUnit()) |
101 | 0 | { |
102 | 0 | aPrefSize = Application::GetDefaultDevice()->PixelToLogic(aPrefSize, MapMode(MapUnit::Map100thMM)); |
103 | 0 | } |
104 | 0 | else |
105 | 0 | { |
106 | 0 | aPrefSize = OutputDevice::LogicToLogic(aPrefSize, GetGrafObject().GetGrafPrefMapMode(), MapMode(MapUnit::Map100thMM)); |
107 | 0 | } |
108 | | |
109 | | // decompose object matrix to get single values |
110 | 0 | basegfx::B2DVector aScale, aTranslate; |
111 | 0 | double fRotate, fShearX; |
112 | 0 | rObjectMatrix.decompose(aScale, aTranslate, fRotate, fShearX); |
113 | |
|
114 | 0 | const double fOffsetX((aScale.getX() - aPrefSize.getWidth()) / 2.0); |
115 | 0 | const double fOffsetY((aScale.getY() - aPrefSize.getHeight()) / 2.0); |
116 | |
|
117 | 0 | if (fOffsetX >= 0.0 && fOffsetY >= 0.0) |
118 | 0 | { |
119 | | // create the EmptyPresObj fallback visualisation. The fallback graphic |
120 | | // is already provided in rGraphicObject in this case, use it |
121 | 0 | aSmallerMatrix = basegfx::utils::createScaleTranslateB2DHomMatrix(aPrefSize.getWidth(), aPrefSize.getHeight(), fOffsetX, fOffsetY); |
122 | 0 | aSmallerMatrix = basegfx::utils::createShearXRotateTranslateB2DHomMatrix(fShearX, fRotate, aTranslate) |
123 | 0 | * aSmallerMatrix; |
124 | |
|
125 | 0 | const GraphicObject& rGraphicObject = GetGrafObject().GetGraphicObject(); |
126 | 0 | const GraphicAttr aLocalGrafInfo; |
127 | 0 | const drawinglayer::primitive2d::Primitive2DReference xReferenceB(new drawinglayer::primitive2d::SdrGrafPrimitive2D( |
128 | 0 | aSmallerMatrix, |
129 | 0 | drawinglayer::attribute::SdrLineFillEffectsTextAttribute(), |
130 | 0 | rGraphicObject, |
131 | 0 | aLocalGrafInfo)); |
132 | | |
133 | | // embed it to a ExclusiveEditViewPrimitive2D to allow to decide in |
134 | | // the primitive if to visualize or not |
135 | 0 | const drawinglayer::primitive2d::Primitive2DReference aEmbedded( |
136 | 0 | new drawinglayer::primitive2d::ExclusiveEditViewPrimitive2D( |
137 | 0 | drawinglayer::primitive2d::Primitive2DContainer { xReferenceB } )); |
138 | |
|
139 | 0 | xRetval.push_back(aEmbedded); |
140 | 0 | } |
141 | |
|
142 | 0 | return xRetval; |
143 | 0 | } |
144 | | |
145 | | drawinglayer::primitive2d::Primitive2DContainer ViewContactOfGraphic::createVIP2DSForDraft( |
146 | | const basegfx::B2DHomMatrix& rObjectMatrix, |
147 | | const drawinglayer::attribute::SdrLineFillEffectsTextAttribute& rAttribute) const |
148 | 97 | { |
149 | 97 | drawinglayer::primitive2d::Primitive2DContainer xRetval; |
150 | 97 | GraphicObject aEmptyGraphicObject; |
151 | 97 | GraphicAttr aEmptyGraphicAttr; |
152 | | |
153 | | // SdrGrafPrimitive2D without content in original size which carries all eventual attributes and texts |
154 | 97 | const drawinglayer::primitive2d::Primitive2DReference xReferenceA(new drawinglayer::primitive2d::SdrGrafPrimitive2D( |
155 | 97 | rObjectMatrix, |
156 | 97 | rAttribute, |
157 | 97 | aEmptyGraphicObject, |
158 | 97 | aEmptyGraphicAttr)); |
159 | 97 | xRetval = drawinglayer::primitive2d::Primitive2DContainer { xReferenceA }; |
160 | | |
161 | 97 | if(rAttribute.getLine().isDefault()) |
162 | 97 | { |
163 | | // create a surrounding frame when no linestyle given |
164 | 97 | const Color aColor(Application::GetSettings().GetStyleSettings().GetShadowColor()); |
165 | 97 | const basegfx::BColor aBColor(aColor.getBColor()); |
166 | 97 | basegfx::B2DPolygon aOutline(basegfx::utils::createUnitPolygon()); |
167 | 97 | aOutline.transform(rObjectMatrix); |
168 | | |
169 | 97 | xRetval.push_back( |
170 | 97 | new drawinglayer::primitive2d::PolygonHairlinePrimitive2D( |
171 | 97 | std::move(aOutline), |
172 | 97 | aBColor)); |
173 | 97 | } |
174 | | |
175 | | // decompose object matrix to get single values |
176 | 97 | basegfx::B2DVector aScale, aTranslate; |
177 | 97 | double fRotate, fShearX; |
178 | 97 | rObjectMatrix.decompose(aScale, aTranslate, fRotate, fShearX); |
179 | | |
180 | | // define a distance value, used for distance from bitmap to borders and from bitmap |
181 | | // to text, too (2 mm) |
182 | 97 | const double fDistance(200.0); |
183 | | |
184 | | // consume borders from values |
185 | 97 | aScale.setX(std::max(0.0, aScale.getX() - (2.0 * fDistance))); |
186 | 97 | aScale.setY(std::max(0.0, aScale.getY() - (2.0 * fDistance))); |
187 | 97 | aTranslate.setX(aTranslate.getX() + fDistance); |
188 | 97 | aTranslate.setY(aTranslate.getY() + fDistance); |
189 | | |
190 | | // draw a draft bitmap |
191 | 97 | const Bitmap aDraftBitmap(BMAP_GrafikEi); |
192 | | |
193 | 97 | if(!aDraftBitmap.IsEmpty()) |
194 | 0 | { |
195 | 0 | Size aPrefSize(aDraftBitmap.GetPrefSize()); |
196 | |
|
197 | 0 | if(MapUnit::MapPixel == aDraftBitmap.GetPrefMapMode().GetMapUnit()) |
198 | 0 | { |
199 | 0 | aPrefSize = Application::GetDefaultDevice()->PixelToLogic(aDraftBitmap.GetSizePixel(), MapMode(MapUnit::Map100thMM)); |
200 | 0 | } |
201 | 0 | else |
202 | 0 | { |
203 | 0 | aPrefSize = OutputDevice::LogicToLogic(aPrefSize, aDraftBitmap.GetPrefMapMode(), MapMode(MapUnit::Map100thMM)); |
204 | 0 | } |
205 | |
|
206 | 0 | const double fBitmapScaling(2.0); |
207 | 0 | const double fWidth(aPrefSize.getWidth() * fBitmapScaling); |
208 | 0 | const double fHeight(aPrefSize.getHeight() * fBitmapScaling); |
209 | |
|
210 | 0 | if(basegfx::fTools::more(fWidth, 1.0) |
211 | 0 | && basegfx::fTools::more(fHeight, 1.0) |
212 | 0 | && basegfx::fTools::lessOrEqual(fWidth, aScale.getX()) |
213 | 0 | && basegfx::fTools::lessOrEqual(fHeight, aScale.getY())) |
214 | 0 | { |
215 | 0 | const basegfx::B2DHomMatrix aBitmapMatrix(basegfx::utils::createScaleShearXRotateTranslateB2DHomMatrix( |
216 | 0 | fWidth, fHeight, fShearX, fRotate, aTranslate.getX(), aTranslate.getY())); |
217 | |
|
218 | 0 | xRetval.push_back( |
219 | 0 | new drawinglayer::primitive2d::BitmapPrimitive2D( |
220 | 0 | aDraftBitmap, |
221 | 0 | aBitmapMatrix)); |
222 | | |
223 | | // consume bitmap size in X |
224 | 0 | aScale.setX(std::max(0.0, aScale.getX() - (fWidth + fDistance))); |
225 | 0 | aTranslate.setX(aTranslate.getX() + fWidth + fDistance); |
226 | 0 | } |
227 | 0 | } |
228 | | |
229 | | // Build the text for the draft object |
230 | 97 | OUString aDraftText = GetGrafObject().GetFileName(); |
231 | | |
232 | 97 | if (aDraftText.isEmpty()) |
233 | 97 | { |
234 | 97 | aDraftText = GetGrafObject().GetName() + " ..."; |
235 | 97 | } |
236 | | |
237 | 97 | if (!aDraftText.isEmpty()) |
238 | 97 | { |
239 | | // #i103255# Goal is to produce TextPrimitives which hold the given text as |
240 | | // BlockText in the available space. It would be very tricky to do |
241 | | // an own word wrap/line layout here. |
242 | | // Using SdrBlockTextPrimitive2D OTOH is critical since it internally |
243 | | // uses the SdrObject it references. To solve this, create a temp |
244 | | // SdrObject with Attributes and Text, generate a SdrBlockTextPrimitive2D |
245 | | // directly and immediately decompose it. After that, it is no longer |
246 | | // needed and can be deleted. |
247 | | |
248 | | // create temp RectObj as TextObj and set needed attributes |
249 | 97 | rtl::Reference<SdrRectObj> pRectObj(new SdrRectObj(GetGrafObject().getSdrModelFromSdrObject(), tools::Rectangle(), SdrObjKind::Text)); |
250 | 97 | pRectObj->NbcSetText(aDraftText); |
251 | 97 | pRectObj->SetMergedItem(SvxColorItem(COL_LIGHTRED, EE_CHAR_COLOR)); |
252 | | |
253 | | // get SdrText and OPO |
254 | 97 | SdrText* pSdrText(pRectObj->getText(0)); |
255 | 97 | OutlinerParaObject* pOPO(pRectObj->GetOutlinerParaObject()); |
256 | | |
257 | 97 | if(pSdrText && pOPO) |
258 | 97 | { |
259 | | // directly use the remaining space as TextRangeTransform |
260 | 97 | const basegfx::B2DHomMatrix aTextRangeTransform(basegfx::utils::createScaleShearXRotateTranslateB2DHomMatrix( |
261 | 97 | aScale, fShearX, fRotate, aTranslate)); |
262 | | |
263 | | // directly create temp SdrBlockTextPrimitive2D |
264 | 97 | rtl::Reference< drawinglayer::primitive2d::SdrBlockTextPrimitive2D > xBlockTextPrimitive(new drawinglayer::primitive2d::SdrBlockTextPrimitive2D( |
265 | 97 | pSdrText, |
266 | 97 | *pOPO, |
267 | 97 | aTextRangeTransform, |
268 | 97 | SDRTEXTHORZADJUST_LEFT, |
269 | 97 | SDRTEXTVERTADJUST_TOP, |
270 | 97 | false, |
271 | 97 | false, |
272 | 97 | false, |
273 | 97 | false)); |
274 | | |
275 | | // decompose immediately with neutral ViewInformation. This will |
276 | | // layout the text to more simple TextPrimitives from drawinglayer |
277 | 97 | const drawinglayer::geometry::ViewInformation2D aViewInformation2D; |
278 | 97 | xBlockTextPrimitive->get2DDecomposition(xRetval, aViewInformation2D); |
279 | 97 | } |
280 | 97 | } |
281 | | |
282 | 97 | return xRetval; |
283 | 97 | } |
284 | | |
285 | | void ViewContactOfGraphic::createViewIndependentPrimitive2DSequence(drawinglayer::primitive2d::Primitive2DDecompositionVisitor& rVisitor) const |
286 | 97 | { |
287 | 97 | const SfxItemSet& rItemSet = GetGrafObject().GetMergedItemSet(); |
288 | | |
289 | | // create and fill GraphicAttr |
290 | 97 | GraphicAttr aLocalGrafInfo; |
291 | 97 | const sal_uInt16 nTrans(rItemSet.Get(SDRATTR_GRAFTRANSPARENCE).GetValue()); |
292 | 97 | const SdrGrafCropItem& rCrop(rItemSet.Get(SDRATTR_GRAFCROP)); |
293 | 97 | aLocalGrafInfo.SetLuminance(rItemSet.Get(SDRATTR_GRAFLUMINANCE).GetValue()); |
294 | 97 | aLocalGrafInfo.SetContrast(rItemSet.Get(SDRATTR_GRAFCONTRAST).GetValue()); |
295 | 97 | aLocalGrafInfo.SetChannelR(rItemSet.Get(SDRATTR_GRAFRED).GetValue()); |
296 | 97 | aLocalGrafInfo.SetChannelG(rItemSet.Get(SDRATTR_GRAFGREEN).GetValue()); |
297 | 97 | aLocalGrafInfo.SetChannelB(rItemSet.Get(SDRATTR_GRAFBLUE).GetValue()); |
298 | 97 | aLocalGrafInfo.SetGamma(rItemSet.Get(SDRATTR_GRAFGAMMA).GetValue() * 0.01); |
299 | 97 | aLocalGrafInfo.SetAlpha(255 - static_cast<sal_uInt8>(::basegfx::fround(std::min(nTrans, sal_uInt16(100)) * 2.55))); |
300 | 97 | aLocalGrafInfo.SetInvert(rItemSet.Get(SDRATTR_GRAFINVERT).GetValue()); |
301 | 97 | aLocalGrafInfo.SetDrawMode(rItemSet.Get(SDRATTR_GRAFMODE).GetValue()); |
302 | 97 | aLocalGrafInfo.SetCrop(rCrop.GetLeft(), rCrop.GetTop(), rCrop.GetRight(), rCrop.GetBottom()); |
303 | | |
304 | | // we have content if graphic is not completely transparent |
305 | 97 | const bool bHasContent(0 != aLocalGrafInfo.GetAlpha()); |
306 | 97 | drawinglayer::attribute::SdrLineFillEffectsTextAttribute aAttribute( |
307 | 97 | drawinglayer::primitive2d::createNewSdrLineFillEffectsTextAttribute( |
308 | 97 | rItemSet, |
309 | 97 | GetGrafObject().getText(0), |
310 | 97 | bHasContent)); |
311 | | |
312 | | // take unrotated snap rect for position and size. Directly use model data, not getBoundRect() or getSnapRect() |
313 | | // which will use the primitive data we just create in the near future |
314 | 97 | const ::basegfx::B2DRange aObjectRange = vcl::unotools::b2DRectangleFromRectangle(GetGrafObject().GetGeoRect()); |
315 | | |
316 | | // look for mirroring |
317 | 97 | const GeoStat& rGeoStat(GetGrafObject().GetGeoStat()); |
318 | 97 | const Degree100 nRotationAngle(rGeoStat.m_nRotationAngle); |
319 | 97 | const bool bMirrored(GetGrafObject().IsMirrored()); |
320 | | |
321 | 97 | if (bMirrored) |
322 | 0 | aLocalGrafInfo.SetMirrorFlags(BmpMirrorFlags::Horizontal); |
323 | | |
324 | | // fill object matrix |
325 | 97 | const double fShearX(-rGeoStat.mfTanShearAngle); |
326 | 97 | const double fRotate(nRotationAngle ? toRadians(36000_deg100 - nRotationAngle) : 0.0); |
327 | 97 | const basegfx::B2DHomMatrix aObjectMatrix(basegfx::utils::createScaleShearXRotateTranslateB2DHomMatrix( |
328 | 97 | aObjectRange.getWidth(), aObjectRange.getHeight(), |
329 | 97 | fShearX, fRotate, |
330 | 97 | aObjectRange.getMinX(), aObjectRange.getMinY())); |
331 | | |
332 | | // get the current, unchanged graphic object from SdrGrafObj |
333 | 97 | const GraphicObject& rGraphicObject = GetGrafObject().GetGraphicObject(); |
334 | | |
335 | 97 | if(visualisationUsesPresObj()) |
336 | 0 | { |
337 | | // it's an EmptyPresObj, create the SdrGrafPrimitive2D without content and another scaled one |
338 | | // with the content which is the placeholder graphic |
339 | 0 | rVisitor.visit(createVIP2DSForPresObj(aObjectMatrix, aAttribute)); |
340 | 0 | } |
341 | 97 | #ifndef IOS // Enforce swap-in for tiled rendering for now, while we have no delayed updating mechanism |
342 | 97 | else if(visualisationUsesDraft()) |
343 | 97 | { |
344 | | // #i102380# The graphic is swapped out. To not force a swap-in here, there is a mechanism |
345 | | // which shows a swapped-out-visualisation (which gets created here now) and an asynchronous |
346 | | // visual update mechanism for swapped-out graphics when they were loaded (see AsynchGraphicLoadingEvent |
347 | | // and ViewObjectContactOfGraphic implementation). Not forcing the swap-in here allows faster |
348 | | // (non-blocking) processing here and thus in the effect e.g. fast scrolling through pages |
349 | 97 | rVisitor.visit(createVIP2DSForDraft(aObjectMatrix, aAttribute)); |
350 | 97 | } |
351 | 0 | #endif |
352 | 0 | else |
353 | 0 | { |
354 | | // create primitive. Info: Calling the copy-constructor of GraphicObject in this |
355 | | // SdrGrafPrimitive2D constructor will force a full swap-in of the graphic |
356 | 0 | const drawinglayer::primitive2d::Primitive2DReference xReference( |
357 | 0 | new drawinglayer::primitive2d::SdrGrafPrimitive2D( |
358 | 0 | aObjectMatrix, |
359 | 0 | aAttribute, |
360 | 0 | rGraphicObject, |
361 | 0 | aLocalGrafInfo)); |
362 | |
|
363 | 0 | rVisitor.visit(xReference); |
364 | 0 | } |
365 | | |
366 | | // always append an invisible outline for the cases where no visible content exists |
367 | 97 | rVisitor.visit( |
368 | 97 | drawinglayer::primitive2d::createHiddenGeometryPrimitives2D( |
369 | 97 | aObjectMatrix)); |
370 | 97 | } |
371 | | |
372 | | bool ViewContactOfGraphic::visualisationUsesPresObj() const |
373 | 194 | { |
374 | 194 | return GetGrafObject().IsEmptyPresObj(); |
375 | 194 | } |
376 | | |
377 | | bool ViewContactOfGraphic::visualisationUsesDraft() const |
378 | 97 | { |
379 | | // no draft when already PresObj |
380 | 97 | if(visualisationUsesPresObj()) |
381 | 0 | return false; |
382 | | |
383 | | // draft when swapped out |
384 | 97 | const GraphicObject& rGraphicObject = GetGrafObject().GetGraphicObject(); |
385 | | |
386 | | // draft when no graphic |
387 | 97 | return GraphicType::NONE == rGraphicObject.GetType() || GraphicType::Default == rGraphicObject.GetType(); |
388 | 97 | } |
389 | | |
390 | | } // end of namespace |
391 | | |
392 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |