Line data Source code
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 :
21 : #include <unotools/streamwrap.hxx>
22 : #include <unotools/ucbstreamhelper.hxx>
23 : #include <comphelper/processfactory.hxx>
24 : #include <comphelper/string.hxx>
25 : #include <ucbhelper/content.hxx>
26 : #include <tools/resmgr.hxx>
27 : #include <tools/urlobj.hxx>
28 : #include <svl/solar.hrc>
29 : #include <svl/urihelper.hxx>
30 : #include <vcl/graphicfilter.hxx>
31 : #include <svl/itempool.hxx>
32 : #include <sfx2/docfile.hxx>
33 : #include <avmedia/mediawindow.hxx>
34 : #include <vcl/svapp.hxx>
35 : #include <vcl/settings.hxx>
36 : #include <svx/svdpage.hxx>
37 : #include <svx/svdograf.hxx>
38 : #include <svx/fmmodel.hxx>
39 : #include <svx/fmview.hxx>
40 : #include <svx/unomodel.hxx>
41 : #include "codec.hxx"
42 : #include "gallery.hrc"
43 : #include "svx/gallery1.hxx"
44 : #include "svx/galtheme.hxx"
45 : #include "svx/galmisc.hxx"
46 : #include <com/sun/star/sdbc/XResultSet.hpp>
47 : #include <com/sun/star/ucb/XContentAccess.hpp>
48 : #include <com/sun/star/ucb/TransferInfo.hpp>
49 : #include <com/sun/star/ucb/NameClash.hpp>
50 :
51 : using namespace ::rtl;
52 : using namespace ::com::sun::star;
53 :
54 0 : ResMgr* GetGalleryResMgr()
55 : {
56 : static ResMgr* pGalleryResMgr = NULL;
57 :
58 0 : if( !pGalleryResMgr )
59 : {
60 : pGalleryResMgr = ResMgr::CreateResMgr(
61 0 : "gal", Application::GetSettings().GetUILanguageTag() );
62 : }
63 :
64 0 : return pGalleryResMgr;
65 : }
66 :
67 0 : BitmapEx GalleryResGetBitmapEx( sal_uInt32 nId )
68 : {
69 0 : BitmapEx aBmpEx( GAL_RES( nId ) );
70 :
71 0 : if( !aBmpEx.IsTransparent() )
72 0 : aBmpEx = BitmapEx( aBmpEx.GetBitmap(), COL_LIGHTMAGENTA );
73 :
74 0 : return aBmpEx;
75 : }
76 :
77 0 : IMPL_LINK( SgaUserDataFactory, MakeUserData, SdrObjFactory*, pObjFactory )
78 : {
79 0 : if ( pObjFactory->nInventor == IV_IMAPINFO && pObjFactory->nIdentifier == ID_IMAPINFO )
80 0 : pObjFactory->pNewData = new SgaIMapInfo;
81 :
82 0 : return 0L;
83 : }
84 :
85 0 : sal_uInt16 GalleryGraphicImport( const INetURLObject& rURL, Graphic& rGraphic,
86 : OUString& rFilterName, bool bShowProgress )
87 : {
88 0 : sal_uInt16 nRet = SGA_IMPORT_NONE;
89 0 : SfxMedium aMedium( rURL.GetMainURL( INetURLObject::NO_DECODE ), STREAM_READ );
90 :
91 0 : aMedium.Download();
92 :
93 0 : SvStream* pIStm = aMedium.GetInStream();
94 :
95 0 : if( pIStm )
96 : {
97 0 : GraphicFilter& rGraphicFilter = GraphicFilter::GetGraphicFilter();
98 0 : GalleryProgress* pProgress = bShowProgress ? new GalleryProgress( &rGraphicFilter ) : NULL;
99 : sal_uInt16 nFormat;
100 :
101 0 : if( !rGraphicFilter.ImportGraphic( rGraphic, rURL.GetMainURL( INetURLObject::NO_DECODE ), *pIStm, GRFILTER_FORMAT_DONTKNOW, &nFormat ) )
102 : {
103 0 : rFilterName = rGraphicFilter.GetImportFormatName( nFormat );
104 0 : nRet = SGA_IMPORT_FILE;
105 : }
106 :
107 0 : delete pProgress;
108 : }
109 :
110 0 : return nRet;
111 : }
112 :
113 0 : bool GallerySvDrawImport( SvStream& rIStm, SdrModel& rModel )
114 : {
115 : sal_uInt32 nVersion;
116 0 : bool bRet = false;
117 :
118 0 : if( GalleryCodec::IsCoded( rIStm, nVersion ) )
119 : {
120 0 : SvMemoryStream aMemStm( 65535, 65535 );
121 0 : GalleryCodec aCodec( rIStm );
122 :
123 0 : aCodec.Read( aMemStm );
124 0 : aMemStm.Seek( 0UL );
125 :
126 0 : if( 1 == nVersion )
127 : {
128 : OSL_FAIL( "staroffice binary file formats are no longer supported inside the gallery!" );
129 0 : bRet = false;
130 : }
131 0 : else if( 2 == nVersion )
132 : {
133 : // recall to read as XML
134 0 : bRet = GallerySvDrawImport( aMemStm, rModel );
135 0 : }
136 : }
137 : else
138 : {
139 : // read as XML
140 0 : uno::Reference< io::XInputStream > xInputStream( new utl::OInputStreamWrapper( rIStm ) );
141 :
142 0 : rModel.GetItemPool().SetDefaultMetric( SFX_MAPUNIT_100TH_MM );
143 0 : uno::Reference< lang::XComponent > xComponent;
144 :
145 0 : bRet = SvxDrawingLayerImport( &rModel, xInputStream, xComponent, "com.sun.star.comp.Draw.XMLOasisImporter" );
146 0 : if( !bRet || (rModel.GetPageCount() == 0) )
147 : {
148 0 : rIStm.Seek(0);
149 0 : bRet = SvxDrawingLayerImport( &rModel, xInputStream, xComponent, "com.sun.star.comp.Draw.XMLImporter" );
150 0 : }
151 :
152 : }
153 :
154 0 : return bRet;
155 : }
156 :
157 0 : bool CreateIMapGraphic( const FmFormModel& rModel, Graphic& rGraphic, ImageMap& rImageMap )
158 : {
159 0 : bool bRet = false;
160 :
161 0 : if ( rModel.GetPageCount() )
162 : {
163 0 : const SdrPage* pPage = rModel.GetPage( 0 );
164 0 : const SdrObject* pObj = pPage->GetObj( 0 );
165 :
166 0 : if ( pPage->GetObjCount() == 1 && pObj->ISA( SdrGrafObj ) )
167 : {
168 0 : const sal_uInt16 nCount = pObj->GetUserDataCount();
169 :
170 : // Exist in the user data an IMap information?
171 0 : for ( sal_uInt16 i = 0; i < nCount; i++ )
172 : {
173 0 : const SdrObjUserData* pUserData = pObj->GetUserData( i );
174 :
175 0 : if ( ( pUserData->GetInventor() == IV_IMAPINFO ) && ( pUserData->GetId() == ID_IMAPINFO ) )
176 : {
177 0 : rGraphic = ( (SdrGrafObj*) pObj )->GetGraphic();
178 0 : rImageMap = ( (SgaIMapInfo*) pUserData )->GetImageMap();
179 0 : bRet = true;
180 0 : break;
181 : }
182 : }
183 : }
184 : }
185 :
186 0 : return bRet;
187 : }
188 :
189 0 : OUString GetReducedString( const INetURLObject& rURL, sal_Int32 nMaxLen )
190 : {
191 0 : OUString aReduced( rURL.GetMainURL( INetURLObject::DECODE_UNAMBIGUOUS ) );
192 :
193 0 : aReduced = aReduced.getToken( comphelper::string::getTokenCount(aReduced, '/') - 1, '/' );
194 :
195 0 : if( INET_PROT_PRIV_SOFFICE != rURL.GetProtocol() )
196 : {
197 : sal_Unicode aDelimiter;
198 0 : const OUString aPath( rURL.getFSysPath( INetURLObject::FSYS_DETECT, &aDelimiter ) );
199 0 : const OUString aName( aReduced );
200 :
201 0 : if( aPath.getLength() > nMaxLen )
202 : {
203 0 : sal_Int32 nPathPrefixLen = nMaxLen - aName.getLength() - 4;
204 :
205 0 : if (nPathPrefixLen >= 0)
206 : {
207 0 : aReduced = aPath.copy(0, nPathPrefixLen);
208 0 : aReduced += "...";
209 0 : aReduced += OUString(aDelimiter);
210 0 : aReduced += aName;
211 : }
212 : else
213 : {
214 0 : aReduced += "...";
215 0 : aReduced += OUString(aDelimiter);
216 0 : aReduced += "...";
217 0 : aReduced += aName.copy( aName.getLength() - (nMaxLen - 7) );
218 : }
219 : }
220 : else
221 0 : aReduced = aPath;
222 : }
223 :
224 0 : return aReduced;
225 : }
226 :
227 0 : OUString GetSvDrawStreamNameFromURL( const INetURLObject& rSvDrawObjURL )
228 : {
229 0 : OUString aRet;
230 :
231 0 : if( rSvDrawObjURL.GetProtocol() == INET_PROT_PRIV_SOFFICE &&
232 0 : comphelper::string::getTokenCount(rSvDrawObjURL.GetMainURL( INetURLObject::NO_DECODE ), '/') == 3 )
233 : {
234 0 : aRet = rSvDrawObjURL.GetMainURL( INetURLObject::NO_DECODE ).getToken( 2, '/' );
235 : }
236 :
237 0 : return aRet;
238 : }
239 :
240 0 : bool FileExists( const INetURLObject& rURL )
241 : {
242 0 : bool bRet = false;
243 :
244 0 : if( rURL.GetProtocol() != INET_PROT_NOT_VALID )
245 : {
246 : try
247 : {
248 0 : ::ucbhelper::Content aCnt( rURL.GetMainURL( INetURLObject::NO_DECODE ), uno::Reference< ucb::XCommandEnvironment >(), comphelper::getProcessComponentContext() );
249 0 : OUString aTitle;
250 :
251 0 : aCnt.getPropertyValue("Title") >>= aTitle;
252 0 : bRet = ( !aTitle.isEmpty() );
253 : }
254 0 : catch( const ucb::ContentCreationException& )
255 : {
256 : }
257 0 : catch( const uno::RuntimeException& )
258 : {
259 : }
260 0 : catch( const uno::Exception& )
261 : {
262 : }
263 : }
264 :
265 0 : return bRet;
266 : }
267 :
268 0 : bool CreateDir( const INetURLObject& rURL )
269 : {
270 0 : bool bRet = FileExists( rURL );
271 :
272 0 : if( !bRet )
273 : {
274 : try
275 : {
276 0 : uno::Reference< ucb::XCommandEnvironment > aCmdEnv;
277 0 : INetURLObject aNewFolderURL( rURL );
278 0 : INetURLObject aParentURL( aNewFolderURL ); aParentURL.removeSegment();
279 0 : ::ucbhelper::Content aParent( aParentURL.GetMainURL( INetURLObject::NO_DECODE ), aCmdEnv, comphelper::getProcessComponentContext() );
280 0 : uno::Sequence< OUString > aProps( 1 );
281 0 : uno::Sequence< uno::Any > aValues( 1 );
282 :
283 0 : aProps[0] = "Title";
284 0 : aValues[0] = uno::makeAny( OUString( aNewFolderURL.GetName() ) );
285 :
286 0 : ::ucbhelper::Content aContent( aNewFolderURL.GetMainURL( INetURLObject::NO_DECODE ), aCmdEnv, comphelper::getProcessComponentContext() );
287 0 : bRet = aParent.insertNewContent( OUString("application/vnd.sun.staroffice.fsys-folder"), aProps, aValues, aContent );
288 : }
289 0 : catch( const ucb::ContentCreationException& )
290 : {
291 : }
292 0 : catch( const uno::RuntimeException& )
293 : {
294 : }
295 0 : catch( const uno::Exception& )
296 : {
297 : }
298 : }
299 :
300 0 : return bRet;
301 : }
302 :
303 0 : bool CopyFile( const INetURLObject& rSrcURL, const INetURLObject& rDstURL )
304 : {
305 0 : bool bRet = false;
306 :
307 : try
308 : {
309 0 : ::ucbhelper::Content aDestPath( rDstURL.GetMainURL( INetURLObject::NO_DECODE ), uno::Reference< ucb::XCommandEnvironment >(), comphelper::getProcessComponentContext() );
310 :
311 : aDestPath.executeCommand( OUString("transfer"),
312 : uno::makeAny( ucb::TransferInfo( sal_False, rSrcURL.GetMainURL( INetURLObject::NO_DECODE ),
313 0 : rDstURL.GetName(), ucb::NameClash::OVERWRITE ) ) );
314 0 : bRet = true;
315 : }
316 0 : catch( const ucb::ContentCreationException& )
317 : {
318 : }
319 0 : catch( const uno::RuntimeException& )
320 : {
321 : }
322 0 : catch( const uno::Exception& )
323 : {
324 : }
325 :
326 0 : return bRet;
327 : }
328 :
329 0 : bool KillFile( const INetURLObject& rURL )
330 : {
331 0 : bool bRet = FileExists( rURL );
332 :
333 0 : if( bRet )
334 : {
335 : try
336 : {
337 0 : ::ucbhelper::Content aCnt( rURL.GetMainURL( INetURLObject::NO_DECODE ), uno::Reference< ucb::XCommandEnvironment >(), comphelper::getProcessComponentContext() );
338 0 : aCnt.executeCommand( OUString("delete"), uno::makeAny( sal_Bool( sal_True ) ) );
339 : }
340 0 : catch( const ucb::ContentCreationException& )
341 : {
342 0 : bRet = false;
343 : }
344 0 : catch( const uno::RuntimeException& )
345 : {
346 0 : bRet = false;
347 : }
348 0 : catch( const uno::Exception& )
349 : {
350 0 : bRet = false;
351 : }
352 : }
353 :
354 0 : return bRet;
355 : }
356 :
357 :
358 0 : GalleryProgress::GalleryProgress( GraphicFilter* pFilter ) :
359 0 : mpFilter( pFilter )
360 : {
361 :
362 0 : uno::Reference< lang::XMultiServiceFactory > xMgr( ::comphelper::getProcessServiceFactory() );
363 :
364 0 : uno::Reference< awt::XProgressMonitor > xMonitor( xMgr->createInstance(
365 0 : OUString("com.sun.star.awt.XProgressMonitor") ),
366 0 : uno::UNO_QUERY );
367 :
368 0 : if ( xMonitor.is() )
369 : {
370 0 : mxProgressBar = uno::Reference< awt::XProgressBar >( xMonitor, uno::UNO_QUERY );
371 :
372 0 : if( mxProgressBar.is() )
373 : {
374 0 : OUString aProgressText;
375 :
376 0 : if( mpFilter )
377 : {
378 0 : aProgressText = GAL_RESSTR(RID_SVXSTR_GALLERY_FILTER);
379 : // mpFilter->SetUpdatePercentHdl( LINK( this, GalleryProgress, Update ) ); // sj: progress wasn't working up from SO7 at all
380 : // // so I am removing this. The gallery progress should
381 : // // be changed to use the XStatusIndicator instead of XProgressMonitor
382 : }
383 : else
384 0 : aProgressText = "Gallery";
385 :
386 0 : xMonitor->addText( "Gallery", aProgressText, sal_False ) ;
387 0 : mxProgressBar->setRange( 0, GALLERY_PROGRESS_RANGE );
388 : }
389 0 : }
390 0 : }
391 :
392 0 : GalleryProgress::~GalleryProgress()
393 : {
394 0 : }
395 :
396 0 : void GalleryProgress::Update( sal_uIntPtr nVal, sal_uIntPtr nMaxVal )
397 : {
398 0 : if( mxProgressBar.is() && nMaxVal )
399 0 : mxProgressBar->setValue( std::min( (sal_uIntPtr)( (double) nVal / nMaxVal * GALLERY_PROGRESS_RANGE ), (sal_uIntPtr) GALLERY_PROGRESS_RANGE ) );
400 0 : }
401 :
402 :
403 0 : GalleryTransferable::GalleryTransferable( GalleryTheme* pTheme, sal_uIntPtr nObjectPos, bool bLazy ) :
404 : mpTheme( pTheme ),
405 0 : meObjectKind( mpTheme->GetObjectKind( nObjectPos ) ),
406 : mnObjectPos( nObjectPos ),
407 : mpGraphicObject( NULL ),
408 : mpImageMap( NULL ),
409 0 : mpURL( NULL )
410 : {
411 :
412 0 : InitData( bLazy );
413 0 : }
414 :
415 0 : GalleryTransferable::~GalleryTransferable()
416 : {
417 0 : }
418 :
419 0 : void GalleryTransferable::InitData( bool bLazy )
420 : {
421 0 : switch( meObjectKind )
422 : {
423 : case( SGA_OBJ_SVDRAW ):
424 : {
425 0 : if( !bLazy )
426 : {
427 0 : if( !mpGraphicObject )
428 : {
429 0 : Graphic aGraphic;
430 :
431 0 : if( mpTheme->GetGraphic( mnObjectPos, aGraphic ) )
432 0 : mpGraphicObject = new GraphicObject( aGraphic );
433 : }
434 :
435 0 : if( !mxModelStream.Is() )
436 : {
437 0 : mxModelStream = new SotStorageStream( "" );
438 0 : mxModelStream->SetBufferSize( 16348 );
439 :
440 0 : if( !mpTheme->GetModelStream( mnObjectPos, mxModelStream ) )
441 0 : mxModelStream.Clear();
442 : else
443 0 : mxModelStream->Seek( 0 );
444 : }
445 : }
446 : }
447 0 : break;
448 :
449 : case( SGA_OBJ_ANIM ):
450 : case( SGA_OBJ_BMP ):
451 : case( SGA_OBJ_INET ):
452 : case( SGA_OBJ_SOUND ):
453 : {
454 0 : if( !mpURL )
455 : {
456 0 : mpURL = new INetURLObject;
457 :
458 0 : if( !mpTheme->GetURL( mnObjectPos, *mpURL ) )
459 0 : delete mpURL, mpURL = NULL;
460 : }
461 :
462 0 : if( ( SGA_OBJ_SOUND != meObjectKind ) && !mpGraphicObject )
463 : {
464 0 : Graphic aGraphic;
465 :
466 0 : if( mpTheme->GetGraphic( mnObjectPos, aGraphic ) )
467 0 : mpGraphicObject = new GraphicObject( aGraphic );
468 : }
469 : }
470 0 : break;
471 :
472 : default:
473 : OSL_FAIL( "GalleryTransferable::GalleryTransferable: invalid object type" );
474 0 : break;
475 : }
476 0 : }
477 :
478 0 : void GalleryTransferable::AddSupportedFormats()
479 : {
480 0 : if( SGA_OBJ_SVDRAW == meObjectKind )
481 : {
482 0 : AddFormat( SOT_FORMATSTR_ID_DRAWING );
483 0 : AddFormat( SOT_FORMATSTR_ID_SVXB );
484 0 : AddFormat( FORMAT_GDIMETAFILE );
485 0 : AddFormat( FORMAT_BITMAP );
486 : }
487 : else
488 : {
489 0 : if( mpURL )
490 0 : AddFormat( FORMAT_FILE );
491 :
492 0 : if( mpGraphicObject )
493 : {
494 0 : AddFormat( SOT_FORMATSTR_ID_SVXB );
495 :
496 0 : if( mpGraphicObject->GetType() == GRAPHIC_GDIMETAFILE )
497 : {
498 0 : AddFormat( FORMAT_GDIMETAFILE );
499 0 : AddFormat( FORMAT_BITMAP );
500 : }
501 : else
502 : {
503 0 : AddFormat( FORMAT_BITMAP );
504 0 : AddFormat( FORMAT_GDIMETAFILE );
505 : }
506 : }
507 : }
508 0 : }
509 :
510 0 : bool GalleryTransferable::GetData( const datatransfer::DataFlavor& rFlavor )
511 : {
512 0 : sal_uInt32 nFormat = SotExchange::GetFormat( rFlavor );
513 0 : bool bRet = false;
514 :
515 0 : InitData( false );
516 :
517 0 : if( ( SOT_FORMATSTR_ID_DRAWING == nFormat ) && ( SGA_OBJ_SVDRAW == meObjectKind ) )
518 : {
519 0 : bRet = ( mxModelStream.Is() && SetObject( &mxModelStream, 0, rFlavor ) );
520 : }
521 0 : else if( ( SOT_FORMATSTR_ID_SVIM == nFormat ) && mpImageMap )
522 : {
523 : // TODO/MBA: do we need a BaseURL here?!
524 0 : bRet = SetImageMap( *mpImageMap, rFlavor );
525 : }
526 0 : else if( ( FORMAT_FILE == nFormat ) && mpURL )
527 : {
528 0 : bRet = SetString( mpURL->GetMainURL( INetURLObject::NO_DECODE ), rFlavor );
529 : }
530 0 : else if( ( SOT_FORMATSTR_ID_SVXB == nFormat ) && mpGraphicObject )
531 : {
532 0 : bRet = SetGraphic( mpGraphicObject->GetGraphic(), rFlavor );
533 : }
534 0 : else if( ( FORMAT_GDIMETAFILE == nFormat ) && mpGraphicObject )
535 : {
536 0 : bRet = SetGDIMetaFile( mpGraphicObject->GetGraphic().GetGDIMetaFile(), rFlavor );
537 : }
538 0 : else if( ( FORMAT_BITMAP == nFormat ) && mpGraphicObject )
539 : {
540 0 : bRet = SetBitmapEx( mpGraphicObject->GetGraphic().GetBitmapEx(), rFlavor );
541 : }
542 :
543 0 : return bRet;
544 : }
545 :
546 0 : bool GalleryTransferable::WriteObject( SotStorageStreamRef& rxOStm, void* pUserObject,
547 : sal_uInt32, const datatransfer::DataFlavor& )
548 : {
549 0 : bool bRet = false;
550 :
551 0 : if( pUserObject )
552 : {
553 0 : rxOStm->WriteStream( *static_cast< SotStorageStream* >( pUserObject ) );
554 0 : bRet = ( rxOStm->GetError() == ERRCODE_NONE );
555 : }
556 :
557 0 : return bRet;
558 : }
559 :
560 0 : void GalleryTransferable::DragFinished( sal_Int8 nDropAction )
561 : {
562 0 : mpTheme->SetDragging( false );
563 0 : mpTheme->SetDragPos( 0 );
564 0 : if ( nDropAction )
565 : {
566 0 : Window *pFocusWindow = Application::GetFocusWindow();
567 0 : if ( pFocusWindow )
568 0 : pFocusWindow->GrabFocusToDocument();
569 : }
570 0 : }
571 :
572 0 : void GalleryTransferable::ObjectReleased()
573 : {
574 0 : mxModelStream.Clear();
575 0 : delete mpGraphicObject, mpGraphicObject = NULL;
576 0 : delete mpImageMap, mpImageMap = NULL;
577 0 : delete mpURL, mpURL = NULL;
578 0 : }
579 :
580 0 : void GalleryTransferable::CopyToClipboard( Window* pWindow )
581 : {
582 0 : TransferableHelper::CopyToClipboard( pWindow );
583 0 : }
584 :
585 0 : void GalleryTransferable::StartDrag( Window* pWindow, sal_Int8 nDragSourceActions,
586 : sal_Int32 nDragPointer, sal_Int32 nDragImage )
587 : {
588 0 : INetURLObject aURL;
589 :
590 0 : if( mpTheme->GetURL( mnObjectPos, aURL ) && ( aURL.GetProtocol() != INET_PROT_NOT_VALID ) )
591 : {
592 0 : mpTheme->SetDragging( true );
593 0 : mpTheme->SetDragPos( mnObjectPos );
594 0 : TransferableHelper::StartDrag( pWindow, nDragSourceActions, nDragPointer, nDragImage );
595 0 : }
596 0 : }
597 :
598 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|