Files
loongoffice/chart2/source/controller/main/SelectionHelper.cxx
Armin Le Grand 4b4942224b SOSAW080: Derive SdrObjGroup from SdrObjList
Also simplify parent/child relationships, get rid
of double data (SdrPage/Parent infos in SdrObjects,
also in SdrObjList). This is all not needed - when a
SdrObject is inserted to a SdrPage, get SdrPage by
traveling over parents (no double info, member as soon
as inserted, ...).
More cleanups/reworks included, will need some more
cleanups, too.

Stabilizing: SetRectsDirty/DefaultStyleSheet

Had to correct the SetRectsDirty stuff for 3D due to
going down the hierarchy while the 2D implementation
goes the other direction -> endless loops. Added special
handling for 3D stuff for now (will be chnaged again when
SnapRect is no longer needed at SdrObject level).
Also had to adapt how the DefaultStyleSheet is set at
incarnated SdrObjects - better: their properties. Since
we now always have a SdrModel, it is possible to correctly
initialize with the correct default StyleSheet from that
SdrModel.
This needs to be done after ForceDefaultAttributes and in a
way that again deletes Items that are set in the StyleSheet.
This leads to an error in CppunitTest_sd_import_tests where
I checked tdf100491 - it is okay and thus I change the control
instance of the imported, XML-dumped file.
The less hard attributes, the better for Styles in general.

Cleanup of comments for last two commits

Corrected SvxShape::getParent()

Needed to get the direct parent, so test for SdrObject
first (to get SdrObjGroup/E3DScene), for SdrPage second

Fixed CppunitTest_sc_subsequent_export_test

Several problems arose. The used SdrCaptionObj was
Cloned, but the clone not inserted to a SdrPage. This
leads to not being able to access a UNO API imlementation
of the SdrPage (SvxPage) on lower levels.
It worked before due to SdrObject having a SdrPage*
additionally to being added to a SdrPage - this is exactly
the main cleanup this change does.
Looked for why it is cloned, could see no reasons. The
SdrCaptionObj exists during all im/export, not difference
to other SdrObjects (that do not get cloned). It is not
changed in any way. It *might* be to suppress a crash that
happened due to UNO API Service emfio/emfio not being
available in the UnitTest scenario. Interestingly it
did not crash with the cloned SdrCaptionObj, but the
Graphic exported was probably wrong.
Fixed by no longer Cloning the SdrCaptionObj and adding
emfio/emfio UNO API Service.

d139f821a5b39535a3e7b9c6261df7e18f8ae8ac
910e7f4bc628a715fda7545dffaf3369d5e76ea0
ca1de01b723051e09ac37d7ec7bba978beea41c5
3a76da1471dfe75e69847f64a6a3519ad21c8c9c

Change-Id: I986586e326b563acebf00d931a7084c6eb09e5f8
Reviewed-on: https://gerrit.libreoffice.org/54689
Tested-by: Jenkins <ci@libreoffice.org>
Reviewed-by: Armin Le Grand <Armin.Le.Grand@cib.de>
2018-05-25 12:31:32 +02:00

654 lines
22 KiB
C++

