0fa827dbb2
Obsoleted by commit2484de6728
(Remove non-const Sequence::begin()/end() in internal code, 2021-10-15) and commitfb3c04bd19
(Drop non-const Sequence::operator[] in internal code, 2021-11-05). Change-Id: Idbafef5d34c0d4771cbbf75b9db9712e504164cd Reviewed-on: https://gerrit.libreoffice.org/c/core/+/162640 Tested-by: Jenkins Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
577 lines
18 KiB
C++
577 lines
18 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 <sal/types.h>
|
|
#include <rtl/process.h>
|
|
#include <osl/diagnose.h>
|
|
#include <sal/log.hxx>
|
|
|
|
#include "DOTransferable.hxx"
|
|
#include "ImplHelper.hxx"
|
|
#include "WinClip.hxx"
|
|
#include "WinClipboard.hxx"
|
|
#include "DTransHelper.hxx"
|
|
#include "TxtCnvtHlp.hxx"
|
|
#include "MimeAttrib.hxx"
|
|
#include "FmtFilter.hxx"
|
|
#include "Fetc.hxx"
|
|
#include <com/sun/star/container/NoSuchElementException.hpp>
|
|
#include <com/sun/star/datatransfer/MimeContentTypeFactory.hpp>
|
|
#include <com/sun/star/datatransfer/UnsupportedFlavorException.hpp>
|
|
#include <com/sun/star/io/IOException.hpp>
|
|
#include <com/sun/star/lang/IllegalArgumentException.hpp>
|
|
|
|
using namespace osl;
|
|
using namespace cppu;
|
|
using namespace com::sun::star::uno;
|
|
using namespace com::sun::star::datatransfer;
|
|
using namespace com::sun::star::io;
|
|
using namespace com::sun::star::lang;
|
|
using namespace com::sun::star::container;
|
|
|
|
namespace
|
|
{
|
|
const Type CPPUTYPE_SEQINT8 = cppu::UnoType<Sequence< sal_Int8 >>::get();
|
|
const Type CPPUTYPE_OUSTRING = cppu::UnoType<OUString>::get();
|
|
|
|
bool isValidFlavor( const DataFlavor& aFlavor )
|
|
{
|
|
return ( aFlavor.MimeType.getLength( ) &&
|
|
( ( aFlavor.DataType == CPPUTYPE_SEQINT8 ) ||
|
|
( aFlavor.DataType == CPPUTYPE_OUSTRING ) ) );
|
|
}
|
|
|
|
void clipDataToByteStream( CLIPFORMAT cf, STGMEDIUM stgmedium, CDOTransferable::ByteSequence_t& aByteSequence )
|
|
{
|
|
CStgTransferHelper memTransferHelper;
|
|
LPSTREAM pStream = nullptr;
|
|
|
|
switch( stgmedium.tymed )
|
|
{
|
|
case TYMED_HGLOBAL:
|
|
memTransferHelper.init( stgmedium.hGlobal );
|
|
break;
|
|
|
|
case TYMED_MFPICT:
|
|
memTransferHelper.init( stgmedium.hMetaFilePict );
|
|
break;
|
|
|
|
case TYMED_ENHMF:
|
|
memTransferHelper.init( stgmedium.hEnhMetaFile );
|
|
break;
|
|
|
|
case TYMED_ISTREAM:
|
|
pStream = stgmedium.pstm;
|
|
break;
|
|
|
|
default:
|
|
throw UnsupportedFlavorException( );
|
|
break;
|
|
}
|
|
|
|
if (pStream)
|
|
{
|
|
// We have a stream, read from it.
|
|
STATSTG aStat;
|
|
HRESULT hr = pStream->Stat(&aStat, STATFLAG_NONAME);
|
|
if (FAILED(hr))
|
|
{
|
|
SAL_WARN("vcl.win.dtrans", "clipDataToByteStream: Stat() failed");
|
|
return;
|
|
}
|
|
|
|
size_t nMemSize = aStat.cbSize.QuadPart;
|
|
aByteSequence.realloc(nMemSize);
|
|
LARGE_INTEGER li;
|
|
li.QuadPart = 0;
|
|
hr = pStream->Seek(li, STREAM_SEEK_SET, nullptr);
|
|
if (FAILED(hr))
|
|
{
|
|
SAL_WARN("vcl.win.dtrans", "clipDataToByteStream: Seek() failed");
|
|
}
|
|
|
|
ULONG nRead = 0;
|
|
hr = pStream->Read(aByteSequence.getArray(), nMemSize, &nRead);
|
|
if (FAILED(hr))
|
|
{
|
|
SAL_WARN("vcl.win.dtrans", "clipDataToByteStream: Read() failed");
|
|
}
|
|
if (nRead < nMemSize)
|
|
{
|
|
SAL_WARN("vcl.win.dtrans", "clipDataToByteStream: Read() was partial");
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
int nMemSize = memTransferHelper.memSize( cf );
|
|
aByteSequence.realloc( nMemSize );
|
|
memTransferHelper.read( aByteSequence.getArray( ), nMemSize );
|
|
}
|
|
|
|
OUString byteStreamToOUString( CDOTransferable::ByteSequence_t& aByteStream )
|
|
{
|
|
sal_Int32 nWChars;
|
|
sal_Int32 nMemSize = aByteStream.getLength( );
|
|
|
|
// if there is a trailing L"\0" subtract 1 from length
|
|
// for unknown reason, the sequence may sometimes arrive empty
|
|
if ( aByteStream.getLength( ) > 1 &&
|
|
0 == aByteStream[ aByteStream.getLength( ) - 2 ] &&
|
|
0 == aByteStream[ aByteStream.getLength( ) - 1 ] )
|
|
nWChars = static_cast< sal_Int32 >( nMemSize / sizeof( sal_Unicode ) ) - 1;
|
|
else
|
|
nWChars = static_cast< sal_Int32 >( nMemSize / sizeof( sal_Unicode ) );
|
|
|
|
return OUString( reinterpret_cast< sal_Unicode* >( aByteStream.getArray( ) ), nWChars );
|
|
}
|
|
|
|
Any byteStreamToAny( CDOTransferable::ByteSequence_t& aByteStream, const Type& aRequestedDataType )
|
|
{
|
|
Any aAny;
|
|
|
|
if ( aRequestedDataType == CPPUTYPE_OUSTRING )
|
|
{
|
|
OUString str = byteStreamToOUString( aByteStream );
|
|
if (str.isEmpty())
|
|
throw RuntimeException();
|
|
aAny <<= str;
|
|
}
|
|
else
|
|
aAny <<= aByteStream;
|
|
|
|
return aAny;
|
|
}
|
|
|
|
bool cmpFullMediaType(
|
|
const Reference< XMimeContentType >& xLhs, const Reference< XMimeContentType >& xRhs )
|
|
{
|
|
return xLhs->getFullMediaType().equalsIgnoreAsciiCase( xRhs->getFullMediaType( ) );
|
|
}
|
|
|
|
bool cmpAllContentTypeParameter(
|
|
const Reference< XMimeContentType >& xLhs, const Reference< XMimeContentType >& xRhs )
|
|
{
|
|
Sequence< OUString > xLhsFlavors = xLhs->getParameters( );
|
|
Sequence< OUString > xRhsFlavors = xRhs->getParameters( );
|
|
bool bRet = true;
|
|
|
|
try
|
|
{
|
|
if ( xLhsFlavors.getLength( ) == xRhsFlavors.getLength( ) )
|
|
{
|
|
OUString pLhs;
|
|
OUString pRhs;
|
|
|
|
for ( sal_Int32 i = 0; i < xLhsFlavors.getLength( ); i++ )
|
|
{
|
|
pLhs = xLhs->getParameterValue( xLhsFlavors[i] );
|
|
pRhs = xRhs->getParameterValue( xLhsFlavors[i] );
|
|
|
|
if ( !pLhs.equalsIgnoreAsciiCase( pRhs ) )
|
|
{
|
|
bRet = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
bRet = false;
|
|
}
|
|
catch( NoSuchElementException& )
|
|
{
|
|
bRet = false;
|
|
}
|
|
catch( IllegalArgumentException& )
|
|
{
|
|
bRet = false;
|
|
}
|
|
|
|
return bRet;
|
|
}
|
|
|
|
} // end namespace
|
|
|
|
CDOTransferable::CDOTransferable(
|
|
const Reference< XComponentContext >& rxContext, IDataObjectPtr rDataObject ) :
|
|
m_rDataObject( rDataObject ),
|
|
m_xContext( rxContext ),
|
|
m_DataFormatTranslator( rxContext ),
|
|
m_bUnicodeRegistered( false ),
|
|
m_TxtFormatOnClipboard( CF_INVALID )
|
|
{
|
|
initFlavorList();
|
|
}
|
|
|
|
CDOTransferable::CDOTransferable(
|
|
const Reference<XComponentContext>& rxContext,
|
|
const css::uno::Reference<css::datatransfer::clipboard::XClipboard>& xClipboard,
|
|
const std::vector<sal_uInt32>& rFormats)
|
|
: m_xClipboard(xClipboard)
|
|
, m_xContext(rxContext)
|
|
, m_DataFormatTranslator(rxContext)
|
|
, m_bUnicodeRegistered(false)
|
|
, m_TxtFormatOnClipboard(CF_INVALID)
|
|
{
|
|
initFlavorListFromFormatList(rFormats);
|
|
}
|
|
|
|
Any SAL_CALL CDOTransferable::getTransferData( const DataFlavor& aFlavor )
|
|
{
|
|
OSL_ASSERT( isValidFlavor( aFlavor ) );
|
|
|
|
std::unique_lock aGuard( m_aMutex );
|
|
|
|
// convert dataflavor to formatetc
|
|
|
|
CFormatEtc fetc = m_DataFormatTranslator.getFormatEtcFromDataFlavor( aFlavor );
|
|
OSL_ASSERT( CF_INVALID != fetc.getClipformat() );
|
|
|
|
// get the data from clipboard in a byte stream
|
|
|
|
ByteSequence_t clipDataStream;
|
|
|
|
try
|
|
{
|
|
clipDataStream = getClipboardData( fetc );
|
|
}
|
|
catch( UnsupportedFlavorException& )
|
|
{
|
|
if ( CDataFormatTranslator::isUnicodeTextFormat( fetc.getClipformat( ) ) &&
|
|
m_bUnicodeRegistered )
|
|
{
|
|
OUString aUnicodeText = synthesizeUnicodeText( );
|
|
Any aAny( aUnicodeText );
|
|
return aAny;
|
|
}
|
|
// #i124085# CF_DIBV5 should not be possible, but keep for reading from the
|
|
// clipboard for being on the safe side
|
|
else if(CF_DIBV5 == fetc.getClipformat())
|
|
{
|
|
// #i123407# CF_DIBV5 has priority; if the try to fetch this failed,
|
|
// check CF_DIB availability as an alternative
|
|
fetc.setClipformat(CF_DIB);
|
|
|
|
clipDataStream = getClipboardData( fetc );
|
|
// pass UnsupportedFlavorException out, tried all possibilities
|
|
}
|
|
else
|
|
throw; // pass through exception
|
|
}
|
|
|
|
// return the data as any
|
|
|
|
return byteStreamToAny( clipDataStream, aFlavor.DataType );
|
|
}
|
|
|
|
// getTransferDataFlavors
|
|
|
|
Sequence< DataFlavor > SAL_CALL CDOTransferable::getTransferDataFlavors( )
|
|
{
|
|
return m_FlavorList;
|
|
}
|
|
|
|
// isDataFlavorSupported
|
|
// returns true if we find a DataFlavor with the same MimeType and
|
|
// DataType
|
|
|
|
sal_Bool SAL_CALL CDOTransferable::isDataFlavorSupported( const DataFlavor& aFlavor )
|
|
{
|
|
OSL_ASSERT( isValidFlavor( aFlavor ) );
|
|
|
|
for (DataFlavor const& df : m_FlavorList)
|
|
if ( compareDataFlavors( aFlavor, df ) )
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
// the list of dataflavors currently on the clipboard will be initialized
|
|
// only once; if the client of this Transferable will hold a reference
|
|
// to it and the underlying clipboard content changes, the client does
|
|
// possible operate on an invalid list
|
|
// if there is only text on the clipboard we will also offer unicode text
|
|
// an synthesize this format on the fly if requested, to accomplish this
|
|
// we save the first offered text format which we will later use for the
|
|
// conversion
|
|
|
|
void CDOTransferable::initFlavorList( )
|
|
{
|
|
std::vector<sal_uInt32> aFormats;
|
|
sal::systools::COMReference<IEnumFORMATETC> pEnumFormatEtc;
|
|
HRESULT hr = m_rDataObject->EnumFormatEtc( DATADIR_GET, &pEnumFormatEtc );
|
|
if ( SUCCEEDED( hr ) )
|
|
{
|
|
pEnumFormatEtc->Reset( );
|
|
|
|
FORMATETC fetc;
|
|
while ( S_OK == pEnumFormatEtc->Next( 1, &fetc, nullptr ) )
|
|
{
|
|
aFormats.push_back(fetc.cfFormat);
|
|
// see MSDN IEnumFORMATETC
|
|
CoTaskMemFree( fetc.ptd );
|
|
}
|
|
initFlavorListFromFormatList(aFormats);
|
|
}
|
|
}
|
|
|
|
void CDOTransferable::initFlavorListFromFormatList(const std::vector<sal_uInt32>& rFormats)
|
|
{
|
|
for (sal_uInt32 cfFormat : rFormats)
|
|
{
|
|
// we use locales only to determine the
|
|
// charset if there is text on the clipboard
|
|
// we don't offer this format
|
|
if (CF_LOCALE == cfFormat)
|
|
continue;
|
|
|
|
// if text or oemtext is offered we pretend to have unicode text
|
|
if (CDataFormatTranslator::isTextFormat(cfFormat))
|
|
{
|
|
if (!m_bUnicodeRegistered)
|
|
{
|
|
m_TxtFormatOnClipboard = cfFormat;
|
|
m_bUnicodeRegistered = true;
|
|
|
|
// register unicode text as format
|
|
addSupportedFlavor(formatEtcToDataFlavor(CF_UNICODETEXT));
|
|
}
|
|
}
|
|
else
|
|
addSupportedFlavor(formatEtcToDataFlavor(cfFormat));
|
|
}
|
|
}
|
|
|
|
inline
|
|
void CDOTransferable::addSupportedFlavor( const DataFlavor& aFlavor )
|
|
{
|
|
// we ignore all formats that couldn't be translated
|
|
if ( aFlavor.MimeType.getLength( ) )
|
|
{
|
|
OSL_ASSERT( isValidFlavor( aFlavor ) );
|
|
|
|
m_FlavorList.realloc( m_FlavorList.getLength( ) + 1 );
|
|
m_FlavorList.getArray()[m_FlavorList.getLength( ) - 1] = aFlavor;
|
|
}
|
|
}
|
|
|
|
DataFlavor CDOTransferable::formatEtcToDataFlavor(sal_uInt32 cfFormat)
|
|
{
|
|
return m_DataFormatTranslator.getDataFlavorFromFormatEtc(cfFormat);
|
|
}
|
|
|
|
// returns the current locale on clipboard; if there is no locale on
|
|
// clipboard the function returns the current thread locale
|
|
|
|
LCID CDOTransferable::getLocaleFromClipboard( )
|
|
{
|
|
LCID lcid = GetThreadLocale( );
|
|
|
|
try
|
|
{
|
|
CFormatEtc fetc = CDataFormatTranslator::getFormatEtcForClipformat( CF_LOCALE );
|
|
ByteSequence_t aLCIDSeq = getClipboardData( fetc );
|
|
lcid = *reinterpret_cast<LCID*>( aLCIDSeq.getArray( ) );
|
|
|
|
// because of a Win95/98 Bug; there the high word
|
|
// of a locale has the same value as the
|
|
// low word e.g. 0x07040704 that's not right
|
|
// correct is 0x00000704
|
|
lcid &= 0x0000FFFF;
|
|
}
|
|
catch(...)
|
|
{
|
|
// we take the default locale
|
|
}
|
|
|
|
return lcid;
|
|
}
|
|
|
|
void CDOTransferable::tryToGetIDataObjectIfAbsent()
|
|
{
|
|
if (!m_rDataObject.is())
|
|
{
|
|
auto xClipboard = m_xClipboard.get(); // holding the reference while we get the object
|
|
if (CWinClipboard* pWinClipboard = dynamic_cast<CWinClipboard*>(xClipboard.get()))
|
|
{
|
|
m_rDataObject = pWinClipboard->getIDataObject();
|
|
}
|
|
}
|
|
}
|
|
|
|
// I think it's not necessary to call ReleaseStgMedium
|
|
// in case of failures because nothing should have been
|
|
// allocated etc.
|
|
|
|
CDOTransferable::ByteSequence_t CDOTransferable::getClipboardData( CFormatEtc& aFormatEtc )
|
|
{
|
|
STGMEDIUM stgmedium;
|
|
tryToGetIDataObjectIfAbsent();
|
|
if (!m_rDataObject.is()) // Maybe we are shutting down, and clipboard is already destroyed?
|
|
throw RuntimeException();
|
|
HRESULT hr = m_rDataObject->GetData( aFormatEtc, &stgmedium );
|
|
|
|
// in case of failure to get a WMF metafile handle, try to get a memory block
|
|
if( FAILED( hr ) &&
|
|
( CF_METAFILEPICT == aFormatEtc.getClipformat() ) &&
|
|
( TYMED_MFPICT == aFormatEtc.getTymed() ) )
|
|
{
|
|
CFormatEtc aTempFormat( aFormatEtc );
|
|
aTempFormat.setTymed( TYMED_HGLOBAL );
|
|
hr = m_rDataObject->GetData( aTempFormat, &stgmedium );
|
|
}
|
|
|
|
if (FAILED(hr) && aFormatEtc.getTymed() == TYMED_HGLOBAL)
|
|
{
|
|
// Handle type is not memory, try stream.
|
|
CFormatEtc aTempFormat(aFormatEtc);
|
|
aTempFormat.setTymed(TYMED_ISTREAM);
|
|
hr = m_rDataObject->GetData(aTempFormat, &stgmedium);
|
|
}
|
|
|
|
if ( FAILED( hr ) )
|
|
{
|
|
OSL_ASSERT( (hr != E_INVALIDARG) &&
|
|
(hr != DV_E_DVASPECT) &&
|
|
(hr != DV_E_LINDEX) &&
|
|
(hr != DV_E_TYMED) );
|
|
|
|
if ( DV_E_FORMATETC == hr )
|
|
throw UnsupportedFlavorException( );
|
|
else if ( STG_E_MEDIUMFULL == hr )
|
|
throw IOException( );
|
|
else
|
|
throw RuntimeException( );
|
|
}
|
|
|
|
ByteSequence_t byteStream;
|
|
|
|
try
|
|
{
|
|
if ( CF_ENHMETAFILE == aFormatEtc.getClipformat() )
|
|
byteStream = WinENHMFPictToOOMFPict( stgmedium.hEnhMetaFile );
|
|
else if (CF_HDROP == aFormatEtc.getClipformat())
|
|
byteStream = CF_HDROPToFileList(stgmedium.hGlobal);
|
|
else if ( CF_BITMAP == aFormatEtc.getClipformat() )
|
|
{
|
|
byteStream = WinBITMAPToOOBMP(stgmedium.hBitmap);
|
|
if( aFormatEtc.getTymed() == TYMED_GDI &&
|
|
! stgmedium.pUnkForRelease )
|
|
{
|
|
DeleteObject(stgmedium.hBitmap);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
clipDataToByteStream( aFormatEtc.getClipformat( ), stgmedium, byteStream );
|
|
|
|
// format conversion if necessary
|
|
// #i124085# DIBV5 should not happen currently, but keep as a hint here
|
|
if(CF_DIBV5 == aFormatEtc.getClipformat() || CF_DIB == aFormatEtc.getClipformat())
|
|
{
|
|
byteStream = WinDIBToOOBMP(byteStream);
|
|
}
|
|
else if(CF_METAFILEPICT == aFormatEtc.getClipformat())
|
|
{
|
|
byteStream = WinMFPictToOOMFPict(byteStream);
|
|
}
|
|
}
|
|
|
|
ReleaseStgMedium( &stgmedium );
|
|
}
|
|
catch( CStgTransferHelper::CStgTransferException& )
|
|
{
|
|
ReleaseStgMedium( &stgmedium );
|
|
throw IOException( );
|
|
}
|
|
|
|
return byteStream;
|
|
}
|
|
|
|
OUString CDOTransferable::synthesizeUnicodeText( )
|
|
{
|
|
ByteSequence_t aTextSequence;
|
|
CFormatEtc fetc;
|
|
LCID lcid = getLocaleFromClipboard( );
|
|
sal_uInt32 cpForTxtCnvt = 0;
|
|
|
|
if ( CF_TEXT == m_TxtFormatOnClipboard )
|
|
{
|
|
fetc = CDataFormatTranslator::getFormatEtcForClipformat( CF_TEXT );
|
|
aTextSequence = getClipboardData( fetc );
|
|
|
|
// determine the codepage used for text conversion
|
|
cpForTxtCnvt = getWinCPFromLocaleId( lcid, LOCALE_IDEFAULTANSICODEPAGE ).toInt32( );
|
|
}
|
|
else if ( CF_OEMTEXT == m_TxtFormatOnClipboard )
|
|
{
|
|
fetc = CDataFormatTranslator::getFormatEtcForClipformat( CF_OEMTEXT );
|
|
aTextSequence = getClipboardData( fetc );
|
|
|
|
// determine the codepage used for text conversion
|
|
cpForTxtCnvt = getWinCPFromLocaleId( lcid, LOCALE_IDEFAULTCODEPAGE ).toInt32( );
|
|
}
|
|
else
|
|
OSL_ASSERT( false );
|
|
|
|
CStgTransferHelper stgTransferHelper;
|
|
|
|
// convert the text
|
|
MultiByteToWideCharEx( cpForTxtCnvt,
|
|
reinterpret_cast<char*>( aTextSequence.getArray( ) ),
|
|
-1,
|
|
stgTransferHelper,
|
|
false);
|
|
|
|
CRawHGlobalPtr ptrHGlob(stgTransferHelper);
|
|
sal_Unicode* pWChar = static_cast<sal_Unicode*>(ptrHGlob.GetMemPtr());
|
|
|
|
return OUString(pWChar);
|
|
}
|
|
|
|
bool CDOTransferable::compareDataFlavors(
|
|
const DataFlavor& lhs, const DataFlavor& rhs )
|
|
{
|
|
if ( !m_rXMimeCntFactory.is( ) )
|
|
{
|
|
m_rXMimeCntFactory = MimeContentTypeFactory::create( m_xContext );
|
|
}
|
|
|
|
bool bRet = false;
|
|
|
|
try
|
|
{
|
|
Reference< XMimeContentType > xLhs( m_rXMimeCntFactory->createMimeContentType( lhs.MimeType ) );
|
|
Reference< XMimeContentType > xRhs( m_rXMimeCntFactory->createMimeContentType( rhs.MimeType ) );
|
|
|
|
if ( cmpFullMediaType( xLhs, xRhs ) )
|
|
{
|
|
bRet = cmpAllContentTypeParameter( xLhs, xRhs );
|
|
}
|
|
}
|
|
catch( IllegalArgumentException& )
|
|
{
|
|
OSL_FAIL( "Invalid content type detected" );
|
|
bRet = false;
|
|
}
|
|
|
|
return bRet;
|
|
}
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|