/src/libreoffice/vcl/source/treelist/transfer2.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice project. |
4 | | * |
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/. |
8 | | * |
9 | | * This file incorporates work covered by the following license notice: |
10 | | * |
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 . |
18 | | */ |
19 | | |
20 | | #include <config_vclplug.h> |
21 | | |
22 | | #include <osl/mutex.hxx> |
23 | | #include <sot/exchange.hxx> |
24 | | #include <tools/debug.hxx> |
25 | | #include <vcl/dndlistenercontainer.hxx> |
26 | | #include <vcl/svapp.hxx> |
27 | | #include <vcl/window.hxx> |
28 | | #include <comphelper/lok.hxx> |
29 | | #include <comphelper/processfactory.hxx> |
30 | | #include <com/sun/star/datatransfer/clipboard/LokClipboard.hpp> |
31 | | #include <com/sun/star/datatransfer/clipboard/SystemClipboard.hpp> |
32 | | #include <com/sun/star/datatransfer/dnd/XDropTargetDragContext.hpp> |
33 | | #include <com/sun/star/datatransfer/dnd/XDragGestureRecognizer.hpp> |
34 | | #include <com/sun/star/datatransfer/dnd/XDropTarget.hpp> |
35 | | #include <com/sun/star/uno/DeploymentException.hpp> |
36 | | #include <svl/urlbmk.hxx> |
37 | | #include <vcl/transfer.hxx> |
38 | | |
39 | | #include <salinst.hxx> |
40 | | #include <svdata.hxx> |
41 | | |
42 | | using namespace ::com::sun::star::uno; |
43 | | using namespace ::com::sun::star::lang; |
44 | | using namespace ::com::sun::star::io; |
45 | | using namespace ::com::sun::star::datatransfer; |
46 | | using namespace ::com::sun::star::datatransfer::clipboard; |
47 | | using namespace ::com::sun::star::datatransfer::dnd; |
48 | | |
49 | | |
50 | | DragSourceHelper::DragGestureListener::DragGestureListener( DragSourceHelper& rDragSourceHelper ) : |
51 | 4.02k | mrParent( rDragSourceHelper ) |
52 | 4.02k | { |
53 | 4.02k | } |
54 | | |
55 | | |
56 | | DragSourceHelper::DragGestureListener::~DragGestureListener() |
57 | 4.02k | { |
58 | 4.02k | } |
59 | | |
60 | | |
61 | | void SAL_CALL DragSourceHelper::DragGestureListener::disposing( const EventObject& ) |
62 | 0 | { |
63 | 0 | } |
64 | | |
65 | | |
66 | | void SAL_CALL DragSourceHelper::DragGestureListener::dragGestureRecognized( const DragGestureEvent& rDGE ) |
67 | 0 | { |
68 | 0 | const SolarMutexGuard aGuard; |
69 | |
|
70 | 0 | const Point aPtPixel( rDGE.DragOriginX, rDGE.DragOriginY ); |
71 | 0 | mrParent.StartDrag( rDGE.DragAction, aPtPixel ); |
72 | 0 | } |
73 | | |
74 | | DragSourceHelper::DragSourceHelper(vcl::Window* pWindow) |
75 | 4.02k | : mpDNDListenerContainer(pWindow->GetDropTarget()) |
76 | 4.02k | { |
77 | 4.02k | if (mpDNDListenerContainer.is()) |
78 | 4.02k | { |
79 | 4.02k | mxDragGestureListener = new DragSourceHelper::DragGestureListener( *this ); |
80 | 4.02k | mpDNDListenerContainer->addDragGestureListener(mxDragGestureListener); |
81 | 4.02k | } |
82 | 4.02k | } |
83 | | |
84 | | |
85 | | void DragSourceHelper::dispose() |
86 | 8.04k | { |
87 | 8.04k | rtl::Reference<DNDListenerContainer> pTmp; |
88 | 8.04k | { |
89 | 8.04k | std::scoped_lock aGuard( maMutex ); |
90 | 8.04k | pTmp = std::move(mpDNDListenerContainer); |
91 | 8.04k | } |
92 | 8.04k | if (pTmp.is()) |
93 | 4.02k | pTmp->removeDragGestureListener(mxDragGestureListener); |
94 | 8.04k | } |
95 | | |
96 | | DragSourceHelper::~DragSourceHelper() |
97 | 4.02k | { |
98 | 4.02k | dispose(); |
99 | 4.02k | } |
100 | | |
101 | | |
102 | | void DragSourceHelper::StartDrag( sal_Int8, const Point& ) |
103 | 0 | { |
104 | 0 | } |
105 | | |
106 | | |
107 | | DropTargetHelper::DropTargetListener::DropTargetListener( DropTargetHelper& rDropTargetHelper ) : |
108 | 4.02k | mrParent( rDropTargetHelper ) |
109 | 4.02k | { |
110 | 4.02k | } |
111 | | |
112 | | |
113 | | DropTargetHelper::DropTargetListener::~DropTargetListener() |
114 | 4.02k | { |
115 | 4.02k | } |
116 | | |
117 | | |
118 | | void SAL_CALL DropTargetHelper::DropTargetListener::disposing( const EventObject& ) |
119 | 0 | { |
120 | 0 | } |
121 | | |
122 | | |
123 | | void SAL_CALL DropTargetHelper::DropTargetListener::drop( const DropTargetDropEvent& rDTDE ) |
124 | 0 | { |
125 | 0 | const SolarMutexGuard aGuard; |
126 | |
|
127 | 0 | try |
128 | 0 | { |
129 | 0 | AcceptDropEvent aAcceptEvent; |
130 | 0 | ExecuteDropEvent aExecuteEvt( rDTDE.DropAction & ~DNDConstants::ACTION_DEFAULT, Point( rDTDE.LocationX, rDTDE.LocationY ), rDTDE ); |
131 | |
|
132 | 0 | aExecuteEvt.mbDefault = ( ( rDTDE.DropAction & DNDConstants::ACTION_DEFAULT ) != 0 ); |
133 | | |
134 | | // in case of a default action, call ::AcceptDrop first and use the returned |
135 | | // accepted action as the execute action in the call to ::ExecuteDrop |
136 | 0 | aAcceptEvent.mnAction = aExecuteEvt.mnAction; |
137 | 0 | aAcceptEvent.maPosPixel = aExecuteEvt.maPosPixel; |
138 | 0 | static_cast<DropTargetEvent&>(const_cast<DropTargetDragEvent&>( aAcceptEvent.maDragEvent )) = rDTDE; |
139 | 0 | const_cast<DropTargetDragEvent&>( aAcceptEvent.maDragEvent ).DropAction = rDTDE.DropAction; |
140 | 0 | const_cast<DropTargetDragEvent&>( aAcceptEvent.maDragEvent ).LocationX = rDTDE.LocationX; |
141 | 0 | const_cast<DropTargetDragEvent&>( aAcceptEvent.maDragEvent ).LocationY = rDTDE.LocationY; |
142 | 0 | const_cast<DropTargetDragEvent&>( aAcceptEvent.maDragEvent ).SourceActions = rDTDE.SourceActions; |
143 | 0 | aAcceptEvent.mbLeaving = false; |
144 | 0 | aAcceptEvent.mbDefault = aExecuteEvt.mbDefault; |
145 | |
|
146 | 0 | sal_Int8 nRet = mrParent.AcceptDrop( aAcceptEvent ); |
147 | |
|
148 | 0 | if( DNDConstants::ACTION_NONE != nRet ) |
149 | 0 | { |
150 | 0 | rDTDE.Context->acceptDrop( nRet ); |
151 | |
|
152 | 0 | if( aExecuteEvt.mbDefault ) |
153 | 0 | aExecuteEvt.mnAction = nRet; |
154 | |
|
155 | 0 | nRet = mrParent.ExecuteDrop( aExecuteEvt ); |
156 | 0 | } |
157 | |
|
158 | 0 | rDTDE.Context->dropComplete( DNDConstants::ACTION_NONE != nRet ); |
159 | |
|
160 | 0 | mpLastDragOverEvent.reset(); |
161 | 0 | } |
162 | 0 | catch( const css::uno::Exception& ) |
163 | 0 | { |
164 | 0 | } |
165 | 0 | } |
166 | | |
167 | | |
168 | | void SAL_CALL DropTargetHelper::DropTargetListener::dragEnter( const DropTargetDragEnterEvent& rDTDEE ) |
169 | 0 | { |
170 | 0 | const SolarMutexGuard aGuard; |
171 | |
|
172 | 0 | try |
173 | 0 | { |
174 | 0 | mrParent.ImplBeginDrag( rDTDEE.SupportedDataFlavors ); |
175 | 0 | } |
176 | 0 | catch( const css::uno::Exception& ) |
177 | 0 | { |
178 | 0 | } |
179 | |
|
180 | 0 | dragOver( rDTDEE ); |
181 | 0 | } |
182 | | |
183 | | |
184 | | void SAL_CALL DropTargetHelper::DropTargetListener::dragOver( const DropTargetDragEvent& rDTDE ) |
185 | 0 | { |
186 | 0 | const SolarMutexGuard aGuard; |
187 | |
|
188 | 0 | try |
189 | 0 | { |
190 | 0 | mpLastDragOverEvent.reset( new AcceptDropEvent( rDTDE.DropAction & ~DNDConstants::ACTION_DEFAULT, Point( rDTDE.LocationX, rDTDE.LocationY ), rDTDE ) ); |
191 | 0 | mpLastDragOverEvent->mbDefault = ( ( rDTDE.DropAction & DNDConstants::ACTION_DEFAULT ) != 0 ); |
192 | |
|
193 | 0 | const sal_Int8 nRet = mrParent.AcceptDrop( *mpLastDragOverEvent ); |
194 | |
|
195 | 0 | if( DNDConstants::ACTION_NONE == nRet ) |
196 | 0 | rDTDE.Context->rejectDrag(); |
197 | 0 | else |
198 | 0 | rDTDE.Context->acceptDrag( nRet ); |
199 | 0 | } |
200 | 0 | catch( const css::uno::Exception& ) |
201 | 0 | { |
202 | 0 | } |
203 | 0 | } |
204 | | |
205 | | |
206 | | void SAL_CALL DropTargetHelper::DropTargetListener::dragExit( const DropTargetEvent& ) |
207 | 0 | { |
208 | 0 | const SolarMutexGuard aGuard; |
209 | |
|
210 | 0 | try |
211 | 0 | { |
212 | 0 | if( mpLastDragOverEvent ) |
213 | 0 | { |
214 | 0 | mpLastDragOverEvent->mbLeaving = true; |
215 | 0 | mrParent.AcceptDrop( *mpLastDragOverEvent ); |
216 | 0 | mpLastDragOverEvent.reset(); |
217 | 0 | } |
218 | |
|
219 | 0 | mrParent.ImplEndDrag(); |
220 | 0 | } |
221 | 0 | catch( const css::uno::Exception& ) |
222 | 0 | { |
223 | 0 | } |
224 | 0 | } |
225 | | |
226 | | |
227 | | void SAL_CALL DropTargetHelper::DropTargetListener::dropActionChanged( const DropTargetDragEvent& ) |
228 | 0 | { |
229 | 0 | } |
230 | | |
231 | | |
232 | | DropTargetHelper::DropTargetHelper( vcl::Window* pWindow ) : |
233 | 4.02k | mxDropTarget( pWindow->GetDropTarget() ) |
234 | 4.02k | { |
235 | 4.02k | ImplConstruct(); |
236 | 4.02k | } |
237 | | |
238 | | |
239 | | DropTargetHelper::DropTargetHelper( const Reference< XDropTarget >& rxDropTarget ) : |
240 | 0 | mxDropTarget( rxDropTarget ) |
241 | 0 | { |
242 | 0 | ImplConstruct(); |
243 | 0 | } |
244 | | |
245 | | |
246 | | void DropTargetHelper::dispose() |
247 | 8.04k | { |
248 | 8.04k | Reference< XDropTarget > xTmp; |
249 | 8.04k | { |
250 | 8.04k | std::scoped_lock aGuard( maMutex ); |
251 | 8.04k | xTmp = std::move(mxDropTarget); |
252 | 8.04k | } |
253 | 8.04k | if( xTmp.is() ) |
254 | 4.02k | xTmp->removeDropTargetListener( mxDropTargetListener ); |
255 | 8.04k | } |
256 | | |
257 | | DropTargetHelper::~DropTargetHelper() |
258 | 4.02k | { |
259 | 4.02k | dispose(); |
260 | 4.02k | } |
261 | | |
262 | | |
263 | | void DropTargetHelper::ImplConstruct() |
264 | 4.02k | { |
265 | 4.02k | if( mxDropTarget.is() ) |
266 | 4.02k | { |
267 | 4.02k | mxDropTargetListener = new DropTargetHelper::DropTargetListener( *this ); |
268 | 4.02k | mxDropTarget->addDropTargetListener( mxDropTargetListener ); |
269 | 4.02k | mxDropTarget->setActive( true ); |
270 | 4.02k | } |
271 | 4.02k | } |
272 | | |
273 | | |
274 | | void DropTargetHelper::ImplBeginDrag( const Sequence< DataFlavor >& rSupportedDataFlavors ) |
275 | 0 | { |
276 | 0 | maFormats.clear(); |
277 | 0 | TransferableDataHelper::FillDataFlavorExVector( rSupportedDataFlavors, maFormats ); |
278 | 0 | } |
279 | | |
280 | | |
281 | | void DropTargetHelper::ImplEndDrag() |
282 | 0 | { |
283 | 0 | maFormats.clear(); |
284 | 0 | } |
285 | | |
286 | | |
287 | | sal_Int8 DropTargetHelper::AcceptDrop( const AcceptDropEvent& ) |
288 | 0 | { |
289 | 0 | return DNDConstants::ACTION_NONE; |
290 | 0 | } |
291 | | |
292 | | |
293 | | sal_Int8 DropTargetHelper::ExecuteDrop( const ExecuteDropEvent& ) |
294 | 0 | { |
295 | 0 | return DNDConstants::ACTION_NONE; |
296 | 0 | } |
297 | | |
298 | | |
299 | | bool DropTargetHelper::IsDropFormatSupported(SotClipboardFormatId nFormat) const |
300 | 0 | { |
301 | 0 | return std::any_of(maFormats.begin(), maFormats.end(), |
302 | 0 | [&](const DataFlavorEx& data) { return data.mnSotId == nFormat; }); |
303 | 0 | } |
304 | | |
305 | | |
306 | | // TransferDataContainer |
307 | | |
308 | | namespace { |
309 | | |
310 | | struct TDataCntnrEntry_Impl |
311 | | { |
312 | | css::uno::Any aAny; |
313 | | SotClipboardFormatId nId; |
314 | | }; |
315 | | |
316 | | } |
317 | | |
318 | | struct TransferDataContainer_Impl |
319 | | { |
320 | | std::vector< TDataCntnrEntry_Impl > aFmtList; |
321 | | Link<sal_Int8,void> aFinishedLnk; |
322 | | std::optional<INetBookmark> moBookmk; |
323 | | |
324 | | TransferDataContainer_Impl() |
325 | 0 | { |
326 | 0 | } |
327 | | }; |
328 | | |
329 | | |
330 | | TransferDataContainer::TransferDataContainer() |
331 | 0 | : pImpl( new TransferDataContainer_Impl ) |
332 | 0 | { |
333 | 0 | } |
334 | | |
335 | | |
336 | | TransferDataContainer::~TransferDataContainer() |
337 | 0 | { |
338 | 0 | } |
339 | | |
340 | | |
341 | | void TransferDataContainer::AddSupportedFormats() |
342 | 0 | { |
343 | 0 | } |
344 | | |
345 | | |
346 | | bool TransferDataContainer::GetData( |
347 | | const css::datatransfer::DataFlavor& rFlavor, const OUString& /*rDestDoc*/ ) |
348 | 0 | { |
349 | 0 | bool bFnd = false; |
350 | 0 | SotClipboardFormatId nFmtId = SotExchange::GetFormat( rFlavor ); |
351 | | |
352 | | // test first the list |
353 | 0 | for (auto const& format : pImpl->aFmtList) |
354 | 0 | { |
355 | 0 | if( nFmtId == format.nId ) |
356 | 0 | { |
357 | 0 | bFnd = SetAny( format.aAny ); |
358 | 0 | break; |
359 | 0 | } |
360 | 0 | } |
361 | | |
362 | | // test second the bookmark pointer |
363 | 0 | if( !bFnd ) |
364 | 0 | switch( nFmtId ) |
365 | 0 | { |
366 | 0 | case SotClipboardFormatId::STRING: |
367 | 0 | case SotClipboardFormatId::SOLK: |
368 | 0 | case SotClipboardFormatId::NETSCAPE_BOOKMARK: |
369 | 0 | case SotClipboardFormatId::FILECONTENT: |
370 | 0 | case SotClipboardFormatId::FILEGRPDESCRIPTOR: |
371 | 0 | case SotClipboardFormatId::UNIFORMRESOURCELOCATOR: |
372 | 0 | if( pImpl->moBookmk ) |
373 | 0 | bFnd = SetINetBookmark( *pImpl->moBookmk, rFlavor ); |
374 | 0 | break; |
375 | | |
376 | 0 | default: break; |
377 | 0 | } |
378 | | |
379 | 0 | return bFnd; |
380 | 0 | } |
381 | | |
382 | | |
383 | | void TransferDataContainer::CopyINetBookmark( const INetBookmark& rBkmk ) |
384 | 0 | { |
385 | 0 | pImpl->moBookmk = rBkmk; |
386 | |
|
387 | 0 | AddFormat( SotClipboardFormatId::STRING ); |
388 | 0 | AddFormat( SotClipboardFormatId::SOLK ); |
389 | 0 | AddFormat( SotClipboardFormatId::NETSCAPE_BOOKMARK ); |
390 | 0 | AddFormat( SotClipboardFormatId::FILECONTENT ); |
391 | 0 | AddFormat( SotClipboardFormatId::FILEGRPDESCRIPTOR ); |
392 | 0 | AddFormat( SotClipboardFormatId::UNIFORMRESOURCELOCATOR ); |
393 | 0 | } |
394 | | |
395 | | |
396 | | void TransferDataContainer::CopyAnyData( SotClipboardFormatId nFormatId, |
397 | | const char* pData, sal_uInt32 nLen ) |
398 | 0 | { |
399 | 0 | if( nLen ) |
400 | 0 | { |
401 | 0 | TDataCntnrEntry_Impl aEntry; |
402 | 0 | aEntry.nId = nFormatId; |
403 | |
|
404 | 0 | Sequence< sal_Int8 > aSeq( nLen ); |
405 | 0 | memcpy( aSeq.getArray(), pData, nLen ); |
406 | 0 | aEntry.aAny <<= aSeq; |
407 | 0 | pImpl->aFmtList.push_back( aEntry ); |
408 | 0 | AddFormat( nFormatId ); |
409 | 0 | } |
410 | 0 | } |
411 | | |
412 | | |
413 | | void TransferDataContainer::CopyByteString( SotClipboardFormatId nFormatId, |
414 | | const OString& rStr ) |
415 | 0 | { |
416 | 0 | CopyAnyData( nFormatId, rStr.getStr(), rStr.getLength() ); |
417 | 0 | } |
418 | | |
419 | | |
420 | | void TransferDataContainer::CopyString( SotClipboardFormatId nFmt, const OUString& rStr ) |
421 | 0 | { |
422 | 0 | if( !rStr.isEmpty() ) |
423 | 0 | { |
424 | 0 | TDataCntnrEntry_Impl aEntry; |
425 | 0 | aEntry.nId = nFmt; |
426 | 0 | aEntry.aAny <<= rStr; |
427 | 0 | pImpl->aFmtList.push_back( aEntry ); |
428 | 0 | AddFormat( aEntry.nId ); |
429 | 0 | } |
430 | 0 | } |
431 | | |
432 | | |
433 | | void TransferDataContainer::CopyString( const OUString& rStr ) |
434 | 0 | { |
435 | 0 | CopyString( SotClipboardFormatId::STRING, rStr ); |
436 | 0 | } |
437 | | |
438 | | |
439 | | bool TransferDataContainer::HasAnyData() const |
440 | 0 | { |
441 | 0 | return !pImpl->aFmtList.empty() || |
442 | 0 | pImpl->moBookmk.has_value(); |
443 | 0 | } |
444 | | |
445 | | |
446 | | void TransferDataContainer::StartDrag( |
447 | | vcl::Window* pWindow, sal_Int8 nDragSourceActions, |
448 | | const Link<sal_Int8,void>& rLnk ) |
449 | 0 | { |
450 | 0 | pImpl->aFinishedLnk = rLnk; |
451 | 0 | TransferableHelper::StartDrag( pWindow, nDragSourceActions ); |
452 | 0 | } |
453 | | |
454 | | |
455 | | void TransferDataContainer::DragFinished( sal_Int8 nDropAction ) |
456 | 0 | { |
457 | 0 | pImpl->aFinishedLnk.Call( nDropAction ); |
458 | 0 | } |
459 | | |
460 | | Reference<XClipboard> GetSystemClipboard() |
461 | 4.02k | { |
462 | | // On Windows, the css.datatransfer.clipboard.SystemClipboard UNO service is implemented as a |
463 | | // single-instance service (dtrans_CWinClipboard_get_implementation in |
464 | | // vcl/win/dtrans/WinClipboard.cxx) that needs timely disposing to join a spawned thread |
465 | | // (done in DeInitVCL, vcl/source/app/svmain.cxx), while on other platforms multiple instances |
466 | | // are used, so we should not hold on to a single instance here: |
467 | | #if defined _WIN32 |
468 | | DBG_TESTSOLARMUTEX(); |
469 | | auto const data = ImplGetSVData(); |
470 | | if (!data->m_xSystemClipboard.is()) |
471 | | { |
472 | | try |
473 | | { |
474 | | data->m_xSystemClipboard = css::datatransfer::clipboard::SystemClipboard::create( |
475 | | comphelper::getProcessComponentContext()); |
476 | | } |
477 | | catch (DeploymentException const &) {} |
478 | | } |
479 | | return data->m_xSystemClipboard; |
480 | | #else |
481 | 4.02k | Reference<XClipboard> xClipboard; |
482 | 4.02k | try |
483 | 4.02k | { |
484 | | #ifdef IOS |
485 | | if (false) |
486 | | ; |
487 | | #else |
488 | 4.02k | if (comphelper::LibreOfficeKit::isActive()) |
489 | 0 | { |
490 | 0 | xClipboard = css::datatransfer::clipboard::LokClipboard::create( |
491 | 0 | comphelper::getProcessComponentContext()); |
492 | 0 | } |
493 | 4.02k | #endif |
494 | 4.02k | else |
495 | 4.02k | { |
496 | 4.02k | xClipboard = GetSalInstance()->CreateClipboard(ClipboardSelectionType::Clipboard); |
497 | 4.02k | } |
498 | 4.02k | } |
499 | 4.02k | catch (DeploymentException const &) {} |
500 | 4.02k | return xClipboard; |
501 | 4.02k | #endif |
502 | 4.02k | } |
503 | | |
504 | | Reference<XClipboard> GetSystemPrimarySelection() |
505 | 0 | { |
506 | 0 | Reference<XClipboard> xSelection; |
507 | 0 | try |
508 | 0 | { |
509 | | #if USING_X11 |
510 | | xSelection = GetSalInstance()->CreateClipboard(ClipboardSelectionType::Primary); |
511 | | #else |
512 | 0 | const Reference<XComponentContext>& xContext(comphelper::getProcessComponentContext()); |
513 | 0 | static Reference< XClipboard > s_xSelection( |
514 | 0 | xContext->getServiceManager()->createInstanceWithContext( |
515 | 0 | "com.sun.star.datatransfer.clipboard.GenericClipboard", xContext), UNO_QUERY); |
516 | 0 | xSelection = s_xSelection; |
517 | 0 | #endif |
518 | 0 | } |
519 | 0 | catch (RuntimeException const &) {} |
520 | 0 | return xSelection; |
521 | 0 | } |
522 | | |
523 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |