From 267c6f2ac71f92999e969232431ba04678e7437e Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 07:54:39 +0200 Subject: Adding upstream version 4:24.2.0. Signed-off-by: Daniel Baumann --- sw/source/uibase/docvw/edtwin2.cxx | 685 +++++++++++++++++++++++++++++++++++++ 1 file changed, 685 insertions(+) create mode 100644 sw/source/uibase/docvw/edtwin2.cxx (limited to 'sw/source/uibase/docvw/edtwin2.cxx') diff --git a/sw/source/uibase/docvw/edtwin2.cxx b/sw/source/uibase/docvw/edtwin2.cxx new file mode 100644 index 0000000000..0c47b2ec55 --- /dev/null +++ b/sw/source/uibase/docvw/edtwin2.cxx @@ -0,0 +1,685 @@ +/* -*- 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +bool HasValidPropertyValue(const uno::Any& rAny) +{ + if (bool bValue; rAny >>= bValue) + { + return true; + } + else if (OUString aValue; (rAny >>= aValue) && !(aValue.isEmpty())) + { + return true; + } + else if (awt::FontSlant eValue; rAny >>= eValue) + { + return true; + } + else if (tools::Long nValueLong; rAny >>= nValueLong) + { + return true; + } + else if (double fValue; rAny >>= fValue) + { + return true; + } + else if (short nValueShort; rAny >>= nValueShort) + { + return true; + } + else + return false; +} + +bool PSCSDFPropsQuickHelp(const HelpEvent &rEvt, SwWrtShell& rSh) +{ + OUString sText; + SwView& rView = rSh.GetView(); + + if (rView.IsHighlightCharDF() || rView.GetStylesHighlighterParaColorMap().size() + || rView.GetStylesHighlighterCharColorMap().size()) + { + SwPosition aPos(rSh.GetDoc()->GetNodes()); + Point aPt(rSh.GetWin()->PixelToLogic( + rSh.GetWin()->ScreenToOutputPixel(rEvt.GetMousePosPixel()))); + + rSh.GetLayout()->GetModelPositionForViewPoint(&aPos, aPt); + + if (!aPos.GetContentNode()->IsTextNode()) + return false; + + rtl::Reference xRange( + SwXTextRange::CreateXTextRange(*(rView.GetDocShell()->GetDoc()), + aPos, &aPos)); + if (!xRange) + throw uno::RuntimeException(); + + SwContentFrame* pContentFrame = aPos.GetContentNode()->GetTextNode()->getLayoutFrame( + rSh.GetLayout()); + + SwRect aFrameAreaRect; + + bool bContainsPt = false; + do + { + aFrameAreaRect = pContentFrame->getFrameArea(); + if (aFrameAreaRect.Contains(aPt)) + { + bContainsPt = true; + break; + } + } while((pContentFrame = pContentFrame->GetFollow())); + + if (bContainsPt) + { + if (rView.GetStylesHighlighterCharColorMap().size()) + { + // check if in CS formatting highlighted area + OUString sCharStyle; + xRange->getPropertyValue("CharStyleName") >>= sCharStyle; + if (!sCharStyle.isEmpty()) + sText = SwStyleNameMapper::GetUIName(sCharStyle, SwGetPoolIdFromName::ChrFmt); + } + + if (sText.isEmpty() && rView.IsHighlightCharDF()) + { + // check if in direct formatting highlighted area + const std::vector aHiddenProperties{ UNO_NAME_RSID, + UNO_NAME_PARA_IS_NUMBERING_RESTART, + UNO_NAME_PARA_STYLE_NAME, + UNO_NAME_PARA_CONDITIONAL_STYLE_NAME, + UNO_NAME_PAGE_STYLE_NAME, + UNO_NAME_NUMBERING_START_VALUE, + UNO_NAME_NUMBERING_IS_NUMBER, + UNO_NAME_PARA_CONTINUEING_PREVIOUS_SUB_TREE, + UNO_NAME_CHAR_STYLE_NAME, + UNO_NAME_NUMBERING_LEVEL, + UNO_NAME_SORTED_TEXT_ID, + UNO_NAME_PARRSID, + UNO_NAME_CHAR_COLOR_THEME, + UNO_NAME_CHAR_COLOR_TINT_OR_SHADE }; + + SfxItemPropertySet const& rPropSet( + *aSwMapProvider.GetPropertySet(PROPERTY_MAP_CHAR_AUTO_STYLE)); + SfxItemPropertyMap const& rMap(rPropSet.getPropertyMap()); + + const uno::Sequence aProperties + = xRange->getPropertySetInfo()->getProperties(); + + for (const beans::Property& rProperty : aProperties) + { + const OUString& rPropName = rProperty.Name; + + if (!rMap.hasPropertyByName(rPropName)) + continue; + + if (std::find(aHiddenProperties.begin(), aHiddenProperties.end(), rPropName) + != aHiddenProperties.end()) + continue; + + if (xRange->getPropertyState(rPropName) + == beans::PropertyState_DIRECT_VALUE) + { + const uno::Any aAny = xRange->getPropertyValue(rPropName); + if (HasValidPropertyValue(aAny)) + { + sText = SwResId(STR_CHARACTER_DIRECT_FORMATTING); + break; + } + } + } + } + } + else if (rView.GetStylesHighlighterParaColorMap().size()) + { + // check if in paragraph style formatting highlighted area + pContentFrame = aPos.GetContentNode()->GetTextNode()->getLayoutFrame( + rSh.GetLayout()); + do + { + aFrameAreaRect = pContentFrame->getFrameArea(); + if (pContentFrame->IsRightToLeft()) + { + aFrameAreaRect.AddRight(375); + aFrameAreaRect.Left(aFrameAreaRect.Right() - 300); + } + else + { + aFrameAreaRect.AddLeft(-375); + aFrameAreaRect.Right(aFrameAreaRect.Left() + 300); + } + if (aFrameAreaRect.Contains(aPt)) + { + OUString sParaStyle; + xRange->getPropertyValue("ParaStyleName") >>= sParaStyle; + sText = SwStyleNameMapper::GetUIName(sParaStyle, SwGetPoolIdFromName::TxtColl); + // check for paragraph direct formatting + if (SwDoc::HasParagraphDirectFormatting(aPos)) + sText = sText + " + " + SwResId(STR_PARAGRAPH_DIRECT_FORMATTING); + break; + } + } while((pContentFrame = pContentFrame->GetFollow())); + } + } + + if (!sText.isEmpty()) + { + tools::Rectangle aRect(rSh.GetWin()->PixelToLogic( + rSh.GetWin()->ScreenToOutputPixel(rEvt.GetMousePosPixel())), + Size(1, 1)); + Point aPt(rSh.GetWin()->OutputToScreenPixel(rSh.GetWin()->LogicToPixel(aRect.TopLeft()))); + aRect.SetLeft(aPt.X()); + aRect.SetTop(aPt.Y()); + aPt = rSh.GetWin()->OutputToScreenPixel(rSh.GetWin()->LogicToPixel(aRect.BottomRight())); + aRect.SetRight(aPt.X()); + aRect.SetBottom(aPt.Y()); + + // tdf#136336 ensure tooltip area surrounds the current mouse position with at least a pixel margin + aRect.Union(tools::Rectangle(rEvt.GetMousePosPixel(), Size(1, 1))); + aRect.AdjustLeft(-1); + aRect.AdjustRight(1); + aRect.AdjustTop(-1); + aRect.AdjustBottom(1); + + QuickHelpFlags nStyle = QuickHelpFlags::NONE; //TipStyleBalloon; + Help::ShowQuickHelp(rSh.GetWin(), aRect, sText, nStyle); + } + + return !sText.isEmpty(); +} +} + +static OUString lcl_GetRedlineHelp( const SwRangeRedline& rRedl, bool bBalloon, + bool bTableChange, bool bTableColChange ) +{ + TranslateId pResId; + switch( rRedl.GetType() ) + { + case RedlineType::Insert: pResId = bTableChange + ? !bTableColChange + ? STR_REDLINE_TABLE_ROW_INSERT + : STR_REDLINE_TABLE_COLUMN_INSERT + : rRedl.IsMoved() + ? STR_REDLINE_INSERT_MOVED + : STR_REDLINE_INSERT; + break; + case RedlineType::Delete: pResId = bTableChange + ? !bTableColChange + ? STR_REDLINE_TABLE_ROW_DELETE + : STR_REDLINE_TABLE_COLUMN_DELETE + : rRedl.IsMoved() + ? STR_REDLINE_DELETE_MOVED + : STR_REDLINE_DELETE; + break; + case RedlineType::Format: pResId = STR_REDLINE_FORMAT; break; + case RedlineType::Table: pResId = STR_REDLINE_TABLE; break; + case RedlineType::FmtColl: pResId = STR_REDLINE_FMTCOLL; break; + case RedlineType::ParagraphFormat: pResId = STR_REDLINE_PARAGRAPH_FORMAT; break; + case RedlineType::TableRowInsert: pResId = STR_REDLINE_TABLE_ROW_INSERT; break; + case RedlineType::TableRowDelete: pResId = STR_REDLINE_TABLE_ROW_DELETE; break; + case RedlineType::TableCellInsert: pResId = STR_REDLINE_TABLE_CELL_INSERT; break; + case RedlineType::TableCellDelete: pResId = STR_REDLINE_TABLE_CELL_DELETE; break; + default: break; + } + + if (!pResId) + return OUString(); + OUStringBuffer sBuf(SwResId(pResId) + + ": " + + rRedl.GetAuthorString() + + " - " + + GetAppLangDateTimeString(rRedl.GetTimeStamp())); + if( bBalloon && !rRedl.GetComment().isEmpty() ) + sBuf.append("\n" + rRedl.GetComment()); + return sBuf.makeStringAndClear(); +} + +OUString SwEditWin::ClipLongToolTip(const OUString& rText) +{ + OUString sDisplayText(rText); + tools::Long nTextWidth = GetTextWidth(sDisplayText); + tools::Long nMaxWidth = GetDesktopRectPixel().GetWidth() * 2 / 3; + nMaxWidth = PixelToLogic(Size(nMaxWidth, 0)).Width(); + if (nTextWidth > nMaxWidth) + sDisplayText = GetOutDev()->GetEllipsisString(sDisplayText, nMaxWidth, DrawTextFlags::CenterEllipsis); + return sDisplayText; +} + +void SwEditWin::RequestHelp(const HelpEvent &rEvt) +{ + SwWrtShell &rSh = m_rView.GetWrtShell(); + + if (PSCSDFPropsQuickHelp(rEvt, rSh)) + return; + + bool bQuickBalloon = bool(rEvt.GetMode() & ( HelpEventMode::QUICK | HelpEventMode::BALLOON )); + if(bQuickBalloon && !rSh.GetViewOptions()->IsShowContentTips()) + return; + bool bContinue = true; + CurrShell aCurr(&rSh); + OUString sText; + Point aPos( PixelToLogic( ScreenToOutputPixel( rEvt.GetMousePosPixel() ) )); + bool bBalloon = bool(rEvt.GetMode() & HelpEventMode::BALLOON); + + SdrView *pSdrView = rSh.GetDrawView(); + + if( bQuickBalloon && pSdrView ) + { + SdrPageView* pPV = pSdrView->GetSdrPageView(); + SwDPage* pPage = pPV ? static_cast(pPV->GetPage()) : nullptr; + bContinue = pPage && pPage->RequestHelp(this, pSdrView, rEvt); + } + + if( bContinue && bQuickBalloon) + { + SwRect aFieldRect; + SwContentAtPos aContentAtPos( IsAttrAtPos::Field | + IsAttrAtPos::InetAttr | + IsAttrAtPos::Ftn | + IsAttrAtPos::Redline | + IsAttrAtPos::ToxMark | + IsAttrAtPos::RefMark | + IsAttrAtPos::SmartTag | +#ifdef DBG_UTIL + IsAttrAtPos::TableBoxValue | + ( bBalloon ? IsAttrAtPos::CurrAttrs : IsAttrAtPos::NONE) | +#endif + IsAttrAtPos::TableBoxFml | + IsAttrAtPos::TableRedline | + IsAttrAtPos::TableColRedline ); + + if( rSh.GetContentAtPos( aPos, aContentAtPos, false, &aFieldRect ) ) + { + QuickHelpFlags nStyle = QuickHelpFlags::NONE; // style of quick help + switch( aContentAtPos.eContentAtPos ) + { + case IsAttrAtPos::TableBoxFml: + sText = "= " + static_cast(aContentAtPos.aFnd.pAttr)->GetFormula(); + break; +#ifdef DBG_UTIL + case IsAttrAtPos::TableBoxValue: + { + if(aContentAtPos.aFnd.pAttr) + { + sText = OUString::number( + static_cast(aContentAtPos.aFnd.pAttr)->GetValue()); + } + break; + } + case IsAttrAtPos::CurrAttrs: + sText = aContentAtPos.sStr; + break; +#endif + + case IsAttrAtPos::InetAttr: + { + sText = static_cast(aContentAtPos.aFnd.pAttr)->GetValue(); + sText = URIHelper::removePassword( sText, + INetURLObject::EncodeMechanism::WasEncoded, + INetURLObject::DecodeMechanism::Unambiguous); + //#i63832# remove the link target type + sal_Int32 nFound = sText.indexOf(cMarkSeparator); + if( nFound != -1 && (++nFound) < sText.getLength() ) + { + std::u16string_view sSuffix( sText.subView(nFound) ); + if( sSuffix == u"table" || + sSuffix == u"frame" || + sSuffix == u"region" || + sSuffix == u"outline" || + sSuffix == u"text" || + sSuffix == u"graphic" || + sSuffix == u"ole" || + sSuffix == u"drawingobject" ) + sText = sText.copy( 0, nFound - 1); + } + // #i104300# + // special handling if target is a cross-reference bookmark + { + OUString sTmpSearchStr = sText.copy( 1 ); + IDocumentMarkAccess* pMarkAccess = rSh.getIDocumentMarkAccess(); + IDocumentMarkAccess::const_iterator_t ppBkmk = + pMarkAccess->findBookmark( sTmpSearchStr ); + if ( ppBkmk != pMarkAccess->getBookmarksEnd() && + IDocumentMarkAccess::GetType(**ppBkmk) + == IDocumentMarkAccess::MarkType::CROSSREF_HEADING_BOOKMARK ) + { + SwTextNode* pTextNode = (*ppBkmk)->GetMarkStart().GetNode().GetTextNode(); + if ( pTextNode ) + { + sText = sw::GetExpandTextMerged(rSh.GetLayout(), *pTextNode, true, false, ExpandMode(0)); + + if( !sText.isEmpty() ) + { + OUStringBuffer sTmp(sText.replaceAll(u"\u00ad", "")); + for (sal_Int32 i = 0; i < sTmp.getLength(); ++i) + { + if (sTmp[i] < 0x20) + sTmp[i] = 0x20; + else if (sTmp[i] == 0x2011) + sTmp[i] = '-'; + } + sText = sTmp.makeStringAndClear(); + } + } + } + } + // #i80029# + bool bExecHyperlinks = m_rView.GetDocShell()->IsReadOnly(); + if ( !bExecHyperlinks ) + { + sText = SfxHelp::GetURLHelpText(sText); + } + break; + } + case IsAttrAtPos::SmartTag: + { + vcl::KeyCode aCode( KEY_SPACE ); + vcl::KeyCode aModifiedCode( KEY_SPACE, KEY_MOD1 ); + OUString aModStr( aModifiedCode.GetName() ); + aModStr = aModStr.replaceFirst(aCode.GetName(), ""); + aModStr = aModStr.replaceAll("+", ""); + sText = SwResId(STR_SMARTTAG_CLICK).replaceAll("%s", aModStr); + break; + } + + case IsAttrAtPos::Ftn: + if( aContentAtPos.pFndTextAttr && aContentAtPos.aFnd.pAttr ) + { + const SwFormatFootnote* pFootnote = static_cast(aContentAtPos.aFnd.pAttr); + OUString sTmp(pFootnote->GetFootnoteText(*rSh.GetLayout())); + sText = SwResId( pFootnote->IsEndNote() + ? STR_ENDNOTE : STR_FTNNOTE ) + sTmp; + bBalloon = true; + if( aContentAtPos.IsInRTLText() ) + nStyle |= QuickHelpFlags::BiDiRtl; + } + break; + + case IsAttrAtPos::TableRedline: + case IsAttrAtPos::TableColRedline: + case IsAttrAtPos::Redline: + { + const bool bShowTrackChanges = IDocumentRedlineAccess::IsShowChanges( m_rView.GetDocShell()->GetDoc()->getIDocumentRedlineAccess().GetRedlineFlags() ); + const bool bShowInlineTooltips = rSh.GetViewOptions()->IsShowInlineTooltips(); + if ( bShowTrackChanges && bShowInlineTooltips ) + { + sText = lcl_GetRedlineHelp(*aContentAtPos.aFnd.pRedl, bBalloon, + IsAttrAtPos::TableRedline == aContentAtPos.eContentAtPos || + IsAttrAtPos::TableColRedline == aContentAtPos.eContentAtPos, + IsAttrAtPos::TableColRedline == aContentAtPos.eContentAtPos); + } + break; + } + + case IsAttrAtPos::ToxMark: + sText = aContentAtPos.sStr; + if( !sText.isEmpty() && aContentAtPos.pFndTextAttr ) + { + const SwTOXType* pTType = aContentAtPos.pFndTextAttr-> + GetTOXMark().GetTOXType(); + if( pTType && !pTType->GetTypeName().isEmpty() ) + { + sText = ": " + sText; + sText = pTType->GetTypeName() + sText; + } + } + break; + + case IsAttrAtPos::RefMark: + if(aContentAtPos.aFnd.pAttr) + { + sText = SwResId(STR_CONTENT_TYPE_SINGLE_REFERENCE) + ": " + + static_cast(aContentAtPos.aFnd.pAttr)->GetRefName(); + } + break; + + default: + { + SwModuleOptions* pModOpt = SW_MOD()->GetModuleConfig(); + if(!pModOpt->IsHideFieldTips()) + { + const SwField* pField = aContentAtPos.aFnd.pField; + switch( pField->Which() ) + { + case SwFieldIds::SetExp: + case SwFieldIds::Table: + case SwFieldIds::GetExp: + { + sal_uInt16 nOldSubType = pField->GetSubType(); + const_cast(pField)->SetSubType(nsSwExtendedSubType::SUB_CMD); + sText = pField->ExpandField(true, rSh.GetLayout()); + const_cast(pField)->SetSubType(nOldSubType); + } + break; + + case SwFieldIds::Postit: + { + break; + } + case SwFieldIds::Input: // BubbleHelp, because the suggestion could be quite long + bBalloon = true; + [[fallthrough]]; + case SwFieldIds::Dropdown: + case SwFieldIds::JumpEdit: + sText = pField->GetPar2(); + break; + + case SwFieldIds::Database: + sText = pField->GetFieldName(); + break; + + case SwFieldIds::User: + { + OUString aTitle = pField->GetTitle(); + if (!aTitle.isEmpty()) + { + sText = aTitle; + } + else + { + sText = pField->GetPar1(); + } + break; + } + case SwFieldIds::HiddenText: + sText = pField->GetPar1(); + break; + + case SwFieldIds::DocStat: + break; + + case SwFieldIds::Macro: + sText = static_cast(pField)->GetMacro(); + break; + + case SwFieldIds::GetRef: + { + // #i85090# + const SwGetRefField* pRefField( dynamic_cast(pField) ); + OSL_ENSURE( pRefField, + " - unexpected type of " ); + if ( pRefField ) + { + if ( pRefField->IsRefToHeadingCrossRefBookmark() || + pRefField->IsRefToNumItemCrossRefBookmark() ) + { + sText = pRefField->GetExpandedTextOfReferencedTextNode(*rSh.GetLayout(), nullptr, nullptr); + if ( sText.getLength() > 80 ) + { + sText = OUString::Concat(sText.subView(0, 80)) + "..."; + } + } + else + { + sText = pRefField->GetSetRefName(); + } + } + break; + } + case SwFieldIds::TableOfAuthorities: + { + const auto pAuthorityField = static_cast(pField); + sText = pAuthorityField->GetAuthority(rSh.GetLayout()); + + if (auto t = pAuthorityField->GetTargetType(); + t == SwAuthorityField::TargetType::UseDisplayURL + || t == SwAuthorityField::TargetType::UseTargetURL) + { + const OUString& rURL = pAuthorityField->GetAbsoluteURL(); + sText += "\n" + SfxHelp::GetURLHelpText(rURL); + } + + break; + } + + default: break; + } + } + + if( sText.isEmpty() ) + { + const bool bShowTrackChanges = IDocumentRedlineAccess::IsShowChanges( m_rView.GetDocShell()->GetDoc()->getIDocumentRedlineAccess().GetRedlineFlags() ); + const bool bShowInlineTooltips = rSh.GetViewOptions()->IsShowInlineTooltips(); + if ( bShowTrackChanges && bShowInlineTooltips ) + { + aContentAtPos.eContentAtPos = IsAttrAtPos::Redline; + if( rSh.GetContentAtPos( aPos, aContentAtPos, false, &aFieldRect ) ) + sText = lcl_GetRedlineHelp(*aContentAtPos.aFnd.pRedl, bBalloon, /*bTableChange=*/false, /*bTableColChange=*/false); + } + } + } + } + if (!sText.isEmpty()) + { + tools::Rectangle aRect( aFieldRect.SVRect() ); + Point aPt( OutputToScreenPixel( LogicToPixel( aRect.TopLeft() ))); + aRect.SetLeft( aPt.X() ); + aRect.SetTop( aPt.Y() ); + aPt = OutputToScreenPixel( LogicToPixel( aRect.BottomRight() )); + aRect.SetRight( aPt.X() ); + aRect.SetBottom( aPt.Y() ); + + // tdf#136336 ensure tooltip area surrounds the current mouse position with at least a pixel margin + aRect.Union(tools::Rectangle(rEvt.GetMousePosPixel(), Size(1, 1))); + aRect.AdjustLeft(-1); + aRect.AdjustRight(1); + aRect.AdjustTop(-1); + aRect.AdjustBottom(1); + + if( bBalloon ) + Help::ShowBalloon( this, rEvt.GetMousePosPixel(), aRect, sText ); + else + { + // the show the help + OUString sDisplayText(ClipLongToolTip(sText)); + Help::ShowQuickHelp(this, aRect, sDisplayText, nStyle); + } + } + + bContinue = false; + } + + } + + if( bContinue ) + Window::RequestHelp( rEvt ); +} + +void SwEditWin::PrePaint(vcl::RenderContext& /*rRenderContext*/) +{ + if (SwWrtShell* pWrtShell = GetView().GetWrtShellPtr()) + pWrtShell->PrePaint(); +} + +void SwEditWin::Paint(vcl::RenderContext& rRenderContext, const tools::Rectangle& rRect) +{ + SwWrtShell* pWrtShell = GetView().GetWrtShellPtr(); + if(!pWrtShell) + return; + + if ( GetView().GetVisArea().GetWidth() <= 0 || + GetView().GetVisArea().GetHeight() <= 0 ) + Invalidate( rRect ); + else + { + pWrtShell->setOutputToWindow(true); + bool bTiledPainting = false; + if (comphelper::LibreOfficeKit::isActive()) + { + bTiledPainting = comphelper::LibreOfficeKit::isTiledPainting(); + comphelper::LibreOfficeKit::setTiledPainting(true); + } + pWrtShell->Paint(rRenderContext, rRect); + if (comphelper::LibreOfficeKit::isActive()) + { + comphelper::LibreOfficeKit::setTiledPainting(bTiledPainting); + } + pWrtShell->setOutputToWindow(false); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ -- cgit v1.2.3