diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:06:44 +0000 |
commit | ed5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch) | |
tree | 7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /sdext/source/presenter/PresenterHelpView.cxx | |
parent | Initial commit. (diff) | |
download | libreoffice-upstream.tar.xz libreoffice-upstream.zip |
Adding upstream version 4:7.4.7.upstream/4%7.4.7upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sdext/source/presenter/PresenterHelpView.cxx')
-rw-r--r-- | sdext/source/presenter/PresenterHelpView.cxx | 747 |
1 files changed, 747 insertions, 0 deletions
diff --git a/sdext/source/presenter/PresenterHelpView.cxx b/sdext/source/presenter/PresenterHelpView.cxx new file mode 100644 index 000000000..74adeedd1 --- /dev/null +++ b/sdext/source/presenter/PresenterHelpView.cxx @@ -0,0 +1,747 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This file incorporates work covered by the following license notice: + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed + * with this work for additional information regarding copyright + * ownership. The ASF licenses this file to you under the Apache + * License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.apache.org/licenses/LICENSE-2.0 . + */ + +#include <vcl/settings.hxx> +#include "PresenterHelpView.hxx" +#include "PresenterButton.hxx" +#include "PresenterCanvasHelper.hxx" +#include "PresenterGeometryHelper.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 <numeric> +#include <string_view> +#include <vector> + +using namespace ::com::sun::star; +using namespace ::com::sun::star::uno; +using namespace ::com::sun::star::drawing::framework; +using ::std::vector; + +namespace sdext::presenter { + +namespace { + const sal_Int32 gnHorizontalGap (20); + const sal_Int32 gnVerticalBorder (30); + const sal_Int32 gnVerticalButtonPadding (12); + + class LineDescriptor + { + public: + LineDescriptor(); + void AddPart ( + std::u16string_view rsLine, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont); + bool IsEmpty() 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() const; + + private: + const OUString msText; + std::shared_ptr<vector<LineDescriptor> > mpLineDescriptors; + + static void SplitText (const OUString& rsText, vector<OUString>& rTextParts); + void FormatText ( + const vector<OUString>& rTextParts, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth); + }; + + class Block + { + public: + Block ( + const OUString& rsLeftText, + const OUString& rsRightText, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont, + const sal_Int32 nMaximalWidth); + Block(const Block&) = delete; + Block& operator=(const Block&) = delete; + 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<std::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), + mpPresenterController(rpPresenterController), + 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_SET_THROW); + mxPane.set(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(true); + + if (mpPresenterController.is()) + { + mpFont = mpPresenterController->GetViewFont(mxViewId->getResourceURL()); + if (mpFont) + { + mpFont->PrepareFont(mxCanvas); + } + } + + // Create the close button. + mpCloseButton = PresenterButton::Create( + mxComponentContext, + mpPresenterController, + mpPresenterController->GetTheme(), + mxWindow, + mxCanvas, + "HelpViewCloser"); + + ReadHelpStrings(); + Resize(); + } + catch (RuntimeException&) + { + mxViewId = nullptr; + mxWindow = nullptr; + throw; + } +} + +PresenterHelpView::~PresenterHelpView() +{ +} + +void SAL_CALL PresenterHelpView::disposing() +{ + mxViewId = nullptr; + + if (mpCloseButton.is()) + { + Reference<lang::XComponent> xComponent = mpCloseButton; + mpCloseButton = nullptr; + 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) +{ + if (rEventObject.Source == mxCanvas) + { + mxCanvas = nullptr; + } + else if (rEventObject.Source == mxWindow) + { + mxWindow = nullptr; + dispose(); + } +} + +//----- XWindowListener ------------------------------------------------------- + +void SAL_CALL PresenterHelpView::windowResized (const awt::WindowEvent&) +{ + ThrowIfDisposed(); + Resize(); +} + +void SAL_CALL PresenterHelpView::windowMoved (const awt::WindowEvent&) +{ + ThrowIfDisposed(); +} + +void SAL_CALL PresenterHelpView::windowShown (const lang::EventObject&) +{ + ThrowIfDisposed(); + Resize(); +} + +void SAL_CALL PresenterHelpView::windowHidden (const lang::EventObject&) +{ + ThrowIfDisposed(); +} + +//----- XPaintListener -------------------------------------------------------- + +void SAL_CALL PresenterHelpView::windowPaint (const css::awt::PaintEvent& rEvent) +{ + 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()), + mxCanvas, + 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), + nullptr, + Sequence<double>(4), + rendering::CompositeOperation::SOURCE); + PresenterCanvasHelper::SetDeviceColor(aRenderState, mpFont->mnColor); + + mxCanvas->drawLine( + geometry::RealPoint2D((aWindowBox.Width/2.0), gnVerticalBorder), + geometry::RealPoint2D((aWindowBox.Width/2.0), 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); + for (const auto& rxBlock : *mpTextContainer) + { + sal_Int32 LeftX1 = gnHorizontalGap; + sal_Int32 LeftX2 = aWindowBox.Width/2 - gnHorizontalGap; + sal_Int32 RightX1 = aWindowBox.Width/2 + gnHorizontalGap; + sal_Int32 RightX2 = aWindowBox.Width - gnHorizontalGap; + /* check whether RTL interface or not + then replace the windowbox position */ + if(AllSettings::GetLayoutRTL()) + { + LeftX1 = aWindowBox.Width/2 + gnHorizontalGap; + LeftX2 = aWindowBox.Width - gnHorizontalGap; + RightX1 = gnHorizontalGap; + RightX2 = aWindowBox.Width/2 - gnHorizontalGap; + } + const double nLeftHeight ( + rxBlock->maLeft.Paint(mxCanvas, + geometry::RealRectangle2D( + LeftX1, + nY, + LeftX2, + aWindowBox.Height - gnVerticalBorder), + false, + aViewState, + aRenderState, + mpFont->mxFont)); + const double nRightHeight ( + rxBlock->maRight.Paint(mxCanvas, + geometry::RealRectangle2D( + RightX1, + nY, + RightX2, + 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(false); +} + +void PresenterHelpView::ReadHelpStrings() +{ + mpTextContainer.reset(new TextContainer); + PresenterConfigurationAccess aConfiguration ( + mxComponentContext, + "/org.openoffice.Office.PresenterScreen/", + PresenterConfigurationAccess::READ_ONLY); + Reference<container::XNameAccess> xStrings ( + aConfiguration.GetConfigurationNode("PresenterScreenSettings/HelpView/HelpStrings"), + UNO_QUERY); + PresenterConfigurationAccess::ForAll( + xStrings, + [this](OUString const&, uno::Reference<beans::XPropertySet> const& xProps) + { + return this->ProcessString(xProps); + }); +} + +void PresenterHelpView::ProcessString ( + const Reference<beans::XPropertySet>& rsProperties) +{ + if ( ! rsProperties.is()) + return; + + OUString sLeftText; + PresenterConfigurationAccess::GetProperty(rsProperties, "Left") >>= sLeftText; + OUString sRightText; + PresenterConfigurationAccess::GetProperty(rsProperties, "Right") >>= sRightText; + mpTextContainer->push_back( + std::make_shared<Block>( + sLeftText, sRightText, mpFont->mxFont, mnMaximalWidth)); +} + +void PresenterHelpView::CheckFontSize() +{ + if (!mpFont) + 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 = std::accumulate(mpTextContainer->begin(), mpTextContainer->end(), double(0), + [](const double& sum, const std::shared_ptr<Block>& rxBlock) { + return sum + std::max( + rxBlock->maLeft.GetHeight(), + rxBlock->maRight.GetHeight()); + }); + + const double nHeightDifference (nY - (mnSeparatorY-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; + } + + // 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(mnSeparatorY-gnVerticalBorder) / nY); + if (nScale > 1.0 && nScale < 1.05) + break; + + sal_Int32 nFontSizeGuess (sal_Int32(mpFont->mnSize * nScale)); + if (nHeightDifference<=0 && mpFont->mnSize>nBestSize) + nBestSize = mpFont->mnSize; + mpFont->mnSize = nFontSizeGuess; + mpFont->mxFont = nullptr; + mpFont->PrepareFont(mxCanvas); + + // Reformat blocks. + for (auto& rxBlock : *mpTextContainer) + rxBlock->Update(mpFont->mxFont, mnMaximalWidth); + } + + if (nBestSize != mpFont->mnSize) + { + mpFont->mnSize = nBestSize; + mpFont->mxFont = nullptr; + mpFont->PrepareFont(mxCanvas); + + // Reformat blocks. + for (auto& rxBlock : *mpTextContainer) + { + rxBlock->Update(mpFont->mxFont, mnMaximalWidth); + } + } +} + +//----- XResourceId ----------------------------------------------------------- + +Reference<XResourceId> SAL_CALL PresenterHelpView::getResourceId() +{ + ThrowIfDisposed(); + return mxViewId; +} + +sal_Bool SAL_CALL PresenterHelpView::isAnchorOnly() +{ + return false; +} + + +void PresenterHelpView::ProvideCanvas() +{ + 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() +{ + if (!(mpCloseButton && mxWindow.is())) + return; + + 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.0, + aWindowBox.Height - mpCloseButton->GetSize().Height/2.0)); + + CheckFontSize(); +} + +void PresenterHelpView::ThrowIfDisposed() +{ + if (rBHelper.bDisposed || rBHelper.bInDispose) + { + throw lang::DisposedException ( + "PresenterHelpView has been already disposed", + static_cast<uno::XWeak*>(this)); + } +} + +//===== LineDescriptor ========================================================= + +namespace { + +LineDescriptor::LineDescriptor() + : maSize(0,0), + mnVerticalOffset(0) +{ +} + +void LineDescriptor::AddPart ( + std::u16string_view rsLine, + const css::uno::Reference<css::rendering::XCanvasFont>& rxFont) +{ + msLine += rsLine; + + CalculateSize(rxFont); +} + +bool LineDescriptor::IsEmpty() const +{ + return msLine.isEmpty(); +} + +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); + for (const auto& rLine : *mpLineDescriptors) + { + double nX; + /// check whether RTL interface or not + if(!AllSettings::GetLayoutRTL()) + { + nX = rBBox.X1; + if ( ! bFlushLeft) + nX = rBBox.X2 - rLine.maSize.Width; + } + else + { + nX=rBBox.X2 - rLine.maSize.Width; + if ( ! bFlushLeft) + nX = rBBox.X1; + } + rRenderState.AffineTransform.m02 = nX; + rRenderState.AffineTransform.m12 = nY + rLine.maSize.Height - rLine.mnVerticalOffset; + + const rendering::StringContext aContext (rLine.msLine, 0, rLine.msLine.getLength()); + Reference<rendering::XTextLayout> xLayout ( + rxFont->createTextLayout(aContext, rendering::TextDirection::WEAK_LEFT_TO_RIGHT, 0)); + rxCanvas->drawTextLayout ( + xLayout, + rViewState, + rRenderState); + + nY += rLine.maSize.Height * 1.2; + } + + return nY - rBBox.Y1; +} + +double LineDescriptorList::GetHeight() const +{ + return std::accumulate(mpLineDescriptors->begin(), mpLineDescriptors->end(), double(0), + [](const double& nHeight, const LineDescriptor& rLine) { + return nHeight + rLine.maSize.Height * 1.2; + }); +} + +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 char cQuote ('\''); + const 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 = std::make_shared<vector<LineDescriptor>>(); + + vector<OUString>::const_iterator iPart (rTextParts.begin()); + vector<OUString>::const_iterator iEnd (rTextParts.end()); + while (iPart!=iEnd) + { + if (aLineDescriptor.IsEmpty()) + { + // Avoid empty lines. + if (PresenterCanvasHelper::GetTextSize( + rxFont, *iPart).Width > nMaximalWidth) + { + const char cSpace (' '); + + sal_Int32 nIndex (0); + sal_Int32 nStart (0); + sal_Int32 nLength (iPart->getLength()); + while (nIndex < nLength) + { + sal_Int32 nSpaceIndex (iPart->indexOf(cSpace, nIndex)); + while (nSpaceIndex >= 0 && PresenterCanvasHelper::GetTextSize( + rxFont, iPart->copy(nStart, nSpaceIndex-nStart)).Width <= nMaximalWidth) + { + nIndex = nSpaceIndex; + nSpaceIndex = iPart->indexOf(cSpace, nIndex+1); + } + + if (nSpaceIndex < 0 && PresenterCanvasHelper::GetTextSize( + rxFont, iPart->copy(nStart, nLength-nStart)).Width <= nMaximalWidth) + { + nIndex = nLength; + } + + if (nIndex == nStart) + { + nIndex = nLength; + } + + aLineDescriptor.AddPart(iPart->subView(nStart, nIndex-nStart), rxFont); + if (nIndex != nLength) + { + mpLineDescriptors->push_back(aLineDescriptor); + aLineDescriptor = LineDescriptor(); + } + nStart = nIndex; + } + } + else + { + aLineDescriptor.AddPart(*iPart, rxFont); + } + } + else if (PresenterCanvasHelper::GetTextSize( + rxFont, aLineDescriptor.msLine+", "+*iPart).Width > nMaximalWidth) + { + aLineDescriptor.AddPart(u",", rxFont); + mpLineDescriptors->push_back(aLineDescriptor); + aLineDescriptor = LineDescriptor(); + continue; + } + else + { + aLineDescriptor.AddPart(OUStringConcatenation(", "+*iPart), rxFont); + } + ++iPart; + } + 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: */ |