/* -*- 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 <SelectionHelper.hxx>
#include <ObjectIdentifier.hxx>
#include <DiagramHelper.hxx>
#include <ChartModelHelper.hxx>
#include <svx/svdpage.hxx>
#include <svx/svditer.hxx>
#include <svx/obj3d.hxx>
#include <svx/svdopath.hxx>
#include <vcl/svapp.hxx>
#include <basegfx/point/b2dpoint.hxx>
#include <com/sun/star/beans/XPropertySet.hpp>
namespace chart
{
using namespace ::com::sun::star;
namespace
{
OUString lcl_getObjectName( SdrObject const * pObj )
{
if(pObj)
return pObj->GetName();
return OUString();
}
void impl_selectObject( SdrObject* pObjectToSelect, DrawViewWrapper& rDrawViewWrapper )
{
SolarMutexGuard aSolarGuard;
if(pObjectToSelect)
{
SelectionHelper aSelectionHelper( pObjectToSelect );
SdrObject* pMarkObj = aSelectionHelper.getObjectToMark();
rDrawViewWrapper.setMarkHandleProvider(&aSelectionHelper);
rDrawViewWrapper.MarkObject(pMarkObj);
rDrawViewWrapper.setMarkHandleProvider(nullptr);
}
}
}//anonymous namespace
bool Selection::hasSelection()
{
return m_aSelectedOID.isValid();
}
OUString const & Selection::getSelectedCID()
{
return m_aSelectedOID.getObjectCID();
}
uno::Reference< drawing::XShape > const & Selection::getSelectedAdditionalShape()
{
return m_aSelectedOID.getAdditionalShape();
}
bool Selection::setSelection( const OUString& rCID )
{
if ( rCID != m_aSelectedOID.getObjectCID() )
{
m_aSelectedOID = ObjectIdentifier( rCID );
return true;
}
return false;
}
bool Selection::setSelection( const uno::Reference< drawing::XShape >& xShape )
{
if ( !( xShape == m_aSelectedOID.getAdditionalShape() ) )
{
clearSelection();
m_aSelectedOID = ObjectIdentifier( xShape );
return true;
}
return false;
}
void Selection::clearSelection()
{
m_aSelectedOID = ObjectIdentifier();
m_aSelectedOID_beforeMouseDown = ObjectIdentifier();
m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing = ObjectIdentifier();
}
bool Selection::maybeSwitchSelectionAfterSingleClickWasEnsured()
{
if ( m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing.isValid()
&& m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing != m_aSelectedOID )
{
m_aSelectedOID = m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing;
m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing = ObjectIdentifier();
return true;
}
return false;
}
void Selection::resetPossibleSelectionAfterSingleClickWasEnsured()
{
if ( m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing.isValid() )
{
m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing = ObjectIdentifier();
}
}
void Selection::remindSelectionBeforeMouseDown()
{
m_aSelectedOID_beforeMouseDown = m_aSelectedOID;
}
bool Selection::isSelectionDifferentFromBeforeMouseDown() const
{
return ( m_aSelectedOID != m_aSelectedOID_beforeMouseDown );
}
void Selection::applySelection( DrawViewWrapper* pDrawViewWrapper )
{
if( pDrawViewWrapper )
{
{
SolarMutexGuard aSolarGuard;
pDrawViewWrapper->UnmarkAll();
}
SdrObject* pObjectToSelect = nullptr;
if ( m_aSelectedOID.isAutoGeneratedObject() )
{
pObjectToSelect = pDrawViewWrapper->getNamedSdrObject( m_aSelectedOID.getObjectCID() );
}
else if( m_aSelectedOID.isAdditionalShape() )
{
pObjectToSelect = DrawViewWrapper::getSdrObject( m_aSelectedOID.getAdditionalShape() );
}
impl_selectObject( pObjectToSelect, *pDrawViewWrapper );
}
}
void Selection::adaptSelectionToNewPos( const Point& rMousePos, DrawViewWrapper const * pDrawViewWrapper
, bool bIsRightMouse, bool bWaitingForDoubleClick )
{
if( pDrawViewWrapper )
{
//do not toggle multiclick selection if right clicked on the selected object or waiting for double click
bool bAllowMultiClickSelectionChange = !bIsRightMouse && !bWaitingForDoubleClick;
ObjectIdentifier aLastSelectedObject( m_aSelectedOID );
SolarMutexGuard aSolarGuard;
//bAllowMultiClickSelectionChange==true -> a second click on the same object can lead to a changed selection (e.g. series -> single data point)
//get object to select:
{
m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing = ObjectIdentifier();
//the search for the object to select starts with the hit object deepest in the grouping hierarchy (a leaf in the tree)
//further we travel along the grouping hierarchy from child to parent
SdrObject* pNewObj = pDrawViewWrapper->getHitObject(rMousePos);
m_aSelectedOID = ObjectIdentifier( lcl_getObjectName( pNewObj ) );//name of pNewObj
//ignore handle only objects for hit test
while( pNewObj && m_aSelectedOID.getObjectCID().match( "HandlesOnly" ) )
{
pNewObj->SetMarkProtect(true);
pNewObj = pDrawViewWrapper->getHitObject(rMousePos);
m_aSelectedOID = ObjectIdentifier( lcl_getObjectName( pNewObj ) );
}
//accept only named objects while searching for the object to select
//this call may change m_aSelectedOID
if ( SelectionHelper::findNamedParent( pNewObj, m_aSelectedOID, true ) )
{
//if the so far found object is a multi click object further steps are necessary
while( ObjectIdentifier::isMultiClickObject( m_aSelectedOID.getObjectCID() ) )
{
bool bSameObjectAsLastSelected = ( aLastSelectedObject == m_aSelectedOID );
if( bSameObjectAsLastSelected )
{
//if the same child is clicked again don't go up further
break;
}
if ( ObjectIdentifier::areSiblings( aLastSelectedObject.getObjectCID(), m_aSelectedOID.getObjectCID() ) )
{
//if a sibling of the last selected object is clicked don't go up further
break;
}
ObjectIdentifier aLastChild = m_aSelectedOID;
if ( !SelectionHelper::findNamedParent( pNewObj, m_aSelectedOID, false ) )
{
//take the one found so far
break;
}
//if the last selected object is found don't go up further
//but take the last child if selection change is allowed
if ( aLastSelectedObject == m_aSelectedOID )
{
if( bAllowMultiClickSelectionChange )
{
m_aSelectedOID = aLastChild;
}
else
m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing = aLastChild;
break;
}
}
OSL_ENSURE(m_aSelectedOID.isValid(), "somehow lost selected object");
}
else
{
//maybe an additional shape was hit
if ( pNewObj )
{
m_aSelectedOID = ObjectIdentifier( uno::Reference< drawing::XShape >( pNewObj->getUnoShape(), uno::UNO_QUERY ) );
}
else
{
m_aSelectedOID = ObjectIdentifier();
}
}
if ( !m_aSelectedOID.isAdditionalShape() )
{
OUString aPageCID( ObjectIdentifier::createClassifiedIdentifier( OBJECTTYPE_PAGE, OUString() ) );//@todo read CID from model
if ( !m_aSelectedOID.isAutoGeneratedObject() )
{
m_aSelectedOID = ObjectIdentifier( aPageCID );
}
//check whether the diagram was hit but not selected (e.g. because it has no filling):
OUString aDiagramCID = ObjectIdentifier::createClassifiedIdentifier( OBJECTTYPE_DIAGRAM, OUString::number( 0 ) );
OUString aWallCID( ObjectIdentifier::createClassifiedIdentifier( OBJECTTYPE_DIAGRAM_WALL, OUString() ) );//@todo read CID from model
bool bBackGroundHit = m_aSelectedOID.getObjectCID() == aPageCID || m_aSelectedOID.getObjectCID() == aWallCID || !m_aSelectedOID.isAutoGeneratedObject();
if( bBackGroundHit )
{
//todo: if more than one diagram is available in future do check the list of all diagrams here
SdrObject* pDiagram = pDrawViewWrapper->getNamedSdrObject( aDiagramCID );
if( pDiagram )
{
if( DrawViewWrapper::IsObjectHit( pDiagram, rMousePos ) )
{
m_aSelectedOID = ObjectIdentifier( aDiagramCID );
}
}
}
//check whether the legend was hit but not selected (e.g. because it has no filling):
if( bBackGroundHit || m_aSelectedOID.getObjectCID() == aDiagramCID )
{
OUString aLegendCID( ObjectIdentifier::createClassifiedIdentifierForParticle( ObjectIdentifier::createParticleForLegend(nullptr) ) );//@todo read CID from model
SdrObject* pLegend = pDrawViewWrapper->getNamedSdrObject( aLegendCID );
if( pLegend )
{
if( DrawViewWrapper::IsObjectHit( pLegend, rMousePos ) )
{
m_aSelectedOID = ObjectIdentifier( aLegendCID );
}
}
}
}
}
if ( bIsRightMouse && m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing.isValid() )
{
m_aSelectedOID_selectOnlyIfNoDoubleClickIsFollowing = ObjectIdentifier();
}
}
}
bool Selection::isResizeableObjectSelected()
{
ObjectType eObjectType = m_aSelectedOID.getObjectType();
switch( eObjectType )
{
case OBJECTTYPE_DIAGRAM:
case OBJECTTYPE_DIAGRAM_WALL:
case OBJECTTYPE_SHAPE:
case OBJECTTYPE_LEGEND:
return true;
default:
return false;
}
}
bool Selection::isRotateableObjectSelected( const uno::Reference< frame::XModel >& xChartModel )
{
return SelectionHelper::isRotateableObject( m_aSelectedOID.getObjectCID(), xChartModel );
}
bool Selection::isDragableObjectSelected()
{
return m_aSelectedOID.isDragableObject();
}
bool Selection::isAdditionalShapeSelected() const
{
return m_aSelectedOID.isAdditionalShape();
}
bool SelectionHelper::findNamedParent( SdrObject*& pInOutObject
, OUString& rOutName
, bool bGivenObjectMayBeResult )
{
SolarMutexGuard aSolarGuard;
//find the deepest named group
SdrObject* pObj = pInOutObject;
OUString aName;
if( bGivenObjectMayBeResult )
aName = lcl_getObjectName( pObj );
while( pObj && !ObjectIdentifier::isCID( aName ) )
{
SdrObjList* pObjList = pObj->getParentOfSdrObject();
if( !pObjList )
return false;
SdrObject* pOwner = pObjList->getSdrObjectFromSdrObjList();
if( !pOwner )
return false;
pObj = pOwner;
aName = lcl_getObjectName( pObj );
}
if(!pObj)
return false;
if(aName.isEmpty())
return false;
pInOutObject = pObj;
rOutName = aName;
return true;
}
bool SelectionHelper::findNamedParent( SdrObject*& pInOutObject
, ObjectIdentifier& rOutObject
, bool bGivenObjectMayBeResult )
{
OUString aName;
if ( findNamedParent( pInOutObject, aName, bGivenObjectMayBeResult ) )
{
rOutObject = ObjectIdentifier( aName );
return true;
}
return false;
}
bool SelectionHelper::isDragableObjectHitTwice( const Point& rMPos
, const OUString& rNameOfSelectedObject
, const DrawViewWrapper& rDrawViewWrapper )
{
if(rNameOfSelectedObject.isEmpty())
return false;
if( !ObjectIdentifier::isDragableObject(rNameOfSelectedObject) )
return false;
SolarMutexGuard aSolarGuard;
SdrObject* pObj = rDrawViewWrapper.getNamedSdrObject( rNameOfSelectedObject );
return DrawViewWrapper::IsObjectHit( pObj, rMPos );
}
OUString SelectionHelper::getHitObjectCID(
const Point& rMPos,
DrawViewWrapper const & rDrawViewWrapper,
bool bGetDiagramInsteadOf_Wall )
{
SolarMutexGuard aSolarGuard;
OUString aRet;
SdrObject* pNewObj = rDrawViewWrapper.getHitObject(rMPos);
aRet = lcl_getObjectName( pNewObj );//name of pNewObj
//ignore handle only objects for hit test
while( pNewObj && aRet.match("HandlesOnly") )
{
pNewObj->SetMarkProtect(true);
pNewObj = rDrawViewWrapper.getHitObject(rMPos);
aRet = lcl_getObjectName( pNewObj );
}
//accept only named objects while searching for the object to select
if( !findNamedParent( pNewObj, aRet, true ) )
{
aRet.clear();
}
OUString aPageCID( ObjectIdentifier::createClassifiedIdentifier( OBJECTTYPE_PAGE, OUString() ) );//@todo read CID from model
//get page when nothing was hit
if( aRet.isEmpty() && !pNewObj )
{
aRet = aPageCID;
}
//get diagram instead wall or page if hit inside diagram
if( !aRet.isEmpty() )
{
if( aRet == aPageCID )
{
OUString aDiagramCID = ObjectIdentifier::createClassifiedIdentifier( OBJECTTYPE_DIAGRAM, OUString::number( 0 ) );
//todo: if more than one diagram is available in future do check the list of all diagrams here
SdrObject* pDiagram = rDrawViewWrapper.getNamedSdrObject( aDiagramCID );
if( pDiagram )
{
if( DrawViewWrapper::IsObjectHit( pDiagram, rMPos ) )
{
aRet = aDiagramCID;
}
}
}
else if( bGetDiagramInsteadOf_Wall )
{
OUString aWallCID( ObjectIdentifier::createClassifiedIdentifier( OBJECTTYPE_DIAGRAM_WALL, OUString() ) );//@todo read CID from model
if( aRet == aWallCID )
{
OUString aDiagramCID = ObjectIdentifier::createClassifiedIdentifier( OBJECTTYPE_DIAGRAM, OUString::number( 0 ) );
aRet = aDiagramCID;
}
}
}
return aRet;
// \\- solar mutex
}
bool SelectionHelper::isRotateableObject( const OUString& rCID
, const uno::Reference< frame::XModel >& xChartModel )
{
if( !ObjectIdentifier::isRotateableObject( rCID ) )
return false;
sal_Int32 nDimensionCount = DiagramHelper::getDimension( ChartModelHelper::findDiagram( xChartModel ) );
return nDimensionCount == 3;
}
SelectionHelper::SelectionHelper( SdrObject* pSelectedObj )
: m_pSelectedObj( pSelectedObj ), m_pMarkObj(nullptr)
{
}
SelectionHelper::~SelectionHelper()
{
}
bool SelectionHelper::getFrameDragSingles()
{
bool bFrameDragSingles = true;//true == green == surrounding handles
if( m_pSelectedObj && dynamic_cast<const E3dObject*>( m_pSelectedObj) != nullptr )
bFrameDragSingles = false;
return bFrameDragSingles;
}
SdrObject* SelectionHelper::getMarkHandlesObject( SdrObject* pObj )
{
if(!pObj)
return nullptr;
OUString aName( lcl_getObjectName( pObj ) );
if( aName.match("MarkHandles") || aName.match("HandlesOnly") )
return pObj;
if( !aName.isEmpty() )//don't get the markhandles of a different object
return nullptr;
//search for a child with name "MarkHandles" or "HandlesOnly"
SolarMutexGuard aSolarGuard;
SdrObjList* pSubList = pObj->GetSubList();
if(pSubList)
{
SdrObjListIter aIterator(pSubList, SdrIterMode::Flat);
while (aIterator.IsMore())
{
SdrObject* pMarkHandles = SelectionHelper::getMarkHandlesObject( aIterator.Next() );
if( pMarkHandles )
return pMarkHandles;
}
}
return nullptr;
}
SdrObject* SelectionHelper::getObjectToMark()
{
//return the selected object itself
//or a specific other object if that exsists
SdrObject* pObj = m_pSelectedObj;
m_pMarkObj = pObj;
//search for a child with name "MarkHandles" or "HandlesOnly"
if(pObj)
{
SolarMutexGuard aSolarGuard;
SdrObjList* pSubList = pObj->GetSubList();
if(pSubList)
{
SdrObjListIter aIterator(pSubList, SdrIterMode::Flat);
while (aIterator.IsMore())
{
SdrObject* pMarkHandles = SelectionHelper::getMarkHandlesObject( aIterator.Next() );
if( pMarkHandles )
{
m_pMarkObj = pMarkHandles;
break;
}
}
}
}
return m_pMarkObj;
}
E3dScene* SelectionHelper::getSceneToRotate( SdrObject* pObj )
{
//search whether the object or one of its children is a 3D object
//if so, return the accessory 3DScene
E3dObject* pRotateable = nullptr;
if(pObj)
{
pRotateable = dynamic_cast<E3dObject*>(pObj);
if( !pRotateable )
{
SolarMutexGuard aSolarGuard;
SdrObjList* pSubList = pObj->GetSubList();
if(pSubList)
{
SdrObjListIter aIterator(pSubList, SdrIterMode::DeepWithGroups);
while( aIterator.IsMore() && !pRotateable )
{
SdrObject* pSubObj = aIterator.Next();
pRotateable = dynamic_cast<E3dObject*>(pSubObj);
}
}
}
}
E3dScene* pScene = nullptr;
if(pRotateable)
{
SolarMutexGuard aSolarGuard;
pScene = pRotateable->GetScene();
}
return pScene;
}
bool SelectionHelper::getMarkHandles( SdrHdlList& rHdlList )
{
SolarMutexGuard aSolarGuard;
//@todo -> more flexible handle creation
//2 scenarios possible:
//1. add an additional invisible shape as a child to the selected object
//this child needs to be named somehow and handles need to be generated therefrom ...
//or 2. offer a central service per view where renderer and so can register for handle creation for a special shape
//.. or 3. feature from drawinglayer to create handles for each shape ... (bad performance ... ?) ?
//scenario 1 is now used:
//if a child with name MarkHandles exsists
//this child is marked instead of the logical selected object
/*
//if a special mark object was found
//that object should be used for marking only
if( m_pMarkObj != m_pSelectedObj)
return false;
*/
//if a special mark object was found
//that object should be used to create handles from
if( m_pMarkObj && m_pMarkObj != m_pSelectedObj)
{
rHdlList.Clear();
if( dynamic_cast<const SdrPathObj*>( m_pMarkObj) != nullptr )
{
//if th object is a polygon
//from each point a handle is generated
const ::basegfx::B2DPolyPolygon& rPolyPolygon = static_cast<SdrPathObj*>(m_pMarkObj)->GetPathPoly();
for( sal_uInt32 nN = 0; nN < rPolyPolygon.count(); nN++)
{
const ::basegfx::B2DPolygon aPolygon(rPolyPolygon.getB2DPolygon(nN));
for( sal_uInt32 nM = 0; nM < aPolygon.count(); nM++)
{
const ::basegfx::B2DPoint aPoint(aPolygon.getB2DPoint(nM));
SdrHdl* pHdl = new SdrHdl(Point(basegfx::fround(aPoint.getX()), basegfx::fround(aPoint.getY())), SdrHdlKind::Poly);
rHdlList.AddHdl(pHdl);
}
}
return true;
}
else
return false; //use the special MarkObject for marking
}
//@todo:
//add and document good marking defaults ...
rHdlList.Clear();
SdrObject* pObj = m_pSelectedObj;
if(!pObj)
return false;
SdrObjList* pSubList = pObj->GetSubList();
if( !pSubList )//no group object !pObj->IsGroupObject()
return false;
OUString aName( lcl_getObjectName( pObj ) );
ObjectType eObjectType( ObjectIdentifier::getObjectType( aName ) );
if( eObjectType == OBJECTTYPE_DATA_POINT
|| eObjectType == OBJECTTYPE_DATA_LABEL
|| eObjectType == OBJECTTYPE_LEGEND_ENTRY
|| eObjectType == OBJECTTYPE_AXIS_UNITLABEL )
{
return false;
}
SdrObjListIter aIterator(pSubList, SdrIterMode::Flat);
while (aIterator.IsMore())
{
SdrObject* pSubObj = aIterator.Next();
if( eObjectType == OBJECTTYPE_DATA_SERIES )
{
OUString aSubName( lcl_getObjectName( pSubObj ) );
ObjectType eSubObjectType( ObjectIdentifier::getObjectType( aSubName ) );
if( eSubObjectType!=OBJECTTYPE_DATA_POINT )
return false;
}
Point aPos = pSubObj->GetCurrentBoundRect().Center();
SdrHdl* pHdl = new SdrHdl(aPos,SdrHdlKind::Poly);
rHdlList.AddHdl(pHdl);
}
return true;
}
} //namespace chart
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */