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 .
22 #include <tools/solar.h>
23 #include <tools/color.hxx>
24 #include "address.hxx"
25 #include <formula/grammar.hxx>
27 #include "rangelst.hxx"
28 #include "tokenarray.hxx"
30 #include <svl/listener.hxx>
32 #include <com/sun/star/sheet/ConditionOperator.hpp>
34 #include <rtl/math.hxx>
35 #include <tools/date.hxx>
36 #include <tools/link.hxx>
46 struct ScRefCellValue
;
50 struct RefUpdateContext
;
51 struct RefUpdateInsertTabContext
;
52 struct RefUpdateDeleteTabContext
;
53 struct RefUpdateMoveTabContext
;
58 #define SC_COND_NOBLANKS 1
59 #define SC_COND_CASESENS 2
61 enum class ScConditionMode
91 // For use in SAL_DEBUG etc. Output format not guaranteed to be stable.
92 template<typename charT
, typename traits
>
93 inline std::basic_ostream
<charT
, traits
> & operator <<(std::basic_ostream
<charT
, traits
> & stream
, const ScConditionMode
& rMode
)
97 case ScConditionMode::Equal
:
100 case ScConditionMode::Less
:
103 case ScConditionMode::Greater
:
106 case ScConditionMode::EqLess
:
109 case ScConditionMode::EqGreater
:
110 stream
<< "EQGREATER";
112 case ScConditionMode::NotEqual
:
113 stream
<< "NOTEQUAL";
115 case ScConditionMode::Between
:
118 case ScConditionMode::NotBetween
:
119 stream
<< "NOTBETWEEN";
121 case ScConditionMode::Duplicate
:
122 stream
<< "DUPLICATE";
124 case ScConditionMode::NotDuplicate
:
125 stream
<< "NOTDUPLICATE";
127 case ScConditionMode::Direct
:
130 case ScConditionMode::Top10
:
133 case ScConditionMode::Bottom10
:
134 stream
<< "BOTTOM10";
136 case ScConditionMode::TopPercent
:
137 stream
<< "TOPPERCENT";
139 case ScConditionMode::BottomPercent
:
140 stream
<< "BOTTOMPERCENT";
142 case ScConditionMode::AboveAverage
:
143 stream
<< "ABOVEAVERAGE";
145 case ScConditionMode::BelowAverage
:
146 stream
<< "BELOWAVERAGE";
148 case ScConditionMode::AboveEqualAverage
:
149 stream
<< "ABOVEEQUALAVERAGE";
151 case ScConditionMode::BelowEqualAverage
:
152 stream
<< "BELOWEQUALAVERAGE";
154 case ScConditionMode::Error
:
157 case ScConditionMode::NoError
:
160 case ScConditionMode::BeginsWith
:
161 stream
<< "BEGINSWITH";
163 case ScConditionMode::EndsWith
:
164 stream
<< "ENDSWITH";
166 case ScConditionMode::ContainsText
:
167 stream
<< "CONTAINSTEXT";
169 case ScConditionMode::NotContainsText
:
170 stream
<< "NOTCONTAINSTEXT";
172 case ScConditionMode::NONE
:
176 stream
<< "?(" << static_cast<int>(rMode
) << ")";
183 class ScFormulaListener final
: public SvtListener
186 mutable bool mbDirty
;
188 std::function
<void()> maCallbackFunction
;
190 void startListening(const ScTokenArray
* pTokens
, const ScRange
& rPos
);
191 void startListening(const ScRangeList
& rPos
);
194 explicit ScFormulaListener(ScFormulaCell
* pCell
);
195 explicit ScFormulaListener(ScDocument
& rDoc
);
196 explicit ScFormulaListener(ScDocument
& rDoc
, const ScRangeList
& rRange
);
197 virtual ~ScFormulaListener() override
;
199 void Notify( const SfxHint
& rHint
) override
;
201 bool NeedsRepaint() const;
203 void addTokenArray(const ScTokenArray
* pTokens
, const ScRange
& rRange
);
204 void stopListening();
205 void setCallback(const std::function
<void()>& aCallbackFunction
);
209 class ScConditionalFormat
;
210 struct ScDataBarInfo
;
211 struct ScIconSetInfo
;
213 struct SC_DLLPUBLIC ScCondFormatData
216 ScCondFormatData(ScCondFormatData
&&);
219 std::optional
<Color
> mxColorScale
;
220 std::unique_ptr
<ScDataBarInfo
> pDataBar
;
221 std::unique_ptr
<ScIconSetInfo
> pIconSet
;
225 class SC_DLLPUBLIC ScFormatEntry
228 ScFormatEntry(ScDocument
* pDoc
);
229 virtual ~ScFormatEntry() {}
241 virtual Type
GetType() const = 0;
242 virtual void UpdateReference( sc::RefUpdateContext
& rCxt
) = 0;
243 virtual void UpdateInsertTab( sc::RefUpdateInsertTabContext
& rCxt
) = 0;
244 virtual void UpdateDeleteTab( sc::RefUpdateDeleteTabContext
& rCxt
) = 0;
245 virtual void UpdateMoveTab( sc::RefUpdateMoveTabContext
& rCxt
) = 0;
247 virtual ScFormatEntry
* Clone( ScDocument
* pDoc
) const = 0;
249 virtual void SetParent( ScConditionalFormat
* pNew
) = 0;
251 bool operator==( const ScFormatEntry
& ) const;
252 virtual bool IsEqual( const ScFormatEntry
&, bool bIgnoreSrcPos
) const;
254 virtual void startRendering();
255 virtual void endRendering();
256 virtual void updateValues();
262 template<typename charT
, typename traits
>
263 inline std::basic_ostream
<charT
, traits
> & operator <<(std::basic_ostream
<charT
, traits
> & stream
, const ScFormatEntry::Type
& rType
)
267 case ScFormatEntry::Type::Condition
:
268 stream
<< "Condition";
270 case ScFormatEntry::Type::ExtCondition
:
271 stream
<< "ExtCondition";
273 case ScFormatEntry::Type::Colorscale
:
274 stream
<< "Colorscale";
276 case ScFormatEntry::Type::Databar
:
279 case ScFormatEntry::Type::Iconset
:
282 case ScFormatEntry::Type::Date
:
286 stream
<< "?(" << static_cast<int>(rType
) << ")";
295 bool operator() (double nVal1
, double nVal2
) const
297 if(nVal1
< nVal2
&& !rtl::math::approxEqual(nVal1
, nVal2
))
304 class SAL_DLLPUBLIC_RTTI ScConditionEntry
: public ScFormatEntry
309 double nVal1
; // input or calculated
311 OUString aStrVal1
; // input or calculated
313 const OUString aStrNmsp1
; // namespace to be used on (re)compilation, e.g. in XML import
314 const OUString aStrNmsp2
; // namespace to be used on (re)compilation, e.g. in XML import
315 const formula::FormulaGrammar::Grammar eTempGrammar1
; // grammar to be used on (re)compilation, e.g. in XML import
316 const formula::FormulaGrammar::Grammar eTempGrammar2
; // grammar to be used on (re)compilation, e.g. in XML import
317 bool bIsStr1
; // for recognition of empty strings
319 std::unique_ptr
<ScTokenArray
> pFormula1
; // entered formula
320 std::unique_ptr
<ScTokenArray
> pFormula2
;
321 ScAddress aSrcPos
; // source position for formulas
323 OUString aSrcString
; // formula source position as text during XML import
324 std::unique_ptr
<ScFormulaCell
> pFCell1
;
325 std::unique_ptr
<ScFormulaCell
> pFCell2
;
329 std::unique_ptr
<ScFormulaListener
> mpListener
;
330 Type eConditionType
; //It can be Condition or ExtCondition
332 static void SimplifyCompiledFormula( std::unique_ptr
<ScTokenArray
>& rFormula
,
337 void MakeCells( const ScAddress
& rPos
);
338 void Compile( const OUString
& rExpr1
, const OUString
& rExpr2
,
339 const OUString
& rExprNmsp1
, const OUString
& rExprNmsp2
,
340 formula::FormulaGrammar::Grammar eGrammar1
,
341 formula::FormulaGrammar::Grammar eGrammar2
,
343 void Interpret( const ScAddress
& rPos
);
345 bool IsValid( double nArg
, const ScAddress
& rPos
) const;
346 bool IsValidStr( const OUString
& rArg
, const ScAddress
& rPos
) const;
347 void StartListening();
350 ScConditionEntry( ScConditionMode eOper
,
351 const OUString
& rExpr1
, const OUString
& rExpr2
,
352 ScDocument
& rDocument
, const ScAddress
& rPos
,
353 const OUString
& rExprNmsp1
, const OUString
& rExprNmsp2
,
354 formula::FormulaGrammar::Grammar eGrammar1
,
355 formula::FormulaGrammar::Grammar eGrammar2
,
356 Type eType
= Type::Condition
);
357 ScConditionEntry( ScConditionMode eOper
,
358 const ScTokenArray
* pArr1
, const ScTokenArray
* pArr2
,
359 ScDocument
& rDocument
, const ScAddress
& rPos
);
360 ScConditionEntry( const ScConditionEntry
& r
); // flat copy of formulas
361 // true copy of formulas (for Ref-Undo):
362 ScConditionEntry( ScDocument
& rDocument
, const ScConditionEntry
& r
);
363 virtual ~ScConditionEntry() override
;
365 bool IsEqual( const ScFormatEntry
& r
, bool bIgnoreSrcPos
) const override
;
367 virtual void SetParent( ScConditionalFormat
* pNew
) override
;
369 bool IsCellValid( ScRefCellValue
& rCell
, const ScAddress
& rPos
) const;
371 ScConditionMode
GetOperation() const { return eOp
; }
372 void SetOperation(ScConditionMode eMode
);
374 bool IsIgnoreBlank() const { return ( nOptions
& SC_COND_NOBLANKS
) == 0; }
375 SC_DLLPUBLIC
void SetIgnoreBlank(bool bSet
);
377 bool IsCaseSensitive() const { return ( nOptions
& SC_COND_CASESENS
) != 0; }
378 SC_DLLPUBLIC
void SetCaseSensitive(bool bSet
);
380 const OUString
& GetSrcString() const { return aSrcString
; }
381 const ScAddress
& GetSrcPos() const { return aSrcPos
; }
383 SC_DLLPUBLIC ScAddress
GetValidSrcPos() const; // adjusted to allow textual representation of expressions
385 SC_DLLPUBLIC
void SetSrcString( const OUString
& rNew
); // for XML import
387 void SetFormula1( const ScTokenArray
& rArray
);
388 void SetFormula2( const ScTokenArray
& rArray
);
390 SC_DLLPUBLIC OUString
GetExpression( const ScAddress
& rCursor
, sal_uInt16 nPos
, sal_uInt32 nNumFmt
= 0,
391 const formula::FormulaGrammar::Grammar eGrammar
= formula::FormulaGrammar::GRAM_DEFAULT
) const;
393 /** Create a flat copy using ScTokenArray copy-ctor with
395 SC_DLLPUBLIC
std::unique_ptr
<ScTokenArray
> CreateFlatCopiedTokenArray( sal_uInt16 nPos
) const;
399 virtual void UpdateReference( sc::RefUpdateContext
& rCxt
) override
;
400 virtual void UpdateInsertTab( sc::RefUpdateInsertTabContext
& rCxt
) override
;
401 virtual void UpdateDeleteTab( sc::RefUpdateDeleteTabContext
& rCxt
) override
;
402 virtual void UpdateMoveTab( sc::RefUpdateMoveTabContext
& rCxt
) override
;
404 bool MarkUsedExternalReferences() const;
406 virtual Type
GetType() const override
{ return eConditionType
; }
408 virtual ScFormatEntry
* Clone(ScDocument
* pDoc
) const override
;
410 static ScConditionMode
GetModeFromApi(css::sheet::ConditionOperator nOperator
);
412 virtual void endRendering() override
;
413 virtual void startRendering() override
;
415 bool NeedsRepaint() const;
419 virtual void DataChanged() const;
420 ScDocument
* GetDocument() const { return mpDoc
; }
421 ScConditionalFormat
* pCondFormat
;
425 bool IsDuplicate(double nArg
, const OUString
& rStr
) const;
426 bool IsTopNElement( double nArg
) const;
427 bool IsTopNPercent( double nArg
) const;
428 bool IsBottomNElement( double nArg
) const;
429 bool IsBottomNPercent( double nArg
) const;
430 bool IsAboveAverage( double nArg
, bool bEqual
) const;
431 bool IsBelowAverage( double nArg
, bool bEqual
) const;
433 bool IsError( const ScAddress
& rPos
) const;
435 void FillCache() const;
437 struct ScConditionEntryCache
439 typedef std::map
<OUString
, sal_Int32
> StringCacheType
;
440 StringCacheType maStrings
;
441 typedef std::map
<double, sal_Int32
, approx_less
> ValueCacheType
;
442 ValueCacheType maValues
;
444 // cache them for easier access
447 ScConditionEntryCache():
451 mutable std::unique_ptr
<ScConditionEntryCache
> mpCache
;
453 std::unique_ptr
<RepaintInIdle
> mpRepaintTask
;
456 // single condition entry for conditional formatting
457 class SAL_DLLPUBLIC_RTTI ScCondFormatEntry final
: public ScConditionEntry
460 Type eCondFormatType
= Type::Condition
;
463 SC_DLLPUBLIC
ScCondFormatEntry( ScConditionMode eOper
,
464 const OUString
& rExpr1
, const OUString
& rExpr2
,
465 ScDocument
& rDocument
, const ScAddress
& rPos
,
467 const OUString
& rExprNmsp1
= OUString(),
468 const OUString
& rExprNmsp2
= OUString(),
469 formula::FormulaGrammar::Grammar eGrammar1
= formula::FormulaGrammar::GRAM_DEFAULT
,
470 formula::FormulaGrammar::Grammar eGrammar2
= formula::FormulaGrammar::GRAM_DEFAULT
,
471 Type eType
= Type::Condition
);
472 SC_DLLPUBLIC
ScCondFormatEntry( ScConditionMode eOper
,
473 const ScTokenArray
* pArr1
, const ScTokenArray
* pArr2
,
474 ScDocument
& rDocument
, const ScAddress
& rPos
,
476 SC_DLLPUBLIC
ScCondFormatEntry( const ScCondFormatEntry
& r
);
477 ScCondFormatEntry( ScDocument
& rDocument
, const ScCondFormatEntry
& r
);
478 SC_DLLPUBLIC
virtual ~ScCondFormatEntry() override
;
480 bool IsEqual( const ScFormatEntry
& r
, bool bIgnoreSrcPos
) const override
;
482 const OUString
& GetStyle() const { return aStyleName
; }
483 void UpdateStyleName(const OUString
& rNew
) { aStyleName
=rNew
; }
484 virtual ScFormatEntry
* Clone(ScDocument
* pDoc
) const override
;
485 virtual Type
GetType() const override
{ return eCondFormatType
; }
488 virtual void DataChanged() const override
;
491 namespace condformat
{
493 enum ScCondFormatDateType
512 class SC_DLLPUBLIC ScCondDateFormatEntry final
: public ScFormatEntry
515 ScCondDateFormatEntry(ScDocument
* pDoc
);
516 ScCondDateFormatEntry(ScDocument
* pDoc
, const ScCondDateFormatEntry
& rEntry
);
518 bool IsValid( const ScAddress
& rPos
) const;
520 void SetDateType(condformat::ScCondFormatDateType eType
);
521 condformat::ScCondFormatDateType
GetDateType() const { return meType
;}
523 const OUString
& GetStyleName() const { return maStyleName
;}
524 void SetStyleName( const OUString
& rStyleName
);
526 virtual Type
GetType() const override
{ return Type::Date
; }
527 virtual void UpdateReference( sc::RefUpdateContext
& ) override
{}
528 virtual void UpdateInsertTab( sc::RefUpdateInsertTabContext
& ) override
{}
529 virtual void UpdateDeleteTab( sc::RefUpdateDeleteTabContext
& ) override
{}
530 virtual void UpdateMoveTab( sc::RefUpdateMoveTabContext
& ) override
{}
532 virtual ScFormatEntry
* Clone( ScDocument
* pDoc
) const override
;
534 virtual void SetParent( ScConditionalFormat
* ) override
{}
536 virtual void startRendering() override
;
537 virtual void endRendering() override
;
540 condformat::ScCondFormatDateType meType
;
542 mutable std::unique_ptr
<Date
> mpCache
;
544 OUString maStyleName
;
547 class ScColorFormatCache final
: public SvtListener
553 explicit ScColorFormatCache(ScDocument
& rDoc
, const ScRangeList
& rRanges
);
554 virtual ~ScColorFormatCache() override
;
556 void Notify( const SfxHint
& rHint
) override
;
558 std::vector
<double> maValues
;
561 // complete conditional formatting
562 class ScConditionalFormat
565 sal_uInt32 nKey
; // Index in attributes
567 std::vector
<std::unique_ptr
<ScFormatEntry
>> maEntries
;
568 ScRangeList maRanges
; // Ranges for conditional format
570 mutable std::unique_ptr
<ScColorFormatCache
> mpCache
;
573 SC_DLLPUBLIC
ScConditionalFormat(sal_uInt32 nNewKey
, ScDocument
* pDocument
);
574 SC_DLLPUBLIC
~ScConditionalFormat();
575 ScConditionalFormat(const ScConditionalFormat
&) = delete;
576 const ScConditionalFormat
& operator=(const ScConditionalFormat
&) = delete;
578 // true copy of formulas (for Ref-Undo / between documents)
579 SC_DLLPUBLIC
std::unique_ptr
<ScConditionalFormat
> Clone(ScDocument
* pNewDoc
= nullptr) const;
581 SC_DLLPUBLIC
void AddEntry( ScFormatEntry
* pNew
);
582 void RemoveEntry(size_t nIndex
);
583 SC_DLLPUBLIC
void SetRange( const ScRangeList
& rRanges
);
584 const ScRangeList
& GetRange() const { return maRanges
; }
585 // don't use the same name as for the const version
586 ScRangeList
& GetRangeList() { return maRanges
; }
588 bool IsEmpty() const;
589 SC_DLLPUBLIC
size_t size() const;
591 ScDocument
* GetDocument();
595 void UpdateReference( sc::RefUpdateContext
& rCxt
, bool bCopyAsMove
= false );
596 void UpdateInsertTab( sc::RefUpdateInsertTabContext
& rCxt
);
597 void UpdateDeleteTab( sc::RefUpdateDeleteTabContext
& rCxt
);
598 void UpdateMoveTab( sc::RefUpdateMoveTabContext
& rCxt
);
600 void InsertRow(SCTAB nTab
, SCCOL nColStart
, SCCOL nColEnd
, SCROW nRowStart
, SCSIZE nSize
);
601 void InsertCol(SCTAB nTab
, SCROW nRowStart
, SCROW nRowEnd
, SCCOL nColStart
, SCSIZE nSize
);
603 void DeleteArea( SCCOL nCol1
, SCROW nRow1
, SCCOL nCol2
, SCROW nRow2
);
604 void RenameCellStyle( std::u16string_view rOld
, const OUString
& rNew
);
606 SC_DLLPUBLIC
const ScFormatEntry
* GetEntry( sal_uInt16 nPos
) const;
608 SC_DLLPUBLIC OUString
GetCellStyle( ScRefCellValue
& rCell
, const ScAddress
& rPos
) const;
610 SC_DLLPUBLIC ScCondFormatData
GetData( ScRefCellValue
& rCell
, const ScAddress
& rPos
) const;
612 bool EqualEntries( const ScConditionalFormat
& r
, bool bIgnoreSrcPos
= false ) const;
616 sal_uInt32
GetKey() const { return nKey
; }
617 void SetKey(sal_uInt32 nNew
) { nKey
= nNew
; } // only if not inserted!
619 bool MarkUsedExternalReferences() const;
621 // sorted (via std::set) by Index
622 bool operator < ( const ScConditionalFormat
& r
) const { return nKey
< r
.nKey
; }
624 void startRendering();
629 // Forced recalculation for formulas
632 void ResetCache() const;
633 void SetCache(const std::vector
<double>& aValues
) const;
634 std::vector
<double>* GetCache() const;
637 class RepaintInIdle final
: public Idle
639 ScConditionalFormat
* mpCondFormat
;
642 RepaintInIdle(ScConditionalFormat
* pCondFormat
)
643 : Idle("Conditional Format Repaint Idle")
644 , mpCondFormat(pCondFormat
)
647 void Invoke() override
650 mpCondFormat
->DoRepaint();
654 struct CompareScConditionalFormat
656 using is_transparent
= void;
657 bool operator()(std::unique_ptr
<ScConditionalFormat
> const& lhs
,
658 std::unique_ptr
<ScConditionalFormat
> const& rhs
) const
660 return (*lhs
) < (*rhs
);
662 bool operator()(sal_uInt32 nKey
, std::unique_ptr
<ScConditionalFormat
> const& rpFormat
) const
664 return nKey
< rpFormat
->GetKey();
666 bool operator()(std::unique_ptr
<ScConditionalFormat
> const& rpFormat
, sal_uInt32 nKey
) const
668 return rpFormat
->GetKey() < nKey
;
672 // List of all conditional formats in a sheet
673 class ScConditionalFormatList
676 typedef std::set
<std::unique_ptr
<ScConditionalFormat
>,
677 CompareScConditionalFormat
> ConditionalFormatContainer
;
678 ConditionalFormatContainer m_ConditionalFormats
;
680 void operator =(ScConditionalFormatList
const &) = delete;
683 ScConditionalFormatList() {}
684 SC_DLLPUBLIC
ScConditionalFormatList(const ScConditionalFormatList
& rList
);
685 ScConditionalFormatList(ScDocument
& rDoc
, const ScConditionalFormatList
& rList
);
687 void InsertNew( std::unique_ptr
<ScConditionalFormat
> pNew
);
690 * Checks that all cond formats have a non empty range.
691 * Deletes empty cond formats. Optionally call rLink
692 * on the empty format before deleting it.
693 * @return true if all cond formats were valid
695 bool CheckAllEntries(const Link
<ScConditionalFormat
*,void>& rLink
= Link
<ScConditionalFormat
*,void>());
697 SC_DLLPUBLIC ScConditionalFormat
* GetFormat( sal_uInt32 nKey
);
698 const ScConditionalFormat
* GetFormat( sal_uInt32 nKey
) const;
702 void UpdateReference( sc::RefUpdateContext
& rCxt
);
703 void UpdateInsertTab( sc::RefUpdateInsertTabContext
& rCxt
);
704 void UpdateDeleteTab( sc::RefUpdateDeleteTabContext
& rCxt
);
705 void UpdateMoveTab( sc::RefUpdateMoveTabContext
& rCxt
);
707 void InsertRow(SCTAB nTab
, SCCOL nColStart
, SCCOL nColEnd
, SCROW nRowStart
, SCSIZE nSize
);
708 void InsertCol(SCTAB nTab
, SCROW nRowStart
, SCROW nRowEnd
, SCCOL nColStart
, SCSIZE nSize
);
710 void RenameCellStyle( std::u16string_view rOld
, const OUString
& rNew
);
711 void DeleteArea( SCCOL nCol1
, SCROW nRow1
, SCCOL nCol2
, SCROW nRow2
);
713 typedef ConditionalFormatContainer::iterator iterator
;
714 typedef ConditionalFormatContainer::const_iterator const_iterator
;
716 ScRangeList
GetCombinedRange() const;
718 void RemoveFromDocument(ScDocument
& rDoc
) const;
719 void AddToDocument(ScDocument
& rDoc
) const;
721 SC_DLLPUBLIC iterator
begin();
722 SC_DLLPUBLIC const_iterator
begin() const;
723 SC_DLLPUBLIC iterator
end();
724 SC_DLLPUBLIC const_iterator
end() const;
726 SC_DLLPUBLIC
size_t size() const;
727 SC_DLLPUBLIC
bool empty() const;
729 SC_DLLPUBLIC
void erase(sal_uLong nIndex
);
732 void startRendering();
737 sal_uInt32
getMaxKey() const;
739 /// Forced recalculation of formulas
743 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */