Version 4.3.0.0.beta1, tag libreoffice-4.3.0.0.beta1
[LibreOffice.git] / vcl / unx / kde / fpicker / kdefilepicker.cxx
blob6b27ae4afb52ceef102012df1a116d89195fb966
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
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 <cstddef>
22 #include <kdecommandthread.hxx>
23 #include <kdefilepicker.hxx>
25 #include <config_vclplug.h>
27 #if ENABLE_TDE
29 #include <tqcheckbox.h>
30 #include <tqcombobox.h>
31 #include <tqgrid.h>
32 #include <tqhbox.h>
33 #include <tqlabel.h>
34 #include <tqlayout.h>
35 #include <tqobjectlist.h>
36 #include <tqpushbutton.h>
37 #include <tqregexp.h>
38 #include <tqvbox.h>
40 #ifdef TQT_NO_EMIT
41 #define emit
42 #endif
44 #include <tdeversion.h>
45 #include <tdediroperator.h>
46 #include <tdefiledialog.h>
47 #include <tdefilefiltercombo.h>
48 #include <tdeio/netaccess.h>
49 #include <tdelocale.h>
50 #include <tdemessagebox.h>
51 #include <tdetempfile.h>
53 #else // ENABLE_TDE
55 #include <qcheckbox.h>
56 #include <qcombobox.h>
57 #include <qgrid.h>
58 #include <qhbox.h>
59 #include <qlabel.h>
60 #include <qlayout.h>
61 #include <qobjectlist.h>
62 #include <qpushbutton.h>
63 #include <qregexp.h>
64 #include <qvbox.h>
66 #ifdef QT_NO_EMIT
67 #define emit
68 #endif
70 #include <kdeversion.h>
71 #include <kdiroperator.h>
72 #include <kfiledialog.h>
73 #include <kfilefiltercombo.h>
74 #include <kio/netaccess.h>
75 #include <klocale.h>
76 #include <kmessagebox.h>
77 #include <ktempfile.h>
79 #endif // ENABLE_TDE
81 #if ENABLE_TDE
82 #define QCheckBox_String "TQCheckBox"
83 #define QComboBox_String "TQComboBox"
84 #else // ENABLE_TDE
85 #define QCheckBox_String "QCheckBox"
86 #define QComboBox_String "QComboBox"
87 #endif // ENABLE_TDE
89 #include <algorithm>
90 #include <iostream>
92 // KDEFileDialog
94 KDEFileDialog::KDEFileDialog( const QString &startDir, const QString &filter,
95 QWidget *parent, const char *name )
96 : KFileDialog( startDir, filter, parent, name, true, m_pCustomWidget = new QVBox() ),
97 m_pCombosAndButtons( new QHBox( m_pCustomWidget ) ),
98 m_pLabels( new QVBox( m_pCombosAndButtons ) ),
99 m_pComboBoxes( new QVBox( m_pCombosAndButtons ) ),
100 m_pPushButtons( new QVBox( m_pCombosAndButtons ) ),
101 m_pCheckBoxes( new QGrid( 2, m_pCustomWidget ) ),
102 m_bIsSave( false ),
103 m_bIsExecuting( false ),
104 m_bCanNotifySelection( true )
106 #if ENABLE_TDE
107 connect( this, SIGNAL( fileHighlighted( const TQString & ) ),
108 this, SLOT( fileHighlightedCommand( const TQString & ) ) );
109 #else // ENABLE_TDE
110 connect( this, SIGNAL( fileHighlighted( const QString & ) ),
111 this, SLOT( fileHighlightedCommand( const QString & ) ) );
112 #endif // ENABLE_TDE
114 connect( this, SIGNAL( selectionChanged() ),
115 this, SLOT( selectionChangedCommand() ) );
117 m_pCustomWidget->setSpacing( KDialog::spacingHint() );
118 m_pCombosAndButtons->setSpacing( KDialog::spacingHint() );
120 updateCustomWidgetLayout();
123 KDEFileDialog::~KDEFileDialog()
127 void KDEFileDialog::resizeEvent( QResizeEvent *pEvent )
129 KFileDialog::resizeEvent( pEvent );
131 updateCustomWidgetLayout();
134 void KDEFileDialog::showEvent( QShowEvent *pEvent )
136 KFileDialog::showEvent( pEvent );
138 updateCustomWidgetLayout();
141 void KDEFileDialog::updateCustomWidgetLayout()
143 QPoint qReferencePoint = filterWidget->mapTo( this, QPoint( 0, 0 ) );
144 QPoint qCustomPoint = m_pCustomWidget->mapTo( this, QPoint( 0, 0 ) );
146 int nLeft = qReferencePoint.x() - qCustomPoint.x();
147 int nRight = m_pCustomWidget->width() - filterWidget->width() - nLeft;
149 nLeft -= KDialog::spacingHint();
150 nRight -= KDialog::spacingHint();
151 m_pLabels->setFixedWidth( ( nLeft > 0 )? nLeft: 80 );
152 // FIXME The following call sets the width of m_pPushButtons all right,
153 // but it also increases the width of m_pComboBoxes rapidly. Can we do
154 // anything about it?
155 m_pPushButtons->setFixedWidth( ( nRight > 0 )? nRight: 100 );
158 void KDEFileDialog::customEvent( QCustomEvent *pEvent )
160 if ( pEvent && pEvent->type() == KDECommandEvent::TypeId )
162 KDECommandEvent *pCommandEvent = static_cast< KDECommandEvent* >( pEvent );
163 QStringList *pStringList = pCommandEvent->stringList();
165 int nListSize = -1;
166 if ( pStringList )
167 nListSize = pStringList->size();
169 switch ( pCommandEvent->command() )
171 case KDECommandEvent::AppendControl:
172 if ( nListSize >= 3 )
174 appendControl( (*pStringList)[0], (*pStringList)[1], (*pStringList)[2] );
176 break;
177 case KDECommandEvent::EnableControl:
178 if ( nListSize >= 2 )
180 enableControl( (*pStringList)[0], (*pStringList)[1] );
182 break;
183 case KDECommandEvent::GetValue:
184 if ( nListSize >= 2 )
186 getValue( (*pStringList)[0], (*pStringList)[1] );
188 break;
189 case KDECommandEvent::SetValue:
190 if ( nListSize >= 2 )
192 QStringList qStringList = (*pStringList);
193 qStringList.pop_front();
194 qStringList.pop_front();
196 setValue( (*pStringList)[0], (*pStringList)[1], qStringList );
198 break;
199 case KDECommandEvent::AppendFilter:
200 if ( nListSize >= 2 )
202 appendFilter( (*pStringList)[0], (*pStringList)[1] );
204 // update the filters widget
205 setFilter( filters() );
207 break;
208 case KDECommandEvent::AppendFilterGroup:
209 if ( nListSize >= 1 )
211 QStringList::const_iterator it = pStringList->begin();
212 ++it; // We ignore the filter group name
214 while ( it != pStringList->end() )
216 QString qTitle = *it;
217 ++it;
218 if ( it != pStringList->end() )
220 appendFilter( qTitle, (*it) );
221 ++it;
225 // update the filters widget
226 setFilter( filters() );
228 break;
229 case KDECommandEvent::GetCurrentFilter:
231 QString qCurrentFilter = filterWidget->currentText();
232 sendCommand( "currentFilter " + escapeString( qCurrentFilter ) );
234 break;
235 case KDECommandEvent::SetCurrentFilter:
236 if ( nListSize >= 1 )
238 static_cast< KDEFileFilterComboHack* >( filterWidget )->setCurrentFilter( pStringList->front() );
240 break;
241 case KDECommandEvent::GetDirectory:
243 QString qDirectory = baseURL().url();
244 if ( qDirectory.startsWith( "file:/" ) && qDirectory.mid( 6, 1 ) != "/" )
245 qDirectory.replace( "file:/", "file:///" );
246 sendCommand( "currentDirectory " + escapeString( qDirectory ) );
248 break;
249 case KDECommandEvent::SetDirectory:
250 if ( nListSize >= 1 )
252 setURL( pStringList->front() );
254 break;
255 case KDECommandEvent::GetFiles:
257 QString qString;
258 qString.reserve( 1024 );
260 qString.append( "files" );
262 if ( result() == QDialog::Accepted )
264 KURL::List qList( selectedURLs() );
265 for ( KURL::List::const_iterator it = qList.begin(); it != qList.end(); ++it )
266 appendURL( qString, (*it) );
268 else
270 // we have to return the selected files anyway
271 const KFileItemList *pItems = ops->selectedItems();
272 for ( KFileItemListIterator it( *pItems ); it.current(); ++it )
273 appendURL( qString, (*it)->url() );
276 sendCommand( qString );
277 setCanNotifySelection( true );
279 break;
280 case KDECommandEvent::SetTitle:
281 if ( nListSize >= 1 )
283 setCaption( pStringList->front() );
285 break;
286 case KDECommandEvent::SetType:
287 if ( nListSize >= 1 )
289 QString qType( pStringList->front() );
290 if ( qType == "open" )
292 setIsSave( false );
293 setCaption( i18n( "Open" ) );
295 else if ( qType == "save" )
297 setIsSave( true );
298 setCaption( i18n( "Save As" ) );
301 break;
302 case KDECommandEvent::SetDefaultName:
303 if ( nListSize >= 1 )
305 setKeepLocation( true );
306 setSelection( pStringList->front() );
308 break;
309 case KDECommandEvent::SetMultiSelection:
310 if ( nListSize >= 1 )
312 if ( pStringList->front() == "true" )
313 setMode( KFile::Files );
314 else
315 setMode( KFile::File );
317 break;
318 case KDECommandEvent::Exec:
320 filterWidget->setEditable( false );
321 setIsExecuting( true );
322 bool bCanExit = false;
323 do {
324 setCanNotifySelection( true );
325 exec();
327 KURL qLocalSelectedURL = mostLocalURL( selectedURL() );
328 QString qProtocol( qLocalSelectedURL.protocol() );
330 if ( isSave() && result() == QDialog::Accepted )
332 if ( qProtocol == "file" )
334 QString qFileName( addExtension( qLocalSelectedURL.path() ) );
335 bCanExit =
336 !QFile::exists( qFileName ) ||
337 ( KMessageBox::warningYesNo( 0,
338 i18n( "A file named \"%1\" already exists. "
339 "Are you sure you want to overwrite it?" ).arg( qFileName ),
340 i18n( "Overwrite File?" ),
341 i18n( "Overwrite" ), KStdGuiItem::cancel() ) == KMessageBox::Yes );
343 else if ( !isSupportedProtocol( qProtocol ) )
345 KMessageBox::sorry( 0,
346 i18n( "Saving using protocol \"%1\" is not supported." ).arg( qProtocol ) );
347 bCanExit = false;
349 else
350 bCanExit = true;
352 else if ( !isSave() && result() == QDialog::Accepted && !isSupportedProtocol( qProtocol ) )
354 KMessageBox::information( 0,
355 i18n( "Protocol \"%1\" is supported only partially. "
356 "Local copy of the file will be created." ).arg( qProtocol ) );
357 bCanExit = true;
359 else
360 bCanExit = true;
361 } while ( !bCanExit );
362 setIsExecuting( false );
364 if ( result() == QDialog::Accepted )
365 sendCommand( "accept" );
366 else
367 sendCommand( "reject" );
369 break;
370 default:
371 break;
374 // FIXME Some cleanup of pEvent? delete something, etc.?
378 void KDEFileDialog::appendControl( const QString &rId, const QString &rType, const QString &rTitle )
380 QString qLabel( rTitle );
381 qLabel.replace( '~', '&' );
383 if ( rType == "checkbox" )
385 QCheckBox *pCheckBox = new QCheckBox( qLabel, m_pCheckBoxes, rId.utf8() );
387 pCheckBox->setEnabled( true );
388 pCheckBox->setChecked( false );
390 else if ( rType == "listbox" )
392 QLabel *pComboLabel = new QLabel( qLabel, m_pLabels );
393 QComboBox *pComboBox = new QComboBox( m_pComboBoxes, rId.utf8() );
395 pComboLabel->setBuddy( pComboBox );
396 pComboBox->setEnabled( true );
398 else if ( rType == "pushbutton" )
400 QPushButton *pPushButton = new QPushButton( qLabel, m_pPushButtons, rId.utf8() );
401 pPushButton->setEnabled( true );
405 QWidget* KDEFileDialog::findControl( const QString &rId ) const
407 QObjectList *pList = m_pCustomWidget->queryList();
408 QCString qName( rId.utf8() );
409 QObjectList::const_iterator it = pList->begin();
411 for ( ; it != pList->end() && qName != (*it)->name(); ++it )
414 QWidget *pWidget = NULL;
415 if ( it != pList->end() )
416 pWidget = static_cast< QWidget* >( *it );
418 delete pList;
420 return pWidget;
423 void KDEFileDialog::enableControl( const QString &rId, const QString &rValue )
425 QWidget *pWidget = findControl( rId );
427 if ( pWidget )
428 pWidget->setEnabled( rValue.lower() == "true" );
431 void KDEFileDialog::getValue( const QString &rId, const QString &rAction )
433 QWidget *pWidget = findControl( rId );
434 QString qString;
435 qString.reserve( 1024 );
436 qString.append( "value" );
438 if ( pWidget )
440 QCString qClassName = pWidget->className();
441 if ( qClassName == QCheckBox_String )
443 QCheckBox *pCheckBox = static_cast< QCheckBox* >( pWidget );
445 if ( pCheckBox->isChecked() )
446 qString.append( " bool true" );
447 else
448 qString.append( " bool false" );
450 else if ( qClassName == QComboBox_String )
452 QComboBox *pComboBox = static_cast< QComboBox* >( pWidget );
453 if ( rAction == "getItems" )
455 qString.append( " stringList" );
456 for ( int nIdx = 0; nIdx < pComboBox->count(); ++nIdx )
458 qString.append( ' ' );
459 appendEscaped( qString, pComboBox->text( nIdx ) );
462 else if ( rAction == "getSelectedItem" )
464 qString.append( " string " );
465 appendEscaped( qString, pComboBox->currentText() );
467 else if ( rAction == "getSelectedItemIndex" )
469 qString.append( " int " );
470 qString.append( QString().setNum( pComboBox->currentItem() ) );
472 // TODO getHelpURL
474 // TODO push button
477 sendCommand( qString );
480 void KDEFileDialog::setValue( const QString &rId, const QString &rAction, const QStringList &rValue )
482 QWidget *pWidget = findControl( rId );
484 if ( pWidget )
486 QCString qClassName = pWidget->className();
487 if ( qClassName == QCheckBox_String )
489 QCheckBox *pCheckBox = static_cast< QCheckBox* >( pWidget );
491 bool bValue = ( !rValue.isEmpty() ) && ( rValue.front().lower() == "true" );
492 pCheckBox->setChecked( bValue );
494 else if ( qClassName == QComboBox_String )
496 QComboBox *pComboBox = static_cast< QComboBox* >( pWidget );
497 if ( rAction == "addItem" )
499 if ( !rValue.isEmpty() )
500 pComboBox->insertItem( rValue.front() );
502 else if ( rAction == "addItems" )
504 pComboBox->insertStringList( rValue );
506 else if ( rAction == "deleteItem" )
508 if ( !rValue.isEmpty() )
509 pComboBox->removeItem( rValue.front().toInt() );
511 else if ( rAction == "deleteItems" )
513 pComboBox->clear();
515 else if ( rAction == "setSelectedItem" )
517 if ( !rValue.isEmpty() )
518 pComboBox->setCurrentItem( rValue.front().toInt() );
520 // FIXME setHelpURL is ignored
522 // TODO push button
526 void KDEFileDialog::appendFilter( const QString &rTitle, const QString &rFilter )
528 // Filters are separated by ';'
529 QString qFilter( rFilter );
530 qFilter.replace( QChar( ';' ), QChar( ' ' ) ).replace( "*.*", "*" );
532 // Workaround for too wide <All formats> (*.bmp;...) entry
533 QString qTitle( rTitle );
534 qTitle.replace( QRegExp( "^<([^>]*)> \\(.*" ), "<\\1>" );
536 m_aFilters.push_back( qMakePair( qTitle, qFilter ) );
539 QString KDEFileDialog::filters() const
541 QString qString, qTmp;
542 bool bFirstFilter = true;
544 for ( FilterList::const_iterator it = m_aFilters.begin(); it != m_aFilters.end(); ++it )
546 if ( bFirstFilter )
547 bFirstFilter = false;
548 else
549 qString.append( '\n' );
551 qString.append( (*it).second );
552 qString.append( '|' );
554 qTmp = (*it).first;
555 qString.append( qTmp.replace( '/', "\\/" ) );
558 return qString;
561 QString KDEFileDialog::addExtension( const QString &rFileName ) const
563 if ( !isSave() )
564 return rFileName;
566 QString qExtension;
568 QWidget *pExtensionWidget = findControl( "100" ); // CHECKBOX_AUTOEXTENSION
569 QCheckBox *pExtensionCB = pExtensionWidget? static_cast< QCheckBox* >( pExtensionWidget->qt_cast( QCheckBox_String ) ): NULL;
570 if ( pExtensionCB && pExtensionCB->isChecked() )
572 // FIXME: qFilter can be a MIME; we ignore it now...
573 QStringList qFilterList = QStringList::split( " ", currentFilter() );
574 for ( QStringList::const_iterator it = qFilterList.begin();
575 qExtension.isEmpty() && it != qFilterList.end();
576 ++it )
578 int nUnwanted = (*it).findRev( '*' );
579 if ( nUnwanted < 0 )
580 nUnwanted = (*it).findRev( '?' );
581 else
582 nUnwanted = ::std::max( nUnwanted, (*it).find( '?', nUnwanted ) );
584 int nIdx = (*it).find( '.', ::std::max( nUnwanted, 0 ) );
585 if ( nIdx >= 0 )
586 qExtension = (*it).mid( nIdx ).lower();
590 if ( qExtension.isEmpty() || qExtension == "." || rFileName.endsWith( qExtension ) )
591 return rFileName;
592 else
593 return rFileName + qExtension;
596 bool KDEFileDialog::isSupportedProtocol( const QString &rProtocol ) const
598 // TODO Get this information directly from OOo
599 const char * pOOoProtocols[] = { "", "smb", "ftp", "http", "file", "mailto",
600 "vnd.sun.star.webdav", "news", "private", "vnd.sun.star.help",
601 "https", "slot", "macro", "javascript", "imap", "pop3", "data",
602 "cid", "out", "vnd.sun.star.wfs", "vnd.sun.star.hier", "vim",
603 ".uno", ".component", "vnd.sun.star.pkg", "ldap", "db",
604 "vnd.sun.star.cmd", "vnd.sun.star.script",
605 "telnet",
606 NULL };
608 for ( const char **pIndex = pOOoProtocols; *pIndex != NULL; ++pIndex )
610 if ( rProtocol == *pIndex )
611 return true;
614 // TODO gnome-vfs bits here
616 return false;
619 KURL KDEFileDialog::mostLocalURL( const KURL &rURL ) const
621 #if KDE_IS_VERSION(3,5,0)
622 KURL qMostLocalURL( KIO::NetAccess::mostLocalURL( rURL, const_cast<KDEFileDialog*>( this ) ) );
623 if ( qMostLocalURL.isLocalFile() )
624 return qMostLocalURL;
625 else
627 // Terrible hack to get even non-existing media:// files right
628 qMostLocalURL.cd( ".." );
629 KURL qMostLocalPath( KIO::NetAccess::mostLocalURL( qMostLocalURL, const_cast<KDEFileDialog*>( this ) ) );
630 if ( qMostLocalPath.isLocalFile() )
632 qMostLocalPath.addPath( rURL.fileName() );
633 return qMostLocalPath;
636 #endif
638 return rURL;
641 QString KDEFileDialog::localCopy( const QString &rFileName ) const
643 // 106 == MIB enum for UTF-8
644 KURL qLocalURL = mostLocalURL( KURL( rFileName, 106 ) );
645 if ( qLocalURL.isLocalFile() )
646 return qLocalURL.url();
648 int nExtensionPos = rFileName.findRev( '/' );
649 if ( nExtensionPos >= 0 )
650 nExtensionPos = rFileName.find( '.', nExtensionPos );
651 else
652 nExtensionPos = rFileName.find( '.' );
654 KTempFile qTempFile( QString::null, ( nExtensionPos < 0 )? QString(): rFileName.mid( nExtensionPos ) );
655 KURL qDestURL;
656 qDestURL.setPath( qTempFile.name() );
658 if ( !KIO::NetAccess::file_copy( rFileName, qDestURL, 0600, true, false, NULL ) )
660 KMessageBox::error( 0, KIO::NetAccess::lastErrorString() );
661 return QString::null;
664 return qDestURL.url();
667 #if ENABLE_TDE
668 void KDEFileDialog::fileHighlightedCommand( const TQString & )
669 #else // ENABLE_TDE
670 void KDEFileDialog::fileHighlightedCommand( const QString & )
671 #endif // ENABLE_TDE
673 if ( canNotifySelection() )
675 sendCommand( "fileSelectionChanged" );
676 setCanNotifySelection( false );
680 void KDEFileDialog::selectionChangedCommand()
682 if ( canNotifySelection() )
684 sendCommand( "fileSelectionChanged" );
685 setCanNotifySelection( false );
689 void KDEFileDialog::sendCommand( const QString &rCommand )
691 #if OSL_DEBUG_LEVEL > 1
692 ::std::cerr << "kdefilepicker sent: " << rCommand.latin1() << ::std::endl;
693 #endif
695 //m_aOutputStream << rCommand << endl;
696 ::std::cout << rCommand.utf8() << ::std::endl;
699 void KDEFileDialog::appendURL( QString &rBuffer, const KURL &rURL )
701 // From Martin Kretzschmar:
702 // file:///path/to/test%E0.odt is not a valid URL from OOo's point of
703 // view. (?Most modern parts of?) OOo assume(s) that the URL contains only
704 // ASCII characters (which test%E0.odt does) and is UTF-8 after unescaping
705 // (which file:///path/test%E0.odt is not).
706 // Cf. the comment in sal/inc/osl/file.h.
707 // 106 == MIB enum for UTF-8
708 QString qUrlStr = addExtension( rURL.url( 0, 106 ) );
710 if ( !isExecuting() && !isSupportedProtocol( rURL.protocol() ) )
711 qUrlStr = localCopy( qUrlStr );
713 if ( qUrlStr.startsWith( "file:/" ) && qUrlStr.mid( 6, 1 ) != "/" )
714 qUrlStr.replace( "file:/", "file:///" );
716 rBuffer.append( " " );
717 if ( !qUrlStr.isEmpty() )
718 appendEscaped( rBuffer, qUrlStr );
721 void KDEFileDialog::appendEscaped( QString &rBuffer, const QString &rString )
723 const QChar *pUnicode = rString.unicode();
724 const QChar *pEnd = pUnicode + rString.length();
726 rBuffer.append( '"' );
727 for ( ; pUnicode != pEnd; ++pUnicode )
729 if ( *pUnicode == '\\' )
730 rBuffer.append( "\\\\" );
731 else if ( *pUnicode == '"' )
732 rBuffer.append( "\\\"" );
733 else if ( *pUnicode == '\n' )
734 rBuffer.append( "\\\n" );
735 else
736 rBuffer.append( *pUnicode );
738 rBuffer.append( '"' );
741 QString KDEFileDialog::escapeString( const QString &rString )
743 QString qString;
744 qString.reserve( 2*rString.length() + 2 ); // every char escaped + quotes
746 appendEscaped( qString, rString );
748 return qString;
751 void KDEFileFilterComboHack::setCurrentFilter( const QString& filter )
753 setCurrentText( filter );
754 filterChanged();
756 // Workaround for 'Filter name (*.blah)' vs. 'Filter name'
757 if ( currentText() != text( currentItem() ) )
759 int nItem = 0;
760 for ( ; nItem < count() && !text( nItem ).startsWith( filter ); ++nItem );
762 if ( nItem < count() )
763 setCurrentItem( nItem );
764 else
765 setCurrentItem( 0 );
767 filterChanged();
771 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */