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 .
19 #include "vcl/svapp.hxx"
20 #include "vcl/settings.hxx"
21 #include "PresenterHelpView.hxx"
22 #include "PresenterButton.hxx"
23 #include "PresenterCanvasHelper.hxx"
24 #include "PresenterGeometryHelper.hxx"
25 #include "PresenterHelper.hxx"
26 #include "PresenterWindowManager.hxx"
27 #include <com/sun/star/awt/XWindowPeer.hpp>
28 #include <com/sun/star/container/XNameAccess.hpp>
29 #include <com/sun/star/drawing/framework/XConfigurationController.hpp>
30 #include <com/sun/star/drawing/framework/XControllerManager.hpp>
31 #include <com/sun/star/rendering/CompositeOperation.hpp>
32 #include <com/sun/star/rendering/TextDirection.hpp>
33 #include <com/sun/star/util/Color.hpp>
36 #include <boost/bind.hpp>
37 #include <boost/noncopyable.hpp>
39 using namespace ::com::sun::star
;
40 using namespace ::com::sun::star::uno
;
41 using namespace ::com::sun::star::drawing::framework
;
44 namespace sdext
{ namespace presenter
{
47 const static sal_Int32
gnHorizontalGap (20);
48 const static sal_Int32
gnVerticalBorder (30);
49 const static sal_Int32
gnVerticalButtonPadding (12);
56 const OUString
& rsLine
,
57 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
);
61 geometry::RealSize2D maSize
;
62 double mnVerticalOffset
;
64 void CalculateSize (const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
);
67 class LineDescriptorList
71 const OUString
& rsText
,
72 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
73 const sal_Int32 nMaximalWidth
);
76 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
77 const sal_Int32 nMaximalWidth
);
80 const Reference
<rendering::XCanvas
>& rxCanvas
,
81 const geometry::RealRectangle2D
& rBBox
,
82 const bool bFlushLeft
,
83 const rendering::ViewState
& rViewState
,
84 rendering::RenderState
& rRenderState
,
85 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
) const;
86 double GetHeight() const;
89 const OUString msText
;
90 ::boost::shared_ptr
<vector
<LineDescriptor
> > mpLineDescriptors
;
92 static void SplitText (const OUString
& rsText
, vector
<OUString
>& rTextParts
);
94 const vector
<OUString
>& rTextParts
,
95 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
96 const sal_Int32 nMaximalWidth
);
99 class Block
: private boost::noncopyable
103 const OUString
& rsLeftText
,
104 const OUString
& rsRightText
,
105 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
106 const sal_Int32 nMaximalWidth
);
108 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
109 const sal_Int32 nMaximalWidth
);
111 LineDescriptorList maLeft
;
112 LineDescriptorList maRight
;
114 } // end of anonymous namespace
116 class PresenterHelpView::TextContainer
: public vector
<boost::shared_ptr
<Block
> >
120 PresenterHelpView::PresenterHelpView (
121 const Reference
<uno::XComponentContext
>& rxContext
,
122 const Reference
<XResourceId
>& rxViewId
,
123 const Reference
<frame::XController
>& rxController
,
124 const ::rtl::Reference
<PresenterController
>& rpPresenterController
)
125 : PresenterHelpViewInterfaceBase(m_aMutex
),
126 mxComponentContext(rxContext
),
131 mpPresenterController(rpPresenterController
),
140 // Get the content window via the pane anchor.
141 Reference
<XControllerManager
> xCM (rxController
, UNO_QUERY_THROW
);
142 Reference
<XConfigurationController
> xCC (
143 xCM
->getConfigurationController(), UNO_QUERY_THROW
);
144 mxPane
= Reference
<XPane
>(xCC
->getResource(rxViewId
->getAnchor()), UNO_QUERY_THROW
);
146 mxWindow
= mxPane
->getWindow();
149 mxWindow
->addWindowListener(this);
150 mxWindow
->addPaintListener(this);
151 Reference
<awt::XWindowPeer
> xPeer (mxWindow
, UNO_QUERY
);
153 xPeer
->setBackground(util::Color(0xff000000));
154 mxWindow
->setVisible(sal_True
);
156 if (mpPresenterController
.is())
158 mpFont
= mpPresenterController
->GetViewFont(mxViewId
->getResourceURL());
159 if (mpFont
.get() != NULL
)
161 mpFont
->PrepareFont(mxCanvas
);
165 // Create the close button.
166 mpCloseButton
= PresenterButton::Create(
168 mpPresenterController
,
169 mpPresenterController
->GetTheme(),
177 catch (RuntimeException
&)
185 PresenterHelpView::~PresenterHelpView()
189 void SAL_CALL
PresenterHelpView::disposing()
193 if (mpCloseButton
.is())
195 Reference
<lang::XComponent
> xComponent (
196 static_cast<XWeak
*>(mpCloseButton
.get()), UNO_QUERY
);
197 mpCloseButton
= NULL
;
199 xComponent
->dispose();
204 mxWindow
->removeWindowListener(this);
205 mxWindow
->removePaintListener(this);
209 //----- lang::XEventListener --------------------------------------------------
211 void SAL_CALL
PresenterHelpView::disposing (const lang::EventObject
& rEventObject
)
212 throw (RuntimeException
, std::exception
)
214 if (rEventObject
.Source
== mxCanvas
)
218 else if (rEventObject
.Source
== mxWindow
)
225 //----- XWindowListener -------------------------------------------------------
227 void SAL_CALL
PresenterHelpView::windowResized (const awt::WindowEvent
& rEvent
)
228 throw (uno::RuntimeException
, std::exception
)
235 void SAL_CALL
PresenterHelpView::windowMoved (const awt::WindowEvent
& rEvent
)
236 throw (uno::RuntimeException
, std::exception
)
242 void SAL_CALL
PresenterHelpView::windowShown (const lang::EventObject
& rEvent
)
243 throw (uno::RuntimeException
, std::exception
)
250 void SAL_CALL
PresenterHelpView::windowHidden (const lang::EventObject
& rEvent
)
251 throw (uno::RuntimeException
, std::exception
)
257 //----- XPaintListener --------------------------------------------------------
259 void SAL_CALL
PresenterHelpView::windowPaint (const css::awt::PaintEvent
& rEvent
)
260 throw (RuntimeException
, std::exception
)
262 Paint(rEvent
.UpdateRect
);
265 void PresenterHelpView::Paint (const awt::Rectangle
& rUpdateBox
)
268 if ( ! mxCanvas
.is())
272 const awt::Rectangle
aWindowBox (mxWindow
->getPosSize());
273 mpPresenterController
->GetCanvasHelper()->Paint(
274 mpPresenterController
->GetViewBackground(mxViewId
->getResourceURL()),
275 Reference
<rendering::XCanvas
>(mxCanvas
, UNO_QUERY
),
277 awt::Rectangle(0,0,aWindowBox
.Width
,aWindowBox
.Height
),
280 // Paint vertical divider.
282 rendering::ViewState
aViewState(
283 geometry::AffineMatrix2D(1,0,0, 0,1,0),
284 PresenterGeometryHelper::CreatePolygon(rUpdateBox
, mxCanvas
->getDevice()));
286 rendering::RenderState
aRenderState (
287 geometry::AffineMatrix2D(1,0,0, 0,1,0),
290 rendering::CompositeOperation::SOURCE
);
291 PresenterCanvasHelper::SetDeviceColor(aRenderState
, mpFont
->mnColor
);
294 geometry::RealPoint2D((aWindowBox
.Width
/2.0), gnVerticalBorder
),
295 geometry::RealPoint2D((aWindowBox
.Width
/2.0), mnSeparatorY
- gnVerticalBorder
),
299 // Paint the horizontal separator.
301 geometry::RealPoint2D(0, mnSeparatorY
),
302 geometry::RealPoint2D(aWindowBox
.Width
, mnSeparatorY
),
307 double nY (gnVerticalBorder
);
308 TextContainer::const_iterator
iBlock (mpTextContainer
->begin());
309 TextContainer::const_iterator
iBlockEnd (mpTextContainer
->end());
310 for ( ; iBlock
!=iBlockEnd
; ++iBlock
)
312 sal_Int32 LeftX1
= gnHorizontalGap
;
313 sal_Int32 LeftX2
= aWindowBox
.Width
/2 - gnHorizontalGap
;
314 sal_Int32 RightX1
= aWindowBox
.Width
/2 + gnHorizontalGap
;
315 sal_Int32 RightX2
= aWindowBox
.Width
- gnHorizontalGap
;
316 /* check whether RTL interface or not
317 then replace the windowbox position */
318 if(AllSettings::GetLayoutRTL())
320 LeftX1
= aWindowBox
.Width
/2 + gnHorizontalGap
;
321 LeftX2
= aWindowBox
.Width
- gnHorizontalGap
;
322 RightX1
= gnHorizontalGap
;
323 RightX2
= aWindowBox
.Width
/2 - gnHorizontalGap
;
325 const double nLeftHeight (
326 (*iBlock
)->maLeft
.Paint(mxCanvas
,
327 geometry::RealRectangle2D(
331 aWindowBox
.Height
- gnVerticalBorder
),
336 const double nRightHeight (
337 (*iBlock
)->maRight
.Paint(mxCanvas
,
338 geometry::RealRectangle2D(
342 aWindowBox
.Height
- gnVerticalBorder
),
348 nY
+= ::std::max(nLeftHeight
,nRightHeight
);
351 Reference
<rendering::XSpriteCanvas
> xSpriteCanvas (mxCanvas
, UNO_QUERY
);
352 if (xSpriteCanvas
.is())
353 xSpriteCanvas
->updateScreen(sal_False
);
356 void PresenterHelpView::ReadHelpStrings()
358 mpTextContainer
.reset(new TextContainer());
359 PresenterConfigurationAccess
aConfiguration (
361 OUString("/org.openoffice.Office.PresenterScreen/"),
362 PresenterConfigurationAccess::READ_ONLY
);
363 Reference
<container::XNameAccess
> xStrings (
364 aConfiguration
.GetConfigurationNode("PresenterScreenSettings/HelpView/HelpStrings"),
366 PresenterConfigurationAccess::ForAll(
368 ::boost::bind(&PresenterHelpView::ProcessString
, this, _2
));
371 void PresenterHelpView::ProcessString (
372 const Reference
<beans::XPropertySet
>& rsProperties
)
374 if ( ! rsProperties
.is())
378 PresenterConfigurationAccess::GetProperty(rsProperties
, "Left") >>= sLeftText
;
380 PresenterConfigurationAccess::GetProperty(rsProperties
, "Right") >>= sRightText
;
381 mpTextContainer
->push_back(
382 ::boost::shared_ptr
<Block
>(
383 new Block(sLeftText
, sRightText
, mpFont
->mxFont
, mnMaximalWidth
)));
386 void PresenterHelpView::CheckFontSize()
388 if (mpFont
.get() == NULL
)
391 sal_Int32
nBestSize (6);
393 // Scaling down and then reformatting can cause the text to be too large
394 // still. So do this again and again until the text size is
395 // small enough. Restrict the number of loops.
396 for (int nLoopCount
=0; nLoopCount
<5; ++nLoopCount
)
399 TextContainer::iterator
iBlock (mpTextContainer
->begin());
400 TextContainer::const_iterator
iBlockEnd (mpTextContainer
->end());
401 for ( ; iBlock
!=iBlockEnd
; ++iBlock
)
403 (*iBlock
)->maLeft
.GetHeight(),
404 (*iBlock
)->maRight
.GetHeight());
406 const double nHeightDifference (nY
- (mnSeparatorY
-gnVerticalBorder
));
407 if (nHeightDifference
<= 0 && nHeightDifference
> -50)
409 // We have found a good font size that is large and leaves not
410 // too much space below the help text.
414 // Use a simple linear transformation to calculate initial guess of
415 // a size that lets all help text be shown inside the window.
416 const double nScale (double(mnSeparatorY
-gnVerticalBorder
) / nY
);
417 if (nScale
> 1.0 && nScale
< 1.05)
420 sal_Int32
nFontSizeGuess (sal_Int32(mpFont
->mnSize
* nScale
));
421 if (nHeightDifference
<=0 && mpFont
->mnSize
>nBestSize
)
422 nBestSize
= mpFont
->mnSize
;
423 mpFont
->mnSize
= nFontSizeGuess
;
424 mpFont
->mxFont
= NULL
;
425 mpFont
->PrepareFont(mxCanvas
);
428 for (iBlock
=mpTextContainer
->begin(); iBlock
!=iBlockEnd
; ++iBlock
)
429 (*iBlock
)->Update(mpFont
->mxFont
, mnMaximalWidth
);
432 if (nBestSize
!= mpFont
->mnSize
)
434 mpFont
->mnSize
= nBestSize
;
435 mpFont
->mxFont
= NULL
;
436 mpFont
->PrepareFont(mxCanvas
);
439 for (TextContainer::iterator
440 iBlock (mpTextContainer
->begin()),
441 iEnd (mpTextContainer
->end());
445 (*iBlock
)->Update(mpFont
->mxFont
, mnMaximalWidth
);
450 //----- XResourceId -----------------------------------------------------------
452 Reference
<XResourceId
> SAL_CALL
PresenterHelpView::getResourceId()
453 throw (RuntimeException
, std::exception
)
459 sal_Bool SAL_CALL
PresenterHelpView::isAnchorOnly()
460 throw (RuntimeException
, std::exception
)
467 void PresenterHelpView::ProvideCanvas()
469 if ( ! mxCanvas
.is() && mxPane
.is())
471 mxCanvas
= mxPane
->getCanvas();
472 if ( ! mxCanvas
.is())
474 Reference
<lang::XComponent
> xComponent (mxCanvas
, UNO_QUERY
);
476 xComponent
->addEventListener(static_cast<awt::XPaintListener
*>(this));
478 if (mpCloseButton
.is())
479 mpCloseButton
->SetCanvas(mxCanvas
, mxWindow
);
483 void PresenterHelpView::Resize()
485 if (mpCloseButton
.get() != NULL
&& mxWindow
.is())
487 const awt::Rectangle
aWindowBox (mxWindow
->getPosSize());
488 mnMaximalWidth
= (mxWindow
->getPosSize().Width
- 4*gnHorizontalGap
) / 2;
490 // Place vertical separator.
491 mnSeparatorY
= aWindowBox
.Height
492 - mpCloseButton
->GetSize().Height
- gnVerticalButtonPadding
;
494 mpCloseButton
->SetCenter(geometry::RealPoint2D(
495 aWindowBox
.Width
/2.0,
496 aWindowBox
.Height
- mpCloseButton
->GetSize().Height
/2.0));
502 void PresenterHelpView::ThrowIfDisposed()
503 throw (lang::DisposedException
)
505 if (rBHelper
.bDisposed
|| rBHelper
.bInDispose
)
507 throw lang::DisposedException (
508 OUString( "PresenterHelpView has been already disposed"),
509 const_cast<uno::XWeak
*>(static_cast<const uno::XWeak
*>(this)));
513 //===== LineDescritor =========================================================
517 LineDescriptor::LineDescriptor()
524 void LineDescriptor::AddPart (
525 const OUString
& rsLine
,
526 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
)
530 CalculateSize(rxFont
);
533 bool LineDescriptor::IsEmpty() const
535 return msLine
.isEmpty();
538 void LineDescriptor::CalculateSize (
539 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
)
541 OSL_ASSERT(rxFont
.is());
543 rendering::StringContext
aContext (msLine
, 0, msLine
.getLength());
544 Reference
<rendering::XTextLayout
> xLayout (
545 rxFont
->createTextLayout(aContext
, rendering::TextDirection::WEAK_LEFT_TO_RIGHT
, 0));
546 const geometry::RealRectangle2D
aTextBBox (xLayout
->queryTextBounds());
547 maSize
= css::geometry::RealSize2D(aTextBBox
.X2
- aTextBBox
.X1
, aTextBBox
.Y2
- aTextBBox
.Y1
);
548 mnVerticalOffset
= aTextBBox
.Y2
;
551 } // end of anonymous namespace
553 //===== LineDescriptorList ====================================================
557 LineDescriptorList::LineDescriptorList (
558 const OUString
& rsText
,
559 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
560 const sal_Int32 nMaximalWidth
)
563 Update(rxFont
, nMaximalWidth
);
566 double LineDescriptorList::Paint(
567 const Reference
<rendering::XCanvas
>& rxCanvas
,
568 const geometry::RealRectangle2D
& rBBox
,
569 const bool bFlushLeft
,
570 const rendering::ViewState
& rViewState
,
571 rendering::RenderState
& rRenderState
,
572 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
) const
574 if ( ! rxCanvas
.is())
577 double nY (rBBox
.Y1
);
578 vector
<LineDescriptor
>::const_iterator
iLine (mpLineDescriptors
->begin());
579 vector
<LineDescriptor
>::const_iterator
iEnd (mpLineDescriptors
->end());
580 for ( ; iLine
!=iEnd
; ++iLine
)
583 /// check whether RTL interface or not
584 if(!AllSettings::GetLayoutRTL())
588 nX
= rBBox
.X2
- iLine
->maSize
.Width
;
592 nX
=rBBox
.X2
- iLine
->maSize
.Width
;
596 rRenderState
.AffineTransform
.m02
= nX
;
597 rRenderState
.AffineTransform
.m12
= nY
+ iLine
->maSize
.Height
- iLine
->mnVerticalOffset
;
599 const rendering::StringContext
aContext (iLine
->msLine
, 0, iLine
->msLine
.getLength());
600 Reference
<rendering::XTextLayout
> xLayout (
601 rxFont
->createTextLayout(aContext
, rendering::TextDirection::WEAK_LEFT_TO_RIGHT
, 0));
602 rxCanvas
->drawTextLayout (
607 nY
+= iLine
->maSize
.Height
* 1.2;
610 return nY
- rBBox
.Y1
;
613 double LineDescriptorList::GetHeight() const
616 vector
<LineDescriptor
>::const_iterator
iLine (mpLineDescriptors
->begin());
617 vector
<LineDescriptor
>::const_iterator
iEnd (mpLineDescriptors
->end());
618 for ( ; iLine
!=iEnd
; ++iLine
)
619 nHeight
+= iLine
->maSize
.Height
* 1.2;
624 void LineDescriptorList::Update (
625 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
626 const sal_Int32 nMaximalWidth
)
628 vector
<OUString
> aTextParts
;
629 SplitText(msText
, aTextParts
);
630 FormatText(aTextParts
, rxFont
, nMaximalWidth
);
633 void LineDescriptorList::SplitText (
634 const OUString
& rsText
,
635 vector
<OUString
>& rTextParts
)
637 const sal_Char
cQuote ('\'');
638 const sal_Char
cSeparator (',');
640 sal_Int32
nIndex (0);
641 sal_Int32
nStart (0);
642 sal_Int32
nLength (rsText
.getLength());
643 bool bIsQuoted (false);
644 while (nIndex
< nLength
)
646 const sal_Int32
nQuoteIndex (rsText
.indexOf(cQuote
, nIndex
));
647 const sal_Int32
nSeparatorIndex (rsText
.indexOf(cSeparator
, nIndex
));
648 if (nQuoteIndex
>=0 && (nSeparatorIndex
==-1 || nQuoteIndex
<nSeparatorIndex
))
650 bIsQuoted
= !bIsQuoted
;
651 nIndex
= nQuoteIndex
+1;
655 const sal_Int32 nNextIndex
= nSeparatorIndex
;
660 else if ( ! bIsQuoted
)
662 rTextParts
.push_back(rsText
.copy(nStart
, nNextIndex
-nStart
));
663 nStart
= nNextIndex
+ 1;
665 nIndex
= nNextIndex
+1;
667 if (nStart
< nLength
)
668 rTextParts
.push_back(rsText
.copy(nStart
, nLength
-nStart
));
671 void LineDescriptorList::FormatText (
672 const vector
<OUString
>& rTextParts
,
673 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
674 const sal_Int32 nMaximalWidth
)
676 LineDescriptor aLineDescriptor
;
678 mpLineDescriptors
.reset(new vector
<LineDescriptor
>());
680 vector
<OUString
>::const_iterator
iPart (rTextParts
.begin());
681 vector
<OUString
>::const_iterator
iEnd (rTextParts
.end());
684 if (aLineDescriptor
.IsEmpty())
686 // Avoid empty lines.
687 if (PresenterCanvasHelper::GetTextSize(
688 rxFont
, *iPart
).Width
> nMaximalWidth
)
690 const sal_Char
cSpace (' ');
692 sal_Int32
nIndex (0);
693 sal_Int32
nStart (0);
694 sal_Int32
nLength (iPart
->getLength());
695 while (nIndex
< nLength
)
697 sal_Int32
nSpaceIndex (iPart
->indexOf(cSpace
, nIndex
));
698 while (nSpaceIndex
>= 0 && PresenterCanvasHelper::GetTextSize(
699 rxFont
, iPart
->copy(nStart
, nSpaceIndex
-nStart
)).Width
<= nMaximalWidth
)
701 nIndex
= nSpaceIndex
;
702 nSpaceIndex
= iPart
->indexOf(cSpace
, nIndex
+1);
705 if (nSpaceIndex
< 0 && PresenterCanvasHelper::GetTextSize(
706 rxFont
, iPart
->copy(nStart
, nLength
-nStart
)).Width
<= nMaximalWidth
)
711 if (nIndex
== nStart
)
716 aLineDescriptor
.AddPart(iPart
->copy(nStart
, nIndex
-nStart
), rxFont
);
717 if (nIndex
!= nLength
)
719 mpLineDescriptors
->push_back(aLineDescriptor
);
720 aLineDescriptor
= LineDescriptor();
727 aLineDescriptor
.AddPart(*iPart
, rxFont
);
730 else if (PresenterCanvasHelper::GetTextSize(
731 rxFont
, aLineDescriptor
.msLine
+", "+*iPart
).Width
> nMaximalWidth
)
733 aLineDescriptor
.AddPart(",", rxFont
);
734 mpLineDescriptors
->push_back(aLineDescriptor
);
735 aLineDescriptor
= LineDescriptor();
740 aLineDescriptor
.AddPart(", "+*iPart
, rxFont
);
744 if ( ! aLineDescriptor
.IsEmpty())
746 mpLineDescriptors
->push_back(aLineDescriptor
);
750 } // end of anonymous namespace
752 //===== Block =================================================================
757 const OUString
& rsLeftText
,
758 const OUString
& rsRightText
,
759 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
760 const sal_Int32 nMaximalWidth
)
761 : maLeft(rsLeftText
, rxFont
, nMaximalWidth
),
762 maRight(rsRightText
, rxFont
, nMaximalWidth
)
767 const css::uno::Reference
<css::rendering::XCanvasFont
>& rxFont
,
768 const sal_Int32 nMaximalWidth
)
770 maLeft
.Update(rxFont
, nMaximalWidth
);
771 maRight
.Update(rxFont
, nMaximalWidth
);
774 } // end of anonymous namespace
776 } } // end of namespace ::sdext::presenter
778 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */