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 /vcl/win/dtrans/target.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 'vcl/win/dtrans/target.cxx')
-rw-r--r-- | vcl/win/dtrans/target.cxx | 646 |
1 files changed, 646 insertions, 0 deletions
diff --git a/vcl/win/dtrans/target.cxx b/vcl/win/dtrans/target.cxx new file mode 100644 index 000000000..077aeeb0d --- /dev/null +++ b/vcl/win/dtrans/target.cxx @@ -0,0 +1,646 @@ +/* -*- 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 <com/sun/star/datatransfer/dnd/DNDConstants.hpp> +#include <com/sun/star/datatransfer/XTransferable.hpp> +#include <cppuhelper/supportsservice.hxx> +#include <o3tl/any.hxx> + +#include <stdio.h> +#include <win/dnd_target.hxx> +#include "idroptarget.hxx" +#include "globals.hxx" +#include "targetdropcontext.hxx" +#include "targetdragcontext.hxx" +#include <rtl/ustring.h> +#include <osl/thread.h> +#include <sal/log.hxx> +#include <comphelper/windowserrorstring.hxx> + +#include "DOTransferable.hxx" + +using namespace cppu; +using namespace osl; +using namespace com::sun::star::datatransfer; +using namespace com::sun::star::datatransfer::dnd; +using namespace com::sun::star::datatransfer::dnd::DNDConstants; + +#define WM_REGISTERDRAGDROP WM_USER + 1 +#define WM_REVOKEDRAGDROP WM_USER + 2 + +DWORD WINAPI DndTargetOleSTAFunc(LPVOID pParams); + +DropTarget::DropTarget( const Reference<XComponentContext>& rxContext): + WeakComponentImplHelper<XInitialization,XDropTarget, XServiceInfo>(m_aMutex), + m_hWnd( nullptr), + m_threadIdWindow(0), + m_threadIdTarget(0), + m_hOleThread(nullptr), + m_oleThreadId( 0), + m_pDropTarget( nullptr), + m_xContext( rxContext ), + m_bActive(true), + m_nDefaultActions(ACTION_COPY|ACTION_MOVE|ACTION_LINK|ACTION_DEFAULT), + m_nCurrentDropAction( ACTION_NONE), + m_nLastDropAction(0), + m_bDropComplete(false) +{ +} + +DropTarget::~DropTarget() +{ +} +// called from WeakComponentImplHelperX::dispose +// WeakComponentImplHelper calls disposing before it destroys +// itself. +// NOTE: RevokeDragDrop decrements the ref count on the IDropTarget +// interface. (m_pDropTarget) +// If the HWND is invalid then it doesn't decrement and +// the IDropTarget object will live on. MEMORY LEAK +void SAL_CALL DropTarget::disposing() +{ + if( m_threadIdTarget) + { + // Call RevokeDragDrop and wait for the OLE thread to die; + PostThreadMessageW( m_threadIdTarget, WM_REVOKEDRAGDROP, reinterpret_cast<WPARAM>(this), 0); + WaitForSingleObject( m_hOleThread, INFINITE); + CloseHandle( m_hOleThread); + //OSL_ENSURE( SUCCEEDED( hr), "HWND not valid!" ); + } + else + { + RevokeDragDrop( m_hWnd); + m_hWnd= nullptr; + } + if( m_pDropTarget) + { + CoLockObjectExternal( m_pDropTarget, FALSE, TRUE); + m_pDropTarget->Release(); + m_pDropTarget = nullptr; + } + + if( m_oleThreadId) + { + if( m_oleThreadId == CoGetCurrentProcess() ) + OleUninitialize(); + } + +} + +void SAL_CALL DropTarget::initialize( const Sequence< Any >& aArguments ) +{ + // The window must be registered for Dnd by RegisterDragDrop. We must ensure + // that RegisterDragDrop is called from an STA ( OleInitialize) thread. + // As long as the window is registered we need to receive OLE messages in + // an OLE thread. That is to say, if DropTarget::initialize was called from an + // MTA thread then we create an OLE thread in which the window is registered. + // The thread will stay alive until aver RevokeDragDrop has been called. + + // Additionally even if RegisterDragDrop is called from an STA thread we have + // to ensure that it is called from the same thread that created the Window + // otherwise messages sent during DND won't reach the windows message queue. + // Calling AttachThreadInput first would resolve this problem but would block + // the message queue of the calling thread. So if the current thread + // (even if it's an STA thread) and the thread that created the window are not + // identical we need to create a new thread as we do when the calling thread is + // an MTA thread. + + if( aArguments.getLength() > 0) + { + // Get the window handle from aArgument. It is needed for RegisterDragDrop. + m_hWnd= reinterpret_cast<HWND>(static_cast<sal_uIntPtr>(*o3tl::doAccess<sal_uInt64>(aArguments[0]))); + OSL_ASSERT( IsWindow( m_hWnd) ); + + // Obtain the id of the thread that created the window + m_threadIdWindow= GetWindowThreadProcessId( m_hWnd, nullptr); + + HRESULT hr= OleInitialize( nullptr); + + // Current thread is MTA or Current thread and Window thread are not identical + if( hr == RPC_E_CHANGED_MODE || GetCurrentThreadId() != m_threadIdWindow ) + { + OSL_ENSURE( ! m_threadIdTarget,"initialize was called twice"); + // create the IDropTargetImplementation + m_pDropTarget= new IDropTargetImpl( *this ); + m_pDropTarget->AddRef(); + + // Obtain the id of the thread that created the window + m_threadIdWindow= GetWindowThreadProcessId( m_hWnd, nullptr); + // The event is set by the thread that we will create momentarily. + // It indicates that the thread is ready to receive messages. + HANDLE m_evtThreadReady= CreateEventW( nullptr, FALSE, FALSE, nullptr); + + m_hOleThread= CreateThread( nullptr, 0, DndTargetOleSTAFunc, + &m_evtThreadReady, 0, &m_threadIdTarget); + WaitForSingleObject( m_evtThreadReady, INFINITE); + CloseHandle( m_evtThreadReady); + PostThreadMessageW( m_threadIdTarget, WM_REGISTERDRAGDROP, reinterpret_cast<WPARAM>(this), 0); + } + else if( hr == S_OK || hr == S_FALSE) + { + // current thread is STA + // If OleInitialize has been called by the caller then we must not call + // OleUninitialize + if( hr == S_OK) + { + // caller did not call OleInitialize, so we call OleUninitialize + // remember the thread that will call OleUninitialize + m_oleThreadId= CoGetCurrentProcess(); // get a unique thread id + } + + // Get the window handle from aArgument. It is needed for RegisterDragDrop. + // create the IDropTargetImplementation + m_pDropTarget= new IDropTargetImpl( *this ); + m_pDropTarget->AddRef(); + // CoLockObjectExternal is prescribed by the protocol. It bumps up the ref count + if( SUCCEEDED( CoLockObjectExternal( m_pDropTarget, TRUE, FALSE))) + { + if( FAILED( RegisterDragDrop( m_hWnd, m_pDropTarget) ) ) + { + // do clean up if drag and drop is not possible + CoLockObjectExternal( m_pDropTarget, FALSE, FALSE); + m_pDropTarget->Release(); + m_pDropTarget = nullptr; + m_hWnd= nullptr; + } + } + } + else + throw Exception("OleInitialize failed with " + OUString::number(hr), nullptr); + + } +} + +// This function is called as extra thread from DragSource::startDrag. +// The function carries out a drag and drop operation by calling +// DoDragDrop. The thread also notifies all XSourceListener. +DWORD WINAPI DndTargetOleSTAFunc(LPVOID pParams) +{ + osl_setThreadName("DropTarget DndTargetOleSTAFunc"); + + HRESULT hr= OleInitialize( nullptr); + if( SUCCEEDED( hr) ) + { + MSG msg; + // force the creation of a message queue + PeekMessageW( &msg, nullptr, 0, 0, PM_NOREMOVE); + // Signal the creator ( DropTarget::initialize) that the thread is + // ready to receive messages. + SetEvent( *static_cast<HANDLE*>(pParams)); + // Thread id is needed for attaching this message queue to the one of the + // thread where the window was created. + DWORD threadId= GetCurrentThreadId(); + // We force the creation of a thread message queue. This is necessary + // for a later call to AttachThreadInput + for (;;) + { + int const bRet = GetMessageW(&msg, nullptr, 0, 0); + if (bRet == 0) + { + break; + } + if (-1 == bRet) + { + SAL_WARN("vcl.win.dtrans", "GetMessageW failed: " << WindowsErrorString(GetLastError())); + break; + } + if( msg.message == WM_REGISTERDRAGDROP) + { + DropTarget *pTarget= reinterpret_cast<DropTarget*>(msg.wParam); + // This thread is attached to the thread that created the window. Hence + // this thread also receives all mouse and keyboard messages which are + // needed + AttachThreadInput( threadId , pTarget->m_threadIdWindow, TRUE ); + + if( SUCCEEDED( CoLockObjectExternal(pTarget-> m_pDropTarget, TRUE, FALSE))) + { + if( FAILED( RegisterDragDrop( pTarget-> m_hWnd, pTarget-> m_pDropTarget) ) ) + { + // do clean up if drag and drop is not possible + CoLockObjectExternal( pTarget->m_pDropTarget, FALSE, FALSE); + pTarget->m_pDropTarget->Release(); + pTarget->m_pDropTarget = nullptr; + pTarget->m_hWnd= nullptr; + } + } + } + else if( msg.message == WM_REVOKEDRAGDROP) + { + DropTarget *pTarget= reinterpret_cast<DropTarget*>(msg.wParam); + RevokeDragDrop( pTarget-> m_hWnd); + // Detach this thread from the window thread + AttachThreadInput( threadId, pTarget->m_threadIdWindow, FALSE); + pTarget->m_hWnd= nullptr; + break; + } + TranslateMessage( &msg); + DispatchMessageW( &msg); + } + OleUninitialize(); + } + return 0; +} + +// XServiceInfo +OUString SAL_CALL DropTarget::getImplementationName( ) +{ + return "com.sun.star.comp.datatransfer.dnd.OleDropTarget_V1"; +} +// XServiceInfo +sal_Bool SAL_CALL DropTarget::supportsService( const OUString& ServiceName ) +{ + return cppu::supportsService(this, ServiceName); +} + +Sequence< OUString > SAL_CALL DropTarget::getSupportedServiceNames( ) +{ + return { "com.sun.star.datatransfer.dnd.OleDropTarget" }; +} + +// XDropTarget +void SAL_CALL DropTarget::addDropTargetListener( const Reference< XDropTargetListener >& dtl ) +{ + rBHelper.addListener( cppu::UnoType<decltype(dtl)>::get(), dtl ); +} + +void SAL_CALL DropTarget::removeDropTargetListener( const Reference< XDropTargetListener >& dtl ) +{ + rBHelper.removeListener( cppu::UnoType<decltype(dtl)>::get(), dtl ); +} + +sal_Bool SAL_CALL DropTarget::isActive( ) +{ + return m_bActive; //m_bDropTargetRegistered; +} + +void SAL_CALL DropTarget::setActive( sal_Bool _b ) +{ + MutexGuard g(m_aMutex); + m_bActive= _b; +} + +sal_Int8 SAL_CALL DropTarget::getDefaultActions( ) +{ + return m_nDefaultActions; +} + +void SAL_CALL DropTarget::setDefaultActions( sal_Int8 actions ) +{ + OSL_ENSURE( actions < 8, "No valid default actions"); + m_nDefaultActions= actions; +} + +HRESULT DropTarget::DragEnter( IDataObject *pDataObj, + DWORD grfKeyState, + POINTL pt, + DWORD *pdwEffect) +{ +#if defined DBG_CONSOLE_OUT + printf("\nDropTarget::DragEnter state: %x effect %d", grfKeyState, *pdwEffect); +#endif + if( m_bActive ) + { + // Intersection of pdwEffect and the allowed actions ( setDefaultActions) + m_nCurrentDropAction= getFilteredActions( grfKeyState, *pdwEffect); + // m_nLastDropAction has to be set by a listener. If no listener calls + //XDropTargetDragContext::acceptDrag and specifies an action then pdwEffect + // will be DROPEFFECT_NONE throughout + m_nLastDropAction= ACTION_DEFAULT | ACTION_MOVE; + + m_currentDragContext = new TargetDragContext(this); + + //--> TRA + + // shortcut + if ( g_XTransferable.is( ) ) + m_currentData = g_XTransferable; + else + { + // Convert the IDataObject to a XTransferable + m_currentData = new CDOTransferable(m_xContext, IDataObjectPtr(pDataObj)); + } + + //<-- TRA + + if( m_nCurrentDropAction != ACTION_NONE) + { + DropTargetDragEnterEvent e; + e.SupportedDataFlavors= m_currentData->getTransferDataFlavors(); + e.DropAction= m_nCurrentDropAction; + e.Source.set( static_cast<XDropTarget*>(this),UNO_QUERY); + e.Context= m_currentDragContext; + POINT point={ pt.x, pt.y}; + ScreenToClient( m_hWnd, &point); + e.LocationX= point.x; + e.LocationY= point.y; + e.SourceActions= dndOleDropEffectsToActions( *pdwEffect); + + fire_dragEnter( e); + // Check if the action derived from grfKeyState (m_nCurrentDropAction) or the action set + // by the listener (m_nCurrentDropAction) is allowed by the source. Only an allowed action is set + // in pdwEffect. The listener notification is asynchronous, that is we cannot expect that the listener + // has already reacted to the notification. + // If there is more than one valid action which is the case when ALT or RIGHT MOUSE BUTTON is pressed + // then getDropEffect returns DROPEFFECT_MOVE which is the default value if no other modifier is pressed. + // On drop the target should present the user a dialog from which the user may change the action. + sal_Int8 allowedActions= dndOleDropEffectsToActions( *pdwEffect); + *pdwEffect= dndActionsToSingleDropEffect( m_nLastDropAction & allowedActions); + } + else + { + *pdwEffect= DROPEFFECT_NONE; + } + } + return S_OK; +} + +HRESULT DropTarget::DragOver( DWORD grfKeyState, + POINTL pt, + DWORD *pdwEffect) +{ + if( m_bActive) + { + m_nCurrentDropAction= getFilteredActions( grfKeyState, *pdwEffect); + + if( m_nCurrentDropAction) + { + DropTargetDragEvent e; + e.DropAction= m_nCurrentDropAction; + e.Source.set(static_cast<XDropTarget*>(this),UNO_QUERY); + e.Context= m_currentDragContext; + POINT point={ pt.x, pt.y}; + ScreenToClient( m_hWnd, &point); + e.LocationX= point.x; + e.LocationY= point.y; + e.SourceActions= dndOleDropEffectsToActions( *pdwEffect); + + // if grfKeyState has changed since the last DragOver then fire events. + // A listener might change m_nCurrentDropAction by calling the + // XDropTargetDragContext::acceptDrag function. But this is not important + // because in the afterwards fired dragOver event the action reflects + // grgKeyState again. + if( m_nLastDropAction != m_nCurrentDropAction) + fire_dropActionChanged( e); + + // The Event contains a XDropTargetDragContext implementation. + fire_dragOver( e); + // Check if the action derived from grfKeyState (m_nCurrentDropAction) or the action set + // by the listener (m_nCurrentDropAction) is allowed by the source. Only an allowed action is set + // in pdwEffect. The listener notification is asynchronous, that is we cannot expect that the listener + // has already reacted to the notification. + // If there is more than one valid action which is the case when ALT or RIGHT MOUSE BUTTON is pressed + // then getDropEffect returns DROPEFFECT_MOVE which is the default value if no other modifier is pressed. + // On drop the target should present the user a dialog from which the user may change the action. + sal_Int8 allowedActions= dndOleDropEffectsToActions( *pdwEffect); + // set the last action to the current if listener has not changed the value yet + *pdwEffect= dndActionsToSingleDropEffect( m_nLastDropAction & allowedActions); + } + else + { + *pdwEffect= DROPEFFECT_NONE; + } + } +#if defined DBG_CONSOLE_OUT + printf("\nDropTarget::DragOver %d", *pdwEffect ); +#endif + return S_OK; +} + +HRESULT DropTarget::DragLeave() +{ +#if defined DBG_CONSOLE_OUT + printf("\nDropTarget::DragLeave"); +#endif + if( m_bActive) + { + + m_currentData=nullptr; + m_currentDragContext= nullptr; + m_currentDropContext= nullptr; + m_nLastDropAction= 0; + + if( m_nDefaultActions != ACTION_NONE) + { + DropTargetEvent e; + e.Source= static_cast<XDropTarget*>(this); + + fire_dragExit( e); + } + } + return S_OK; +} + +HRESULT DropTarget::Drop( IDataObject * /*pDataObj*/, + DWORD grfKeyState, + POINTL pt, + DWORD *pdwEffect) +{ +#if defined DBG_CONSOLE_OUT + printf("\nDropTarget::Drop"); +#endif + if( m_bActive) + { + + m_bDropComplete= false; + + m_nCurrentDropAction= getFilteredActions( grfKeyState, *pdwEffect); + m_currentDropContext = new TargetDropContext(this); + if( m_nCurrentDropAction) + { + DropTargetDropEvent e; + e.DropAction= m_nCurrentDropAction; + e.Source.set( static_cast<XDropTarget*>(this), UNO_QUERY); + e.Context= m_currentDropContext; + POINT point={ pt.x, pt.y}; + ScreenToClient( m_hWnd, &point); + e.LocationX= point.x; + e.LocationY= point.y; + e.SourceActions= dndOleDropEffectsToActions( *pdwEffect); + e.Transferable= m_currentData; + fire_drop( e); + + //if fire_drop returns than a listener might have modified m_nCurrentDropAction + if( m_bDropComplete ) + { + sal_Int8 allowedActions= dndOleDropEffectsToActions( *pdwEffect); + *pdwEffect= dndActionsToSingleDropEffect( m_nCurrentDropAction & allowedActions); + } + else + *pdwEffect= DROPEFFECT_NONE; + } + else + *pdwEffect= DROPEFFECT_NONE; + + m_currentData= nullptr; + m_currentDragContext= nullptr; + m_currentDropContext= nullptr; + m_nLastDropAction= 0; + } + return S_OK; +} + +void DropTarget::fire_drop( const DropTargetDropEvent& dte) +{ + OInterfaceContainerHelper* pContainer= rBHelper.getContainer( cppu::UnoType<XDropTargetListener>::get()); + if( pContainer) + { + OInterfaceIteratorHelper iter( *pContainer); + while( iter.hasMoreElements()) + { + Reference<XDropTargetListener> listener( static_cast<XDropTargetListener*>( iter.next())); + listener->drop( dte); + } + } +} + +void DropTarget::fire_dragEnter( const DropTargetDragEnterEvent& e ) +{ + OInterfaceContainerHelper* pContainer= rBHelper.getContainer( cppu::UnoType<XDropTargetListener>::get()); + if( pContainer) + { + OInterfaceIteratorHelper iter( *pContainer); + while( iter.hasMoreElements()) + { + Reference<XDropTargetListener> listener( static_cast<XDropTargetListener*>( iter.next())); + listener->dragEnter( e); + } + } +} + +void DropTarget::fire_dragExit( const DropTargetEvent& dte ) +{ + OInterfaceContainerHelper* pContainer= rBHelper.getContainer( cppu::UnoType<XDropTargetListener>::get()); + + if( pContainer) + { + OInterfaceIteratorHelper iter( *pContainer); + while( iter.hasMoreElements()) + { + Reference<XDropTargetListener> listener( static_cast<XDropTargetListener*>( iter.next())); + listener->dragExit( dte); + } + } +} + +void DropTarget::fire_dragOver( const DropTargetDragEvent& dtde ) +{ + OInterfaceContainerHelper* pContainer= rBHelper.getContainer( cppu::UnoType<XDropTargetListener>::get()); + if( pContainer) + { + OInterfaceIteratorHelper iter( *pContainer ); + while( iter.hasMoreElements()) + { + Reference<XDropTargetListener> listener( static_cast<XDropTargetListener*>( iter.next())); + listener->dragOver( dtde); + } + } +} + +void DropTarget::fire_dropActionChanged( const DropTargetDragEvent& dtde ) +{ + OInterfaceContainerHelper* pContainer= rBHelper.getContainer( cppu::UnoType<XDropTargetListener>::get()); + if( pContainer) + { + OInterfaceIteratorHelper iter( *pContainer); + while( iter.hasMoreElements()) + { + Reference<XDropTargetListener> listener( static_cast<XDropTargetListener*>( iter.next())); + listener->dropActionChanged( dtde); + } + } +} + +// Non - interface functions +// DropTarget fires events to XDropTargetListeners. The event object contains an +// XDropTargetDropContext implementation. When the listener calls on that interface +// then the calls are delegated from DropContext (XDropTargetDropContext) to these +// functions. +// Only one listener which visible area is affected is allowed to call on +// XDropTargetDropContext +// Returning sal_False would cause the XDropTargetDropContext or ..DragContext implementation +// to throw an InvalidDNDOperationException, meaning that a Drag is not currently performed. +// return sal_False results in throwing an InvalidDNDOperationException in the caller. + +void DropTarget::_acceptDrop(sal_Int8 dropOperation, const Reference<XDropTargetDropContext>& context) +{ + if( context == m_currentDropContext) + { + m_nCurrentDropAction= dropOperation; + } +} + +void DropTarget::_rejectDrop( const Reference<XDropTargetDropContext>& context) +{ + if( context == m_currentDropContext) + { + m_nCurrentDropAction= ACTION_NONE; + } +} + +void DropTarget::_dropComplete(bool success, const Reference<XDropTargetDropContext>& context) +{ + if(context == m_currentDropContext) + { + m_bDropComplete= success; + } +} + +// DropTarget fires events to XDropTargetListeners. The event object can contains an +// XDropTargetDragContext implementation. When the listener calls on that interface +// then the calls are delegated from DragContext (XDropTargetDragContext) to these +// functions. +// Only one listener which visible area is affected is allowed to call on +// XDropTargetDragContext +void DropTarget::_acceptDrag( sal_Int8 dragOperation, const Reference<XDropTargetDragContext>& context) +{ + if( context == m_currentDragContext) + { + m_nLastDropAction= dragOperation; + } +} + +void DropTarget::_rejectDrag( const Reference<XDropTargetDragContext>& context) +{ + if(context == m_currentDragContext) + { + m_nLastDropAction= ACTION_NONE; + } +} + +// This function determines the action dependent on the pressed +// key modifiers ( CTRL, SHIFT, ALT, Right Mouse Button). The result +// is then checked against the allowed actions which can be set through +// XDropTarget::setDefaultActions. Only those values which are also +// default actions are returned. If setDefaultActions has not been called +// beforehand the default actions comprise all possible actions. +// params: grfKeyState - the modifier keys and mouse buttons currently pressed +inline sal_Int8 DropTarget::getFilteredActions( DWORD grfKeyState, DWORD dwEffect) +{ + sal_Int8 actions= dndOleKeysToAction( grfKeyState, dndOleDropEffectsToActions( dwEffect)); + return actions & m_nDefaultActions; +} + +extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface* +dtrans_DropTarget_get_implementation( + css::uno::XComponentContext* context, css::uno::Sequence<css::uno::Any> const&) +{ + return cppu::acquire(new DropTarget(context)); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |