1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
9 * This file incorporates work covered by the following license notice:
11 * Licensed to the Apache Software Foundation (ASF) under one or more
12 * contributor license agreements. See the NOTICE file distributed
13 * with this work for additional information regarding copyright
14 * ownership. The ASF licenses this file to you under the Apache
15 * License, Version 2.0 (the "License"); you may not use this file
16 * except in compliance with the License. You may obtain a copy of
17 * the License at http://www.apache.org/licenses/LICENSE-2.0 .
20 #include <Qt5Instance.hxx>
21 #include <Qt5Instance.moc>
23 #include <com/sun/star/lang/IllegalArgumentException.hpp>
25 #include <Qt5Bitmap.hxx>
26 #include <Qt5Clipboard.hxx>
27 #include <Qt5Data.hxx>
28 #include <Qt5DragAndDrop.hxx>
29 #include <Qt5FilePicker.hxx>
30 #include <Qt5Frame.hxx>
31 #include <Qt5Menu.hxx>
32 #include <Qt5Object.hxx>
33 #include <Qt5OpenGLContext.hxx>
34 #include "Qt5SvpVirtualDevice.hxx"
35 #include <Qt5System.hxx>
36 #include <Qt5Timer.hxx>
37 #include <Qt5VirtualDevice.hxx>
39 #include <headless/svpvd.hxx>
41 #include <QtCore/QAbstractEventDispatcher>
42 #include <QtCore/QThread>
43 #include <QtWidgets/QApplication>
44 #include <QtWidgets/QWidget>
46 #include <vclpluginapi.h>
47 #include <tools/debug.hxx>
48 #include <comphelper/flagguard.hxx>
49 #include <sal/log.hxx>
50 #include <osl/process.h>
51 #include <unx/gstsink.hxx>
52 #include <headless/svpbmp.hxx>
55 #include <condition_variable>
57 /// TODO: not much Qt5 specific here? could be generalised, esp. for OSX...
58 /// this subclass allows for the transfer of a closure for running on the main
59 /// thread, to handle all the thread affine stuff in Qt5; the SolarMutex is
60 /// "loaned" to the main thread for the execution of the closure.
61 /// @note it doesn't work to just use "emit" and signals/slots to move calls to
62 /// the main thread, because the other thread has the SolarMutex; the other
63 /// thread (typically) cannot release SolarMutex, because then the main thread
64 /// will handle all sorts of events and whatnot; this design ensures that the
65 /// main thread only runs the passed closure (unless the closure releases
66 /// SolarMutex itself, which should probably be avoided).
67 class Qt5YieldMutex
: public SalYieldMutex
70 /// flag only accessed on main thread:
71 /// main thread has "borrowed" SolarMutex from another thread
72 bool m_bNoYieldLock
= false;
73 /// members for communication from non-main thread to main thread
74 std::mutex m_RunInMainMutex
;
75 std::condition_variable m_InMainCondition
;
76 bool m_isWakeUpMain
= false;
77 std::function
<void()> m_Closure
; ///< code for main thread to run
78 /// members for communication from main thread to non-main thread
79 std::condition_variable m_ResultCondition
;
80 bool m_isResultReady
= false;
82 virtual bool IsCurrentThread() const override
;
83 virtual void doAcquire(sal_uInt32 nLockCount
) override
;
84 virtual sal_uInt32
doRelease(bool const bUnlockAll
) override
;
87 bool Qt5YieldMutex::IsCurrentThread() const
89 auto const* pSalInst(static_cast<Qt5Instance
const*>(GetSalData()->m_pInstance
));
91 if (pSalInst
->IsMainThread() && m_bNoYieldLock
)
93 return true; // main thread has borrowed SolarMutex
95 return SalYieldMutex::IsCurrentThread();
98 void Qt5YieldMutex::doAcquire(sal_uInt32 nLockCount
)
100 auto const* pSalInst(static_cast<Qt5Instance
const*>(GetSalData()->m_pInstance
));
102 if (!pSalInst
->IsMainThread())
104 SalYieldMutex::doAcquire(nLockCount
);
109 return; // special case for main thread: borrowed from other thread
111 do // main thread acquire...
113 std::function
<void()> func
; // copy of closure on thread stack
115 std::unique_lock
<std::mutex
> g(m_RunInMainMutex
);
116 if (m_aMutex
.tryToAcquire())
118 // if there's a closure, the other thread holds m_aMutex
120 m_isWakeUpMain
= false;
121 --nLockCount
; // have acquired once!
125 m_InMainCondition
.wait(g
, [this]() { return m_isWakeUpMain
; });
126 m_isWakeUpMain
= false;
127 std::swap(func
, m_Closure
);
131 assert(!m_bNoYieldLock
);
132 m_bNoYieldLock
= true; // execute closure with borrowed SolarMutex
134 m_bNoYieldLock
= false;
135 std::scoped_lock
<std::mutex
> g(m_RunInMainMutex
);
136 assert(!m_isResultReady
);
137 m_isResultReady
= true;
138 m_ResultCondition
.notify_all(); // unblock other thread
141 SalYieldMutex::doAcquire(nLockCount
);
144 sal_uInt32
Qt5YieldMutex::doRelease(bool const bUnlockAll
)
146 auto const* pSalInst(static_cast<Qt5Instance
const*>(GetSalData()->m_pInstance
));
148 if (pSalInst
->IsMainThread() && m_bNoYieldLock
)
150 return 1; // dummy value
153 std::scoped_lock
<std::mutex
> g(m_RunInMainMutex
);
154 // read m_nCount before doRelease (it's guarded by m_aMutex)
155 bool const isReleased(bUnlockAll
|| m_nCount
== 1);
156 sal_uInt32 nCount
= SalYieldMutex::doRelease(bUnlockAll
);
157 if (isReleased
&& !pSalInst
->IsMainThread())
159 m_isWakeUpMain
= true;
160 m_InMainCondition
.notify_all(); // unblock main thread
165 // this could be abstracted to be independent of Qt5 by passing in the
166 // event-trigger as another function parameter...
167 // it could also be a template of the return type, then it could return the
168 // result of func... but then how to handle the result in doAcquire?
169 void Qt5Instance::RunInMainThread(std::function
<void()> func
)
171 DBG_TESTSOLARMUTEX();
178 Qt5YieldMutex
* const pMutex(static_cast<Qt5YieldMutex
*>(GetYieldMutex()));
180 std::scoped_lock
<std::mutex
> g(pMutex
->m_RunInMainMutex
);
181 assert(!pMutex
->m_Closure
);
182 pMutex
->m_Closure
= func
;
183 // unblock main thread in case it is blocked on condition
184 pMutex
->m_isWakeUpMain
= true;
185 pMutex
->m_InMainCondition
.notify_all();
187 // wake up main thread in case it is blocked on event queue
188 // TriggerUserEventProcessing() appears to be insufficient in case the
189 // main thread does QEventLoop::WaitForMoreEvents
190 Q_EMIT
ImplRunInMainSignal();
192 std::unique_lock
<std::mutex
> g(pMutex
->m_RunInMainMutex
);
193 pMutex
->m_ResultCondition
.wait(g
, [pMutex
]() { return pMutex
->m_isResultReady
; });
194 pMutex
->m_isResultReady
= false;
198 void Qt5Instance::ImplRunInMain()
200 SolarMutexGuard g
; // trigger the dispatch code in Qt5YieldMutex::doAcquire
201 (void)this; // suppress unhelpful [loplugin:staticmethods]; can't be static
204 Qt5Instance::Qt5Instance(std::unique_ptr
<QApplication
>& pQApp
, bool bUseCairo
)
205 : SalGenericInstance(std::make_unique
<Qt5YieldMutex
>())
206 , m_postUserEventId(-1)
207 , m_bUseCairo(bUseCairo
)
208 , m_pQApplication(std::move(pQApp
))
209 , m_aUpdateStyleTimer("vcl::qt5 m_aUpdateStyleTimer")
210 , m_bUpdateFonts(false)
212 ImplSVData
* pSVData
= ImplGetSVData();
214 pSVData
->maAppData
.mxToolkitName
= OUString("qt5+cairo");
216 pSVData
->maAppData
.mxToolkitName
= OUString("qt5");
218 m_postUserEventId
= QEvent::registerEventType();
220 // this one needs to be blocking, so that the handling in main thread
221 // is processed before the thread emitting the signal continues
222 connect(this, SIGNAL(ImplYieldSignal(bool, bool)), this, SLOT(ImplYield(bool, bool)),
223 Qt::BlockingQueuedConnection
);
224 connect(this, &Qt5Instance::ImplRunInMainSignal
, this, &Qt5Instance::ImplRunInMain
,
225 Qt::QueuedConnection
); // no Blocking!
227 // this one needs to be queued non-blocking
228 // in order to have this event arriving to correct event processing loop
229 connect(this, &Qt5Instance::deleteObjectLaterSignal
, this,
230 [](QObject
* pObject
) { Qt5Instance::deleteObjectLater(pObject
); },
231 Qt::QueuedConnection
);
233 m_aUpdateStyleTimer
.SetTimeout(50);
234 m_aUpdateStyleTimer
.SetInvokeHandler(LINK(this, Qt5Instance
, updateStyleHdl
));
237 Qt5Instance::~Qt5Instance()
239 // force freeing the QApplication before freeing the arguments,
240 // as it uses references to the provided arguments!
241 m_pQApplication
.reset();
244 void Qt5Instance::AfterAppInit()
246 // set the default application icon via desktop file just on Wayland,
247 // as this otherwise overrides the individual desktop icons on X11.
248 if (QGuiApplication::platformName() == "wayland")
249 QGuiApplication::setDesktopFileName(QStringLiteral("libreoffice-startcenter.desktop"));
250 QGuiApplication::setLayoutDirection(AllSettings::GetLayoutRTL() ? Qt::RightToLeft
254 void Qt5Instance::deleteObjectLater(QObject
* pObject
) { pObject
->deleteLater(); }
256 SalFrame
* Qt5Instance::CreateChildFrame(SystemParentData
* /*pParent*/, SalFrameStyleFlags nStyle
)
258 return new Qt5Frame(nullptr, nStyle
, m_bUseCairo
);
261 SalFrame
* Qt5Instance::CreateFrame(SalFrame
* pParent
, SalFrameStyleFlags nStyle
)
263 assert(!pParent
|| dynamic_cast<Qt5Frame
*>(pParent
));
264 return new Qt5Frame(static_cast<Qt5Frame
*>(pParent
), nStyle
, m_bUseCairo
);
267 void Qt5Instance::DestroyFrame(SalFrame
* pFrame
)
271 assert(dynamic_cast<Qt5Frame
*>(pFrame
));
272 Q_EMIT
deleteObjectLaterSignal(static_cast<Qt5Frame
*>(pFrame
));
276 SalObject
* Qt5Instance::CreateObject(SalFrame
* pParent
, SystemWindowData
*, bool bShow
)
278 assert(!pParent
|| dynamic_cast<Qt5Frame
*>(pParent
));
279 return new Qt5Object(static_cast<Qt5Frame
*>(pParent
), bShow
);
282 void Qt5Instance::DestroyObject(SalObject
* pObject
)
286 assert(dynamic_cast<Qt5Object
*>(pObject
));
287 Q_EMIT
deleteObjectLaterSignal(static_cast<Qt5Object
*>(pObject
));
291 std::unique_ptr
<SalVirtualDevice
> Qt5Instance::CreateVirtualDevice(SalGraphics
* pGraphics
,
292 long& nDX
, long& nDY
,
293 DeviceFormat eFormat
,
294 const SystemGraphicsData
* pGd
)
298 SvpSalGraphics
* pSvpSalGraphics
= dynamic_cast<Qt5SvpGraphics
*>(pGraphics
);
299 assert(pSvpSalGraphics
);
300 // tdf#127529 see SvpSalInstance::CreateVirtualDevice for the rare case of a non-null pPreExistingTarget
301 cairo_surface_t
* pPreExistingTarget
302 = pGd
? static_cast<cairo_surface_t
*>(pGd
->pSurface
) : nullptr;
303 std::unique_ptr
<SalVirtualDevice
> pVD(
304 new Qt5SvpVirtualDevice(eFormat
, pSvpSalGraphics
->getSurface(), pPreExistingTarget
));
305 pVD
->SetSize(nDX
, nDY
);
310 std::unique_ptr
<SalVirtualDevice
> pVD(new Qt5VirtualDevice(eFormat
, 1));
311 pVD
->SetSize(nDX
, nDY
);
316 std::unique_ptr
<SalMenu
> Qt5Instance::CreateMenu(bool bMenuBar
, Menu
* pVCLMenu
)
318 std::unique_ptr
<SalMenu
> pRet
;
319 RunInMainThread([&pRet
, bMenuBar
, pVCLMenu
]() {
320 Qt5Menu
* pSalMenu
= new Qt5Menu(bMenuBar
);
321 pRet
.reset(pSalMenu
);
322 pSalMenu
->SetMenu(pVCLMenu
);
328 std::unique_ptr
<SalMenuItem
> Qt5Instance::CreateMenuItem(const SalItemParams
& rItemData
)
330 return std::unique_ptr
<SalMenuItem
>(new Qt5MenuItem(&rItemData
));
333 SalTimer
* Qt5Instance::CreateSalTimer() { return new Qt5Timer(); }
335 SalSystem
* Qt5Instance::CreateSalSystem() { return new Qt5System
; }
337 std::shared_ptr
<SalBitmap
> Qt5Instance::CreateSalBitmap()
340 return std::make_shared
<SvpSalBitmap
>();
342 return std::make_shared
<Qt5Bitmap
>();
345 bool Qt5Instance::ImplYield(bool bWait
, bool bHandleAllCurrentEvents
)
347 // Re-acquire the guard for user events when called via Q_EMIT ImplYieldSignal
348 SolarMutexGuard aGuard
;
349 bool wasEvent
= DispatchUserEvents(bHandleAllCurrentEvents
);
350 if (!bHandleAllCurrentEvents
&& wasEvent
)
354 * Quoting the Qt docs: [QAbstractEventDispatcher::processEvents] processes
355 * pending events that match flags until there are no more events to process.
357 SolarMutexReleaser aReleaser
;
358 QAbstractEventDispatcher
* dispatcher
= QAbstractEventDispatcher::instance(qApp
->thread());
359 if (bWait
&& !wasEvent
)
360 wasEvent
= dispatcher
->processEvents(QEventLoop::WaitForMoreEvents
);
362 wasEvent
= dispatcher
->processEvents(QEventLoop::AllEvents
) || wasEvent
;
366 bool Qt5Instance::DoYield(bool bWait
, bool bHandleAllCurrentEvents
)
368 bool bWasEvent
= false;
369 if (qApp
->thread() == QThread::currentThread())
371 bWasEvent
= ImplYield(bWait
, bHandleAllCurrentEvents
);
373 m_aWaitingYieldCond
.set();
378 SolarMutexReleaser aReleaser
;
379 bWasEvent
= Q_EMIT
ImplYieldSignal(false, bHandleAllCurrentEvents
);
381 if (!bWasEvent
&& bWait
)
383 m_aWaitingYieldCond
.reset();
384 SolarMutexReleaser aReleaser
;
385 m_aWaitingYieldCond
.wait();
392 bool Qt5Instance::AnyInput(VclInputFlags
/*nType*/) { return false; }
394 OUString
Qt5Instance::GetConnectionIdentifier() { return OUString(); }
396 void Qt5Instance::AddToRecentDocumentList(const OUString
&, const OUString
&, const OUString
&) {}
398 OpenGLContext
* Qt5Instance::CreateOpenGLContext() { return new Qt5OpenGLContext
; }
400 bool Qt5Instance::IsMainThread() const { return qApp
->thread() == QThread::currentThread(); }
402 void Qt5Instance::TriggerUserEventProcessing()
404 QApplication::postEvent(this, new QEvent(QEvent::Type(m_postUserEventId
)));
407 void Qt5Instance::ProcessEvent(SalUserEvent aEvent
)
409 aEvent
.m_pFrame
->CallCallback(aEvent
.m_nEvent
, aEvent
.m_pData
);
413 Qt5Instance::createPicker(css::uno::Reference
<css::uno::XComponentContext
> const& context
,
414 QFileDialog::FileMode eMode
)
419 Qt5FilePicker
* pPicker
;
420 RunInMainThread([&, this]() { pPicker
= createPicker(context
, eMode
); });
425 return new Qt5FilePicker(context
, eMode
);
428 css::uno::Reference
<css::ui::dialogs::XFilePicker2
>
429 Qt5Instance::createFilePicker(const css::uno::Reference
<css::uno::XComponentContext
>& context
)
431 return css::uno::Reference
<css::ui::dialogs::XFilePicker2
>(
432 createPicker(context
, QFileDialog::ExistingFile
));
435 css::uno::Reference
<css::ui::dialogs::XFolderPicker2
>
436 Qt5Instance::createFolderPicker(const css::uno::Reference
<css::uno::XComponentContext
>& context
)
438 return css::uno::Reference
<css::ui::dialogs::XFolderPicker2
>(
439 createPicker(context
, QFileDialog::Directory
));
442 css::uno::Reference
<css::uno::XInterface
>
443 Qt5Instance::CreateClipboard(const css::uno::Sequence
<css::uno::Any
>& arguments
)
446 if (arguments
.getLength() == 0)
450 else if (arguments
.getLength() != 1 || !(arguments
[0] >>= sel
))
452 throw css::lang::IllegalArgumentException("bad Qt5Instance::CreateClipboard arguments",
453 css::uno::Reference
<css::uno::XInterface
>(), -1);
456 // This could also use RunInMain, but SolarMutexGuard is enough
457 // since at this point we're not accessing the clipboard, just get the
458 // accessor to the clipboard.
459 SolarMutexGuard aGuard
;
461 auto it
= m_aClipboards
.find(sel
);
462 if (it
!= m_aClipboards
.end())
465 css::uno::Reference
<css::uno::XInterface
> xClipboard
= Qt5Clipboard::create(sel
);
467 m_aClipboards
[sel
] = xClipboard
;
472 css::uno::Reference
<css::uno::XInterface
> Qt5Instance::CreateDragSource()
474 return css::uno::Reference
<css::uno::XInterface
>(
475 static_cast<cppu::OWeakObject
*>(new Qt5DragSource()));
478 css::uno::Reference
<css::uno::XInterface
> Qt5Instance::CreateDropTarget()
480 return css::uno::Reference
<css::uno::XInterface
>(
481 static_cast<cppu::OWeakObject
*>(new Qt5DropTarget()));
484 IMPL_LINK_NOARG(Qt5Instance
, updateStyleHdl
, Timer
*, void)
486 SolarMutexGuard aGuard
;
487 SalFrame
* pFrame
= anyFrame();
490 pFrame
->CallCallback(SalEvent::SettingsChanged
, nullptr);
493 pFrame
->CallCallback(SalEvent::FontChanged
, nullptr);
494 m_bUpdateFonts
= false;
499 void Qt5Instance::UpdateStyle(bool bFontsChanged
)
502 m_bUpdateFonts
= true;
503 if (!m_aUpdateStyleTimer
.IsActive())
504 m_aUpdateStyleTimer
.Start();
507 void* Qt5Instance::CreateGStreamerSink(const SystemChildWindow
* pWindow
)
509 #if ENABLE_GSTREAMER_1_0 && QT5_HAVE_GOBJECT
510 auto pSymbol
= gstElementFactoryNameSymbol();
514 const SystemEnvData
* pEnvData
= pWindow
->GetSystemData();
518 if (pEnvData
->platform
!= SystemEnvData::Platform::Wayland
)
521 GstElement
* pVideosink
= pSymbol("qwidget5videosink", "qwidget5videosink");
524 QWidget
* pQWidget
= static_cast<QWidget
*>(pEnvData
->pWidget
);
525 g_object_set(G_OBJECT(pVideosink
), "widget", pQWidget
, nullptr);
529 SAL_WARN("vcl.qt5", "Couldn't initialize qwidget5videosink."
530 " Video playback might not work as expected."
531 " Please install Qt5 packages for QtGStreamer.");
532 // with no videosink explicitly set, GStreamer will open its own (misplaced) window(s) to display video
542 void Qt5Instance::AllocFakeCmdlineArgs(std::unique_ptr
<char* []>& rFakeArgv
,
543 std::unique_ptr
<int>& rFakeArgc
,
544 std::vector
<FreeableCStr
>& rFakeArgvFreeable
)
546 OString
aVersion(qVersion());
547 SAL_INFO("vcl.qt5", "qt version string is " << aVersion
);
549 const sal_uInt32 nParams
= osl_getCommandArgCount();
551 sal_uInt32 nDisplayValueIdx
= 0;
552 OUString aParam
, aBin
;
554 for (sal_uInt32 nIdx
= 0; nIdx
< nParams
; ++nIdx
)
556 osl_getCommandArg(nIdx
, &aParam
.pData
);
557 if (aParam
!= "-display")
560 nDisplayValueIdx
= nIdx
;
563 osl_getExecutableFile(&aParam
.pData
);
564 osl_getSystemPathFromFileURL(aParam
.pData
, &aBin
.pData
);
565 OString aExec
= OUStringToOString(aBin
, osl_getThreadTextEncoding());
567 std::vector
<FreeableCStr
> aFakeArgvFreeable
;
568 aFakeArgvFreeable
.reserve(4);
569 aFakeArgvFreeable
.emplace_back(strdup(aExec
.getStr()));
570 aFakeArgvFreeable
.emplace_back(strdup("--nocrashhandler"));
571 if (nDisplayValueIdx
)
573 aFakeArgvFreeable
.emplace_back(strdup("-display"));
574 osl_getCommandArg(nDisplayValueIdx
, &aParam
.pData
);
575 aDisplay
= OUStringToOString(aParam
, osl_getThreadTextEncoding());
576 aFakeArgvFreeable
.emplace_back(strdup(aDisplay
.getStr()));
578 rFakeArgvFreeable
.swap(aFakeArgvFreeable
);
580 const int nFakeArgc
= rFakeArgvFreeable
.size();
581 rFakeArgv
.reset(new char*[nFakeArgc
]);
582 for (int i
= 0; i
< nFakeArgc
; i
++)
583 rFakeArgv
[i
] = rFakeArgvFreeable
[i
].get();
585 rFakeArgc
.reset(new int);
586 *rFakeArgc
= nFakeArgc
;
589 void Qt5Instance::MoveFakeCmdlineArgs(std::unique_ptr
<char* []>& rFakeArgv
,
590 std::unique_ptr
<int>& rFakeArgc
,
591 std::vector
<FreeableCStr
>& rFakeArgvFreeable
)
593 m_pFakeArgv
= std::move(rFakeArgv
);
594 m_pFakeArgc
= std::move(rFakeArgc
);
595 m_pFakeArgvFreeable
.swap(rFakeArgvFreeable
);
598 std::unique_ptr
<QApplication
> Qt5Instance::CreateQApplication(int& nArgc
, char** pArgv
)
600 QApplication::setAttribute(Qt::AA_DisableHighDpiScaling
);
602 FreeableCStr session_manager
;
603 if (getenv("SESSION_MANAGER") != nullptr)
605 session_manager
.reset(strdup(getenv("SESSION_MANAGER")));
606 unsetenv("SESSION_MANAGER");
609 std::unique_ptr
<QApplication
> pQApp
= std::make_unique
<QApplication
>(nArgc
, pArgv
);
611 if (session_manager
!= nullptr)
613 // coverity[tainted_string] - trusted source for setenv
614 setenv("SESSION_MANAGER", session_manager
.get(), 1);
617 QApplication::setQuitOnLastWindowClosed(false);
622 VCLPLUG_QT5_PUBLIC SalInstance
* create_SalInstance()
624 static const bool bUseCairo
= (nullptr != getenv("SAL_VCL_QT5_USE_CAIRO"));
626 std::unique_ptr
<char* []> pFakeArgv
;
627 std::unique_ptr
<int> pFakeArgc
;
628 std::vector
<FreeableCStr
> aFakeArgvFreeable
;
629 Qt5Instance::AllocFakeCmdlineArgs(pFakeArgv
, pFakeArgc
, aFakeArgvFreeable
);
631 std::unique_ptr
<QApplication
> pQApp
632 = Qt5Instance::CreateQApplication(*pFakeArgc
, pFakeArgv
.get());
634 Qt5Instance
* pInstance
= new Qt5Instance(pQApp
, bUseCairo
);
635 pInstance
->MoveFakeCmdlineArgs(pFakeArgv
, pFakeArgc
, aFakeArgvFreeable
);
637 new Qt5Data(pInstance
);
643 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */