summaryrefslogtreecommitdiffstats
path: root/ucb/source/ucp/tdoc/tdoc_docmgr.cxx
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:06:44 +0000
commited5640d8b587fbcfed7dd7967f3de04b37a76f26 (patch)
tree7a5f7c6c9d02226d7471cb3cc8fbbf631b415303 /ucb/source/ucp/tdoc/tdoc_docmgr.cxx
parentInitial commit. (diff)
downloadlibreoffice-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 '')
-rw-r--r--ucb/source/ucp/tdoc/tdoc_docmgr.cxx657
1 files changed, 657 insertions, 0 deletions
diff --git a/ucb/source/ucp/tdoc/tdoc_docmgr.cxx b/ucb/source/ucp/tdoc/tdoc_docmgr.cxx
new file mode 100644
index 000000000..1ecc77520
--- /dev/null
+++ b/ucb/source/ucp/tdoc/tdoc_docmgr.cxx
@@ -0,0 +1,657 @@
+/* -*- 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 <rtl/ref.hxx>
+#include <tools/diagnose_ex.h>
+
+#include <comphelper/documentinfo.hxx>
+#include <comphelper/namedvaluecollection.hxx>
+#include <comphelper/sequence.hxx>
+
+#include <com/sun/star/awt/XTopWindow.hpp>
+#include <com/sun/star/beans/XPropertySet.hpp>
+#include <com/sun/star/document/XDocumentEventBroadcaster.hpp>
+#include <com/sun/star/document/XStorageBasedDocument.hpp>
+#include <com/sun/star/frame/UnknownModuleException.hpp>
+#include <com/sun/star/frame/theGlobalEventBroadcaster.hpp>
+#include <com/sun/star/frame/ModuleManager.hpp>
+#include <com/sun/star/lang/DisposedException.hpp>
+#include <com/sun/star/lang/NotInitializedException.hpp>
+#include <com/sun/star/util/XCloseBroadcaster.hpp>
+
+#include "tdoc_docmgr.hxx"
+#include "tdoc_provider.hxx"
+
+using namespace com::sun::star;
+using namespace tdoc_ucp;
+
+// OfficeDocumentsCloseListener Implementation.
+
+
+// util::XCloseListener
+
+
+// virtual
+void SAL_CALL OfficeDocumentsManager::OfficeDocumentsCloseListener::queryClosing(
+ const lang::EventObject& /*Source*/, sal_Bool /*GetsOwnership*/ )
+{
+}
+
+
+void SAL_CALL OfficeDocumentsManager::OfficeDocumentsCloseListener::notifyClosing(
+ const lang::EventObject& Source )
+{
+ if (!m_pManager) return; // disposed?
+
+ document::DocumentEvent aDocEvent;
+ aDocEvent.Source = Source.Source;
+ aDocEvent.EventName = "OfficeDocumentsListener::notifyClosing";
+ m_pManager->documentEventOccured( aDocEvent );
+}
+
+
+// lang::XDocumentEventListener (base of util::XCloseListener)
+
+
+// virtual
+void SAL_CALL OfficeDocumentsManager::OfficeDocumentsCloseListener::disposing(
+ const lang::EventObject& /*Source*/ )
+{
+}
+
+
+// OfficeDocumentsManager Implementation.
+
+
+OfficeDocumentsManager::OfficeDocumentsManager(
+ const uno::Reference< uno::XComponentContext > & rxContext,
+ ContentProvider * pDocEventListener )
+: m_xContext( rxContext ),
+ m_xDocEvtNotifier( frame::theGlobalEventBroadcaster::get( rxContext ) ),
+ m_pDocEventListener( pDocEventListener ),
+ m_xDocCloseListener( new OfficeDocumentsCloseListener( this ) )
+{
+ // Order is important (multithreaded environment)
+ uno::Reference< document::XDocumentEventBroadcaster >(
+ m_xDocEvtNotifier, uno::UNO_QUERY_THROW )->addDocumentEventListener( this );
+ buildDocumentsList();
+}
+
+
+// virtual
+OfficeDocumentsManager::~OfficeDocumentsManager()
+{
+ //OSL_ENSURE( m_aDocs.empty(), "document list not empty!" );
+ // no need to assert this: Normal shutdown of LibreOffice could already trigger it, since the order
+ // in which objects are actually released/destroyed upon shutdown is not defined. And when we
+ // arrive *here*, LibreOffice *is* shutting down currently, since we're held by the TDOC provider,
+ // which is disposed upon shutdown.
+ m_xDocCloseListener->Dispose();
+}
+
+
+void OfficeDocumentsManager::destroy()
+{
+ uno::Reference< document::XDocumentEventBroadcaster >(
+ m_xDocEvtNotifier, uno::UNO_QUERY_THROW )->removeDocumentEventListener( this );
+}
+
+
+static OUString
+getDocumentId( const uno::Reference< uno::XInterface > & xDoc )
+{
+ OUString aId;
+
+ // Try to get the UID directly from the document.
+ uno::Reference< beans::XPropertySet > xPropSet( xDoc, uno::UNO_QUERY );
+ if ( xPropSet.is() )
+ {
+ try
+ {
+ uno::Any aValue = xPropSet->getPropertyValue("RuntimeUID");
+ aValue >>= aId;
+ }
+ catch ( beans::UnknownPropertyException const & )
+ {
+ // Not actually an error. Property is optional.
+ }
+ catch ( lang::WrappedTargetException const & )
+ {
+ TOOLS_WARN_EXCEPTION("ucb.ucp", "Caught WrappedTargetException!");
+ }
+ }
+
+ if ( aId.isEmpty() )
+ {
+ // fallback: generate UID from document's this pointer.
+ // normalize the interface pointer first. Else, calls with different
+ // interfaces to the same object (say, XFoo and XBar) will produce
+ // different IDs
+ uno::Reference< uno::XInterface > xNormalizedIFace( xDoc, uno::UNO_QUERY );
+ sal_Int64 nId = reinterpret_cast< sal_Int64 >( xNormalizedIFace.get() );
+ aId = OUString::number( nId );
+ }
+
+ OSL_ENSURE( !aId.isEmpty(), "getDocumentId - Empty id!" );
+ return aId;
+}
+
+
+// document::XDocumentEventListener
+
+
+// virtual
+void SAL_CALL OfficeDocumentsManager::documentEventOccured(
+ const document::DocumentEvent & Event )
+{
+/*
+ Events documentation: OOo Developer's Guide / Writing UNO Components /
+ Integrating Components into OpenOffice.org / Jobs
+*/
+
+ if ( Event.EventName == "OnLoadFinished" // document loaded
+ || Event.EventName == "OnCreate" ) // document created
+ {
+ if ( isOfficeDocument( Event.Source ) )
+ {
+ uno::Reference<frame::XModel> const xModel(
+ Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xModel.is(), "Got no frame::XModel!" );
+
+ bool found(false);
+
+ {
+ std::scoped_lock aGuard( m_aMtx );
+
+ found = std::any_of(m_aDocs.begin(), m_aDocs.end(),
+ [&xModel](const DocumentList::value_type& rEntry) { return rEntry.second.xModel == xModel; });
+ }
+
+ if (!found)
+ {
+ // no mutex to avoid deadlocks!
+ // need no lock to access const members, ContentProvider is safe
+
+ // new document
+
+ uno::Reference< document::XStorageBasedDocument >
+ xDoc( Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xDoc.is(), "Got no document::XStorageBasedDocument!" );
+
+ uno::Reference< embed::XStorage > xStorage
+ = xDoc->getDocumentStorage();
+ OSL_ENSURE( xStorage.is(), "Got no document storage!" );
+
+ rtl:: OUString aDocId = getDocumentId( Event.Source );
+ rtl:: OUString aTitle = comphelper::DocumentInfo::getDocumentTitle(
+ uno::Reference< frame::XModel >( Event.Source, uno::UNO_QUERY ) );
+
+ {
+ std::scoped_lock g(m_aMtx);
+ m_aDocs[ aDocId ] = StorageInfo( aTitle, xStorage, xModel );
+ }
+
+ uno::Reference< util::XCloseBroadcaster > xCloseBroadcaster(
+ Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xCloseBroadcaster.is(),
+ "OnLoadFinished/OnCreate event: got no close broadcaster!" );
+
+ if ( xCloseBroadcaster.is() )
+ xCloseBroadcaster->addCloseListener(m_xDocCloseListener);
+
+ // Propagate document closure.
+ OSL_ENSURE( m_pDocEventListener,
+ "OnLoadFinished/OnCreate event: no owner for insert event propagation!" );
+
+ if ( m_pDocEventListener )
+ m_pDocEventListener->notifyDocumentOpened( aDocId );
+ }
+ }
+ }
+ else if ( Event.EventName == "OfficeDocumentsListener::notifyClosing" )
+ {
+ if ( isOfficeDocument( Event.Source ) )
+ {
+ // Document has been closed (unloaded)
+
+ // Official event "OnUnload" does not work here. Event
+ // gets fired too early. Other OnUnload listeners called after this
+ // listener may still need TDOC access to the document. Remove the
+ // document from TDOC docs list on XCloseListener::notifyClosing.
+ // See OfficeDocumentsManager::OfficeDocumentsListener::notifyClosing.
+
+ uno::Reference< frame::XModel >
+ xModel( Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xModel.is(), "Got no frame::XModel!" );
+
+ bool found(false);
+ OUString aDocId;
+
+ {
+ std::scoped_lock aGuard( m_aMtx );
+
+ auto it = std::find_if(m_aDocs.begin(), m_aDocs.end(),
+ [&xModel](const DocumentList::value_type& rEntry) { return rEntry.second.xModel == xModel; });
+ if ( it != m_aDocs.end() )
+ {
+ aDocId = (*it).first;
+ found = true;
+ m_aDocs.erase( it );
+ }
+ }
+
+ OSL_ENSURE( found,
+ "OnUnload event notified for unknown document!" );
+
+ if (found)
+ {
+ // Propagate document closure.
+ OSL_ENSURE( m_pDocEventListener,
+ "OnUnload event: no owner for close event propagation!" );
+ if (m_pDocEventListener)
+ {
+ m_pDocEventListener->notifyDocumentClosed(aDocId);
+ }
+ uno::Reference< util::XCloseBroadcaster > xCloseBroadcaster(
+ Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xCloseBroadcaster.is(),
+ "OnUnload event: got no XCloseBroadcaster from XModel" );
+ if ( xCloseBroadcaster.is() )
+ xCloseBroadcaster->removeCloseListener(m_xDocCloseListener);
+ }
+ }
+ }
+ else if ( Event.EventName == "OnSaveDone" )
+ {
+ if ( isOfficeDocument( Event.Source ) )
+ {
+ // Storage gets exchanged while saving.
+ uno::Reference<document::XStorageBasedDocument> const xDoc(
+ Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xDoc.is(),
+ "Got no document::XStorageBasedDocument!" );
+ uno::Reference<embed::XStorage> const xStorage(
+ xDoc->getDocumentStorage());
+ OSL_ENSURE( xStorage.is(), "Got no document storage!" );
+
+ uno::Reference< frame::XModel >
+ xModel( Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xModel.is(), "Got no frame::XModel!" );
+
+ std::scoped_lock aGuard( m_aMtx );
+
+ DocumentList::iterator it = std::find_if(m_aDocs.begin(), m_aDocs.end(),
+ [&xModel](const DocumentList::value_type& rEntry) { return rEntry.second.xModel == xModel; });
+
+ OSL_ENSURE( it != m_aDocs.end(),
+ "OnSaveDone event notified for unknown document!" );
+ if ( it != m_aDocs.end() )
+ {
+ (*it).second.xStorage = xStorage;
+ }
+ }
+ }
+ else if ( Event.EventName == "OnSaveAsDone" )
+ {
+ if ( isOfficeDocument( Event.Source ) )
+ {
+ // Storage gets exchanged while saving.
+ uno::Reference<document::XStorageBasedDocument> const xDoc(
+ Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xDoc.is(),
+ "Got no document::XStorageBasedDocument!" );
+ uno::Reference<embed::XStorage> const xStorage(
+ xDoc->getDocumentStorage());
+ OSL_ENSURE( xStorage.is(), "Got no document storage!" );
+
+ uno::Reference< frame::XModel >
+ xModel( Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xModel.is(), "Got no frame::XModel!" );
+
+ OUString const title(comphelper::DocumentInfo::getDocumentTitle(xModel));
+
+ std::scoped_lock aGuard( m_aMtx );
+
+ DocumentList::iterator it = std::find_if(m_aDocs.begin(), m_aDocs.end(),
+ [&xModel](const DocumentList::value_type& rEntry) { return rEntry.second.xModel == xModel; });
+
+ OSL_ENSURE( it != m_aDocs.end(),
+ "OnSaveAsDone event notified for unknown document!" );
+ if ( it != m_aDocs.end() )
+ {
+ (*it).second.xStorage = xStorage;
+
+ // Adjust title.
+ (*it).second.aTitle = title;
+ }
+ }
+ }
+ else if ( Event.EventName == "OnTitleChanged"
+ || Event.EventName == "OnStorageChanged" )
+ {
+ if ( isOfficeDocument( Event.Source ) )
+ {
+ // Storage gets exchanged while saving.
+ uno::Reference<document::XStorageBasedDocument> const xDoc(
+ Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xDoc.is(),
+ "Got no document::XStorageBasedDocument!" );
+ uno::Reference<embed::XStorage> const xStorage(
+ xDoc->getDocumentStorage());
+ OSL_ENSURE( xStorage.is(), "Got no document storage!" );
+
+ uno::Reference< frame::XModel >
+ xModel( Event.Source, uno::UNO_QUERY );
+ OSL_ENSURE( xModel.is(), "Got no frame::XModel!" );
+
+ OUString const aTitle(comphelper::DocumentInfo::getDocumentTitle(xModel));
+
+ OUString const aDocId(getDocumentId(Event.Source));
+
+ std::scoped_lock aGuard( m_aMtx );
+
+ DocumentList::iterator it = std::find_if(m_aDocs.begin(), m_aDocs.end(),
+ [&xModel](const DocumentList::value_type& rEntry) { return rEntry.second.xModel == xModel; });
+ if ( it != m_aDocs.end() )
+ {
+ // Adjust title.
+ (*it).second.aTitle = aTitle;
+
+ m_aDocs[ aDocId ] = StorageInfo( aTitle, xStorage, xModel );
+ }
+
+// OSL_ENSURE( it != m_aDocs.end(),
+// "TitleChanged event notified for unknown document!" );
+ // TODO: re-enable this assertion. It has been disabled for now, since it breaks the assertion-free smoketest,
+ // and the fix is more difficult than what can be done now.
+ // The problem is that at the moment, when you close a SFX-based document via API, it will first
+ // fire the notifyClosing event, which will make the OfficeDocumentsManager remove the doc from its list.
+ // Then, it will notify an OnTitleChanged, then an OnUnload. Documents closed via call the notifyClosing
+ // *after* OnUnload and all other On* events.
+ // In agreement with MBA, the implementation for SfxBaseModel::Close should be changed to also send notifyClosing
+ // as last event. When this happens, the assertion here must be enabled, again.
+ }
+ }
+}
+
+// lang::XDocumentEventListener (base of document::XDocumentEventListener)
+
+// virtual
+void SAL_CALL OfficeDocumentsManager::disposing(
+ const lang::EventObject& /*Source*/ )
+{
+}
+
+// Non-interface.
+
+void OfficeDocumentsManager::buildDocumentsList()
+{
+ uno::Reference< container::XEnumeration > xEnum
+ = m_xDocEvtNotifier->createEnumeration();
+
+ while ( xEnum->hasMoreElements() )
+ {
+ uno::Any aValue = xEnum->nextElement();
+ // container::NoSuchElementException
+ // lang::WrappedTargetException
+
+ try
+ {
+ uno::Reference< frame::XModel > xModel;
+ aValue >>= xModel;
+
+ if ( xModel.is() )
+ {
+ if ( isOfficeDocument( xModel ) )
+ {
+ bool found(false);
+
+ {
+ std::scoped_lock aGuard( m_aMtx );
+
+ found = std::any_of(m_aDocs.begin(), m_aDocs.end(),
+ [&xModel](const DocumentList::value_type& rEntry) { return rEntry.second.xModel == xModel; });
+ }
+
+ if (!found)
+ {
+ // new document
+ OUString aDocId = getDocumentId( xModel );
+ OUString aTitle = comphelper::DocumentInfo::getDocumentTitle( xModel );
+
+ uno::Reference< document::XStorageBasedDocument >
+ xDoc( xModel, uno::UNO_QUERY );
+ OSL_ENSURE( xDoc.is(),
+ "Got no document::XStorageBasedDocument!" );
+
+ uno::Reference< embed::XStorage > xStorage
+ = xDoc->getDocumentStorage();
+ OSL_ENSURE( xStorage.is(), "Got no document storage!" );
+
+ {
+ std::scoped_lock aGuard( m_aMtx );
+ m_aDocs[ aDocId ]
+ = StorageInfo( aTitle, xStorage, xModel );
+ }
+
+ uno::Reference< util::XCloseBroadcaster > xCloseBroadcaster(
+ xModel, uno::UNO_QUERY );
+ OSL_ENSURE( xCloseBroadcaster.is(),
+ "buildDocumentsList: got no close broadcaster!" );
+
+ if ( xCloseBroadcaster.is() )
+ xCloseBroadcaster->addCloseListener(m_xDocCloseListener);
+ }
+ }
+ }
+ }
+ catch ( lang::DisposedException const & )
+ {
+ // Note: Due to race conditions the XEnumeration can
+ // contain docs that have already been closed
+ }
+ catch ( lang::NotInitializedException const & )
+ {
+ // Note: Due to race conditions the XEnumeration can
+ // contain docs that are still uninitialized
+ }
+ }
+}
+
+uno::Reference< embed::XStorage >
+OfficeDocumentsManager::queryStorage( const OUString & rDocId )
+{
+ std::scoped_lock aGuard( m_aMtx );
+
+ DocumentList::const_iterator it = m_aDocs.find( rDocId );
+ if ( it == m_aDocs.end() )
+ return uno::Reference< embed::XStorage >();
+
+ return (*it).second.xStorage;
+}
+
+
+OUString OfficeDocumentsManager::queryDocumentId(
+ const uno::Reference< frame::XModel > & xModel )
+{
+ return getDocumentId( xModel );
+}
+
+
+uno::Reference< frame::XModel >
+OfficeDocumentsManager::queryDocumentModel( const OUString & rDocId )
+{
+ std::scoped_lock aGuard( m_aMtx );
+
+ DocumentList::const_iterator it = m_aDocs.find( rDocId );
+ if ( it == m_aDocs.end() )
+ return uno::Reference< frame::XModel >();
+
+ return (*it).second.xModel;
+}
+
+
+uno::Sequence< OUString > OfficeDocumentsManager::queryDocuments()
+{
+ std::scoped_lock aGuard( m_aMtx );
+
+ return comphelper::mapKeysToSequence( m_aDocs );
+}
+
+
+OUString
+OfficeDocumentsManager::queryStorageTitle( const OUString & rDocId )
+{
+ std::scoped_lock aGuard( m_aMtx );
+
+ DocumentList::const_iterator it = m_aDocs.find( rDocId );
+ if ( it == m_aDocs.end() )
+ return OUString();
+
+ return (*it).second.aTitle;
+}
+
+
+bool OfficeDocumentsManager::isDocumentPreview(
+ const uno::Reference< frame::XModel > & xModel )
+{
+ if ( !xModel.is() )
+ return false;
+
+ bool bIsPreview = ::comphelper::NamedValueCollection::getOrDefault( xModel->getArgs(), u"Preview", false );
+ return bIsPreview;
+}
+
+
+bool OfficeDocumentsManager::isHelpDocument(
+ const uno::Reference< frame::XModel > & xModel )
+{
+ if ( !xModel.is() )
+ return false;
+
+ OUString sURL( xModel->getURL() );
+ return sURL.match( "vnd.sun.star.help://" );
+}
+
+
+bool OfficeDocumentsManager::isWithoutOrInTopLevelFrame(
+ const uno::Reference< frame::XModel > & xModel )
+{
+ if ( !xModel.is() )
+ return false;
+
+ uno::Reference< frame::XController > xController
+ = xModel->getCurrentController();
+ if ( xController.is() )
+ {
+ uno::Reference< frame::XFrame > xFrame
+ = xController->getFrame();
+ if ( xFrame.is() )
+ {
+ // don't use XFrame::isTop here. This nowadays excludes
+ // "sub documents" such as forms embedded in database documents
+ uno::Reference< awt::XTopWindow > xFrameContainer(
+ xFrame->getContainerWindow(), uno::UNO_QUERY );
+ if ( !xFrameContainer.is() )
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
+bool OfficeDocumentsManager::isBasicIDE(
+ const uno::Reference< frame::XModel > & xModel )
+{
+ if ( !m_xModuleMgr.is() )
+ {
+ std::scoped_lock aGuard( m_aMtx );
+ if ( !m_xModuleMgr.is() )
+ {
+ try
+ {
+ m_xModuleMgr = frame::ModuleManager::create( m_xContext );
+ }
+ catch ( uno::Exception const & )
+ {
+ // handled below.
+ }
+
+ OSL_ENSURE( m_xModuleMgr .is(),
+ "Could not instantiate ModuleManager service!" );
+ }
+ }
+
+ if ( m_xModuleMgr.is() )
+ {
+ OUString aModule;
+ try
+ {
+ aModule = m_xModuleMgr->identify( xModel );
+ }
+ catch ( lang::IllegalArgumentException const & )
+ {
+ TOOLS_WARN_EXCEPTION("ucb.ucp", "");
+ }
+ catch ( frame::UnknownModuleException const & )
+ {
+ TOOLS_WARN_EXCEPTION("ucb.ucp", "");
+ }
+
+ if ( !aModule.isEmpty() )
+ {
+ // Filter unwanted items, that are no real documents.
+ if ( aModule == "com.sun.star.script.BasicIDE" )
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+
+bool OfficeDocumentsManager::isOfficeDocument(
+ const uno::Reference< uno::XInterface > & xDoc )
+{
+ uno::Reference< frame::XModel > xModel( xDoc, uno::UNO_QUERY );
+ uno::Reference< document::XStorageBasedDocument >
+ xStorageBasedDoc( xModel, uno::UNO_QUERY );
+ if ( !xStorageBasedDoc.is() )
+ return false;
+
+ if ( !isWithoutOrInTopLevelFrame( xModel ) )
+ return false;
+
+ if ( isDocumentPreview( xModel ) )
+ return false;
+
+ if ( isHelpDocument( xModel ) )
+ return false;
+
+ if ( isBasicIDE( xModel ) )
+ return false;
+
+ return true;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */