diff options
Diffstat (limited to 'sdext/source/presenter/PresenterHelpView.cxx')
-rw-r--r-- | sdext/source/presenter/PresenterHelpView.cxx | 825 |
1 files changed, 825 insertions, 0 deletions
diff --git a/sdext/source/presenter/PresenterHelpView.cxx b/sdext/source/presenter/PresenterHelpView.cxx new file mode 100644 index 000000000000..d8e3a6c0ac5c --- /dev/null +++ b/sdext/source/presenter/PresenterHelpView.cxx @@ -0,0 +1,825 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/************************************************************************* + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * Copyright 2000, 2010 Oracle and/or its affiliates. + * + * OpenOffice.org - a multi-platform office productivity suite + * + * This file is part of OpenOffice.org. + * + * OpenOffice.org is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 + * only, as published by the Free Software Foundation. + * + * OpenOffice.org is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License version 3 for more details + * (a copy is included in the LICENSE file that accompanied this code). + * + * You should have received a copy of the GNU Lesser General Public License + * version 3 along with OpenOffice.org. If not, see + * <http://www.openoffice.org/license.html> + * for a copy of the LGPLv3 License. + * + ************************************************************************/ + +// MARKER(update_precomp.py): autogen include statement, do not remove +#include "precompiled_sdext.hxx" + +#include "PresenterHelpView.hxx" +#include "PresenterButton.hxx" +#include "PresenterCanvasHelper.hxx" +#include "PresenterGeometryHelper.hxx" +#include "PresenterHelper.hxx" +#include "PresenterWindowManager.hxx" +#include <com/sun/star/awt/XWindowPeer.hpp> +#include <com/sun/star/container/XNameAccess.hpp> +#include <com/sun/star/drawing/framework/XConfigurationController.hpp> +#include <com/sun/star/drawing/framework/XControllerManager.hpp> +#include <com/sun/star/rendering/CompositeOperation.hpp> +#include <com/sun/star/rendering/TextDirection.hpp> +#include <com/sun/star/util/Color.hpp> +#include <algorithm> +#include <vector> +#include <boost/bind.hpp> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::drawing::framework; +using ::rtl::OUString; +using ::std::vector; + +#define A2S(pString) (::rtl::OUString(RTL_CONSTASCII_USTRINGPARAM(pString))) + + +namespace sdext { namespace presenter { + +namespace { + const static sal_Int32 gnHorizontalGap (20); + const static sal_Int32 gnVerticalBorder (30); + const static sal_Int32 gnVerticalButtonPadding (12); + + class LineDescriptor + { + public: + LineDescriptor(void); + void AddPart ( + const OUString& rsLine, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont); + bool IsEmpty (void) const; + + OUString msLine; + geometry::RealSize2D maSize; + double mnVerticalOffset; + + void CalculateSize (const css::uno::Reference<css::rendering::XCanvasFont>& rxFont); + }; + + class LineDescriptorList + { + public: + LineDescriptorList ( + const OUString& rsText, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth); + + void Update ( + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth); + + double Paint( + const Reference<rendering::XCanvas>& rxCanvas, + const geometry::RealRectangle2D& rBBox, + const bool bFlushLeft, + const rendering::ViewState& rViewState, + rendering::RenderState& rRenderState, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont) const; + double GetHeight (void) const; + + private: + const OUString msText; + ::boost::shared_ptr<vector<LineDescriptor> > mpLineDescriptors; + + void SplitText (const ::rtl::OUString& rsText, vector<rtl::OUString>& rTextParts); + void FormatText ( + const vector<rtl::OUString>& rTextParts, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth); + }; + + class Block + { + public: + Block (const Block& rBlock); + Block ( + const OUString& rsLeftText, + const OUString& rsRightText, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth); + void Update ( + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth); + + LineDescriptorList maLeft; + LineDescriptorList maRight; + }; +} // end of anonymous namespace + +class PresenterHelpView::TextContainer : public vector<boost::shared_ptr<Block> > +{ +}; + + +PresenterHelpView::PresenterHelpView ( + const Reference<uno::XComponentContext>& rxContext, + const Reference<XResourceId>& rxViewId, + const Reference<frame::XController>& rxController, + const ::rtl::Reference<PresenterController>& rpPresenterController) + : PresenterHelpViewInterfaceBase(m_aMutex), + mxComponentContext(rxContext), + mxViewId(rxViewId), + mxPane(), + mxWindow(), + mxCanvas(), + mpPresenterController(rpPresenterController), + mpFont(), + mpTextContainer(), + mpCloseButton(), + mnSeparatorY(0), + mnMaximalWidth(0) +{ + try + { + // Get the content window via the pane anchor. + Reference<XControllerManager> xCM (rxController, UNO_QUERY_THROW); + Reference<XConfigurationController> xCC ( + xCM->getConfigurationController(), UNO_QUERY_THROW); + mxPane = Reference<XPane>(xCC->getResource(rxViewId->getAnchor()), UNO_QUERY_THROW); + + mxWindow = mxPane->getWindow(); + ProvideCanvas(); + + mxWindow->addWindowListener(this); + mxWindow->addPaintListener(this); + Reference<awt::XWindowPeer> xPeer (mxWindow, UNO_QUERY); + if (xPeer.is()) + xPeer->setBackground(util::Color(0xff000000)); + mxWindow->setVisible(sal_True); + + if (mpPresenterController.is()) + { + mpFont = mpPresenterController->GetViewFont(mxViewId->getResourceURL()); + if (mpFont.get() != NULL) + { + mpFont->PrepareFont(mxCanvas); + } + } + + // Create the close button. + mpCloseButton = PresenterButton::Create( + mxComponentContext, + mpPresenterController, + mpPresenterController->GetTheme(), + mxWindow, + mxCanvas, + A2S("HelpViewCloser")); + + ReadHelpStrings(); + Resize(); + } + catch (RuntimeException&) + { + mxViewId = NULL; + mxWindow = NULL; + throw; + } +} + + + + +PresenterHelpView::~PresenterHelpView (void) +{ +} + + + + +void SAL_CALL PresenterHelpView::disposing (void) +{ + mxViewId = NULL; + + if (mpCloseButton.is()) + { + Reference<lang::XComponent> xComponent ( + static_cast<XWeak*>(mpCloseButton.get()), UNO_QUERY); + mpCloseButton = NULL; + if (xComponent.is()) + xComponent->dispose(); + } + + if (mxWindow.is()) + { + mxWindow->removeWindowListener(this); + mxWindow->removePaintListener(this); + } +} + + + + +//----- lang::XEventListener -------------------------------------------------- + +void SAL_CALL PresenterHelpView::disposing (const lang::EventObject& rEventObject) + throw (RuntimeException) +{ + if (rEventObject.Source == mxCanvas) + { + mxCanvas = NULL; + } + else if (rEventObject.Source == mxWindow) + { + mxWindow = NULL; + dispose(); + } +} + + + + +//----- XWindowListener ------------------------------------------------------- + +void SAL_CALL PresenterHelpView::windowResized (const awt::WindowEvent& rEvent) + throw (uno::RuntimeException) +{ + (void)rEvent; + ThrowIfDisposed(); + Resize(); +} + + + + +void SAL_CALL PresenterHelpView::windowMoved (const awt::WindowEvent& rEvent) + throw (uno::RuntimeException) +{ + (void)rEvent; + ThrowIfDisposed(); +} + + + + +void SAL_CALL PresenterHelpView::windowShown (const lang::EventObject& rEvent) + throw (uno::RuntimeException) +{ + (void)rEvent; + ThrowIfDisposed(); + Resize(); +} + + + + +void SAL_CALL PresenterHelpView::windowHidden (const lang::EventObject& rEvent) + throw (uno::RuntimeException) +{ + (void)rEvent; + ThrowIfDisposed(); +} + + + + +//----- XPaintListener -------------------------------------------------------- + +void SAL_CALL PresenterHelpView::windowPaint (const css::awt::PaintEvent& rEvent) + throw (RuntimeException) +{ + Paint(rEvent.UpdateRect); +} + + + + +void PresenterHelpView::Paint (const awt::Rectangle& rUpdateBox) +{ + ProvideCanvas(); + if ( ! mxCanvas.is()) + return; + + // Clear background. + const awt::Rectangle aWindowBox (mxWindow->getPosSize()); + mpPresenterController->GetCanvasHelper()->Paint( + mpPresenterController->GetViewBackground(mxViewId->getResourceURL()), + Reference<rendering::XCanvas>(mxCanvas, UNO_QUERY), + rUpdateBox, + awt::Rectangle(0,0,aWindowBox.Width,aWindowBox.Height), + awt::Rectangle()); + + // Paint vertical divider. + + rendering::ViewState aViewState( + geometry::AffineMatrix2D(1,0,0, 0,1,0), + PresenterGeometryHelper::CreatePolygon(rUpdateBox, mxCanvas->getDevice())); + + rendering::RenderState aRenderState ( + geometry::AffineMatrix2D(1,0,0, 0,1,0), + NULL, + Sequence<double>(4), + rendering::CompositeOperation::SOURCE); + PresenterCanvasHelper::SetDeviceColor(aRenderState, mpFont->mnColor); + + mxCanvas->drawLine( + geometry::RealPoint2D(aWindowBox.Width/2, gnVerticalBorder), + geometry::RealPoint2D(aWindowBox.Width/2, mnSeparatorY - gnVerticalBorder), + aViewState, + aRenderState); + + // Paint the horizontal separator. + mxCanvas->drawLine( + geometry::RealPoint2D(0, mnSeparatorY), + geometry::RealPoint2D(aWindowBox.Width, mnSeparatorY), + aViewState, + aRenderState); + + // Paint text. + double nY (gnVerticalBorder); + TextContainer::const_iterator iBlock (mpTextContainer->begin()); + TextContainer::const_iterator iBlockEnd (mpTextContainer->end()); + for ( ; iBlock!=iBlockEnd; ++iBlock) + { + const double nLeftHeight ( + (*iBlock)->maLeft.Paint(mxCanvas, + geometry::RealRectangle2D( + gnHorizontalGap, + nY, + aWindowBox.Width/2 - gnHorizontalGap, + aWindowBox.Height - gnVerticalBorder), + false, + aViewState, + aRenderState, + mpFont->mxFont)); + const double nRightHeight ( + (*iBlock)->maRight.Paint(mxCanvas, + geometry::RealRectangle2D( + aWindowBox.Width/2 + gnHorizontalGap, + nY, + aWindowBox.Width - gnHorizontalGap, + aWindowBox.Height - gnVerticalBorder), + true, + aViewState, + aRenderState, + mpFont->mxFont)); + nY += ::std::max(nLeftHeight,nRightHeight); + } + + Reference<rendering::XSpriteCanvas> xSpriteCanvas (mxCanvas, UNO_QUERY); + if (xSpriteCanvas.is()) + xSpriteCanvas->updateScreen(sal_False); +} + + + + +void PresenterHelpView::ReadHelpStrings (void) +{ + mpTextContainer.reset(new TextContainer()); + PresenterConfigurationAccess aConfiguration ( + mxComponentContext, + OUString(RTL_CONSTASCII_USTRINGPARAM("/org.openoffice.Office.extension.PresenterScreen/")), + PresenterConfigurationAccess::READ_ONLY); + Reference<container::XNameAccess> xStrings ( + aConfiguration.GetConfigurationNode(A2S("PresenterScreenSettings/HelpView/HelpStrings")), + UNO_QUERY); + PresenterConfigurationAccess::ForAll( + xStrings, + ::boost::bind(&PresenterHelpView::ProcessString, this, _2)); +} + + + + +void PresenterHelpView::ProcessString ( + const Reference<beans::XPropertySet>& rsProperties) +{ + if ( ! rsProperties.is()) + return; + + OUString sLeftText; + PresenterConfigurationAccess::GetProperty(rsProperties, A2S("Left")) >>= sLeftText; + OUString sRightText; + PresenterConfigurationAccess::GetProperty(rsProperties, A2S("Right")) >>= sRightText; + + const awt::Rectangle aWindowBox (mxWindow->getPosSize()); + mpTextContainer->push_back( + ::boost::shared_ptr<Block>( + new Block(sLeftText, sRightText, mpFont->mxFont, mnMaximalWidth))); +} + + + + +void PresenterHelpView::CheckFontSize (void) +{ + if (mpFont.get() == NULL) + return; + + const awt::Rectangle aWindowBox (mxWindow->getPosSize()); + if (aWindowBox.Width<=0 || aWindowBox.Height<=0) + return; + + sal_Int32 nBestSize (6); + + // Scaling down and then reformatting can cause the text to be too large + // still. So do this again and again until the text size is + // small enough. Restrict the number of loops. + for (int nLoopCount=0; nLoopCount<5; ++nLoopCount) + { + double nY (gnVerticalBorder); + TextContainer::iterator iBlock (mpTextContainer->begin()); + TextContainer::const_iterator iBlockEnd (mpTextContainer->end()); + for ( ; iBlock!=iBlockEnd; ++iBlock) + nY += ::std::max( + (*iBlock)->maLeft.GetHeight(), + (*iBlock)->maRight.GetHeight()); + + const double nHeightDifference (nY - (aWindowBox.Height-gnVerticalBorder)); + if (nHeightDifference <= 0 && nHeightDifference > -50) + { + // We have found a good font size that is large and leaves not + // too much space below the help text. + return; + } + + // Font is too large. Make it smaller. + + // Use a simple linear transformation to calculate initial guess of + // a size that lets all help text be shown inside the window. + const double nScale (double(aWindowBox.Height-gnVerticalBorder) / nY); + if (nScale > 0.95 && nScale <1.05) + break; + + sal_Int32 nFontSizeGuess (::std::max(sal_Int32(1),sal_Int32(mpFont->mnSize * nScale))); + if (nHeightDifference<0 && mpFont->mnSize>nBestSize) + nBestSize = mpFont->mnSize; + mpFont->mnSize = nFontSizeGuess; + mpFont->mxFont = NULL; + mpFont->PrepareFont(mxCanvas); + + // Reformat blocks. + for (iBlock=mpTextContainer->begin(); iBlock!=iBlockEnd; ++iBlock) + (*iBlock)->Update(mpFont->mxFont, mnMaximalWidth); + } + + if (nBestSize != mpFont->mnSize) + { + mpFont->mnSize = nBestSize; + mpFont->mxFont = NULL; + mpFont->PrepareFont(mxCanvas); + + // Reformat blocks. + for (TextContainer::iterator + iBlock (mpTextContainer->begin()), + iEnd (mpTextContainer->end()); + iBlock!=iEnd; + ++iBlock) + { + (*iBlock)->Update(mpFont->mxFont, mnMaximalWidth); + } + } +} + + + + +//----- XResourceId ----------------------------------------------------------- + +Reference<XResourceId> SAL_CALL PresenterHelpView::getResourceId (void) + throw (RuntimeException) +{ + ThrowIfDisposed(); + return mxViewId; +} + + + + +sal_Bool SAL_CALL PresenterHelpView::isAnchorOnly (void) + throw (RuntimeException) +{ + return false; +} + + + + +//----------------------------------------------------------------------------- + +void PresenterHelpView::ProvideCanvas (void) +{ + if ( ! mxCanvas.is() && mxPane.is()) + { + mxCanvas = mxPane->getCanvas(); + if ( ! mxCanvas.is()) + return; + Reference<lang::XComponent> xComponent (mxCanvas, UNO_QUERY); + if (xComponent.is()) + xComponent->addEventListener(static_cast<awt::XPaintListener*>(this)); + + if (mpCloseButton.is()) + mpCloseButton->SetCanvas(mxCanvas, mxWindow); + } +} + + + + +void PresenterHelpView::Resize (void) +{ + if (mpCloseButton.get() != NULL && mxWindow.is()) + { + const awt::Rectangle aWindowBox (mxWindow->getPosSize()); + mnMaximalWidth = (mxWindow->getPosSize().Width - 4*gnHorizontalGap) / 2; + + // Place vertical separator. + mnSeparatorY = aWindowBox.Height + - mpCloseButton->GetSize().Height - gnVerticalButtonPadding; + + mpCloseButton->SetCenter(geometry::RealPoint2D( + aWindowBox.Width/2, + aWindowBox.Height - mpCloseButton->GetSize().Height/2)); + + CheckFontSize(); + } +} + + + + +void PresenterHelpView::ThrowIfDisposed (void) + throw (lang::DisposedException) +{ + if (rBHelper.bDisposed || rBHelper.bInDispose) + { + throw lang::DisposedException ( + OUString(RTL_CONSTASCII_USTRINGPARAM( + "PresenterHelpView has been already disposed")), + const_cast<uno::XWeak*>(static_cast<const uno::XWeak*>(this))); + } +} + + + + +//===== LineDescritor ========================================================= + +namespace { + +LineDescriptor::LineDescriptor (void) + : msLine(), + maSize(0,0), + mnVerticalOffset(0) +{ +} + + + + +void LineDescriptor::AddPart ( + const OUString& rsLine, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont) +{ + msLine += rsLine; + + CalculateSize(rxFont); +} + + + + +bool LineDescriptor::IsEmpty (void) const +{ + return msLine.getLength()==0; +} + + + + +void LineDescriptor::CalculateSize ( + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont) +{ + OSL_ASSERT(rxFont.is()); + + rendering::StringContext aContext (msLine, 0, msLine.getLength()); + Reference<rendering::XTextLayout> xLayout ( + rxFont->createTextLayout(aContext, rendering::TextDirection::WEAK_LEFT_TO_RIGHT, 0)); + const geometry::RealRectangle2D aTextBBox (xLayout->queryTextBounds()); + maSize = css::geometry::RealSize2D(aTextBBox.X2 - aTextBBox.X1, aTextBBox.Y2 - aTextBBox.Y1); + mnVerticalOffset = aTextBBox.Y2; +} + +} // end of anonymous namespace + + + + +//===== LineDescriptorList ==================================================== + +namespace { + +LineDescriptorList::LineDescriptorList ( + const OUString& rsText, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth) + : msText(rsText) +{ + Update(rxFont, nMaximalWidth); +} + + + + +double LineDescriptorList::Paint( + const Reference<rendering::XCanvas>& rxCanvas, + const geometry::RealRectangle2D& rBBox, + const bool bFlushLeft, + const rendering::ViewState& rViewState, + rendering::RenderState& rRenderState, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont) const +{ + if ( ! rxCanvas.is()) + return 0; + + double nY (rBBox.Y1); + vector<LineDescriptor>::const_iterator iLine (mpLineDescriptors->begin()); + vector<LineDescriptor>::const_iterator iEnd (mpLineDescriptors->end()); + for ( ; iLine!=iEnd; ++iLine) + { + double nX (rBBox.X1); + if ( ! bFlushLeft) + nX = rBBox.X2 - iLine->maSize.Width; + rRenderState.AffineTransform.m02 = nX; + rRenderState.AffineTransform.m12 = nY + iLine->maSize.Height - iLine->mnVerticalOffset; + + const rendering::StringContext aContext (iLine->msLine, 0, iLine->msLine.getLength()); + + rxCanvas->drawText ( + aContext, + rxFont, + rViewState, + rRenderState, + rendering::TextDirection::WEAK_LEFT_TO_RIGHT); + + nY += iLine->maSize.Height * 1.2; + } + + return nY - rBBox.Y1; +} + + + + +double LineDescriptorList::GetHeight (void) const +{ + double nHeight (0); + vector<LineDescriptor>::const_iterator iLine (mpLineDescriptors->begin()); + vector<LineDescriptor>::const_iterator iEnd (mpLineDescriptors->end()); + for ( ; iLine!=iEnd; ++iLine) + nHeight += iLine->maSize.Height * 1.2; + + return nHeight; +} + + + + +void LineDescriptorList::Update ( + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth) +{ + vector<OUString> aTextParts; + SplitText(msText, aTextParts); + FormatText(aTextParts, rxFont, nMaximalWidth); +} + + + + +void LineDescriptorList::SplitText ( + const OUString& rsText, + vector<OUString>& rTextParts) +{ + const sal_Char cQuote ('\''); + const sal_Char cSeparator (','); + + sal_Int32 nIndex (0); + sal_Int32 nStart (0); + sal_Int32 nLength (rsText.getLength()); + bool bIsQuoted (false); + while (nIndex < nLength) + { + const sal_Int32 nQuoteIndex (rsText.indexOf(cQuote, nIndex)); + const sal_Int32 nSeparatorIndex (rsText.indexOf(cSeparator, nIndex)); + if (nQuoteIndex>=0 && (nSeparatorIndex==-1 || nQuoteIndex<nSeparatorIndex)) + { + bIsQuoted = !bIsQuoted; + nIndex = nQuoteIndex+1; + continue; + } + + const sal_Int32 nNextIndex = nSeparatorIndex; + if (nNextIndex < 0) + { + break; + } + else if ( ! bIsQuoted) + { + rTextParts.push_back(rsText.copy(nStart, nNextIndex-nStart)); + nStart = nNextIndex + 1; + } + nIndex = nNextIndex+1; + } + if (nStart < nLength) + rTextParts.push_back(rsText.copy(nStart, nLength-nStart)); +} + + + + +void LineDescriptorList::FormatText ( + const vector<OUString>& rTextParts, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth) +{ + LineDescriptor aLineDescriptor; + + mpLineDescriptors.reset(new vector<LineDescriptor>()); + + vector<OUString>::const_iterator iPart (rTextParts.begin()); + vector<OUString>::const_iterator iEnd (rTextParts.end()); + for ( ; iPart!=iEnd; ++iPart) + { + if (aLineDescriptor.IsEmpty()) + { + // Avoid empty lines. + aLineDescriptor.AddPart(*iPart, rxFont); + } + else if (PresenterCanvasHelper::GetTextSize( + rxFont, aLineDescriptor.msLine+A2S(", ")+*iPart).Width > nMaximalWidth) + { + aLineDescriptor.AddPart(A2S(","), rxFont); + mpLineDescriptors->push_back(aLineDescriptor); + aLineDescriptor = LineDescriptor(); + aLineDescriptor.AddPart(*iPart, rxFont); + } + else + { + aLineDescriptor.AddPart(A2S(", ")+*iPart, rxFont); + } + } + if ( ! aLineDescriptor.IsEmpty()) + { + mpLineDescriptors->push_back(aLineDescriptor); + } +} + + +} // end of anonymous namespace + + + + +//===== Block ================================================================= + +namespace { + +Block::Block ( + const OUString& rsLeftText, + const OUString& rsRightText, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth) + : maLeft(rsLeftText, rxFont, nMaximalWidth), + maRight(rsRightText, rxFont, nMaximalWidth) +{ +} + + + +void Block::Update ( + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth) +{ + maLeft.Update(rxFont, nMaximalWidth); + maRight.Update(rxFont, nMaximalWidth); +} + +} // end of anonymous namespace + +} } // end of namespace ::sdext::presenter + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |