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 "atkwrapper.hxx"
21 #include "atktextattributes.hxx"
24 #include <osl/diagnose.h>
25 #include <rtl/character.hxx>
27 #include <com/sun/star/accessibility/AccessibleScrollType.hpp>
28 #include <com/sun/star/accessibility/AccessibleTextType.hpp>
29 #include <com/sun/star/accessibility/TextSegment.hpp>
30 #include <com/sun/star/accessibility/XAccessibleMultiLineText.hpp>
31 #include <com/sun/star/accessibility/XAccessibleText.hpp>
32 #include <com/sun/star/accessibility/XAccessibleTextAttributes.hpp>
33 #include <com/sun/star/accessibility/XAccessibleTextMarkup.hpp>
34 #include <com/sun/star/lang/NoSupportException.hpp>
35 #include <com/sun/star/text/TextMarkupType.hpp>
37 using namespace ::com::sun::star
;
40 text_type_from_boundary(AtkTextBoundary boundary_type
)
44 case ATK_TEXT_BOUNDARY_CHAR
:
45 return accessibility::AccessibleTextType::CHARACTER
;
46 case ATK_TEXT_BOUNDARY_WORD_START
:
47 case ATK_TEXT_BOUNDARY_WORD_END
:
48 return accessibility::AccessibleTextType::WORD
;
49 case ATK_TEXT_BOUNDARY_SENTENCE_START
:
50 case ATK_TEXT_BOUNDARY_SENTENCE_END
:
51 return accessibility::AccessibleTextType::SENTENCE
;
52 case ATK_TEXT_BOUNDARY_LINE_START
:
53 case ATK_TEXT_BOUNDARY_LINE_END
:
54 return accessibility::AccessibleTextType::LINE
;
60 /*****************************************************************************/
62 #if ATK_CHECK_VERSION(2,32,0)
63 static accessibility::AccessibleScrollType
64 scroll_type_from_scroll_type(AtkScrollType type
)
68 case ATK_SCROLL_TOP_LEFT
:
69 return accessibility::AccessibleScrollType_SCROLL_TOP_LEFT
;
70 case ATK_SCROLL_BOTTOM_RIGHT
:
71 return accessibility::AccessibleScrollType_SCROLL_BOTTOM_RIGHT
;
72 case ATK_SCROLL_TOP_EDGE
:
73 return accessibility::AccessibleScrollType_SCROLL_TOP_EDGE
;
74 case ATK_SCROLL_BOTTOM_EDGE
:
75 return accessibility::AccessibleScrollType_SCROLL_BOTTOM_EDGE
;
76 case ATK_SCROLL_LEFT_EDGE
:
77 return accessibility::AccessibleScrollType_SCROLL_LEFT_EDGE
;
78 case ATK_SCROLL_RIGHT_EDGE
:
79 return accessibility::AccessibleScrollType_SCROLL_RIGHT_EDGE
;
80 case ATK_SCROLL_ANYWHERE
:
81 return accessibility::AccessibleScrollType_SCROLL_ANYWHERE
;
83 throw lang::NoSupportException();
88 /*****************************************************************************/
91 adjust_boundaries( css::uno::Reference
<css::accessibility::XAccessibleText
> const & pText
,
92 accessibility::TextSegment
const & rTextSegment
,
93 AtkTextBoundary boundary_type
,
94 gint
* start_offset
, gint
* end_offset
)
96 accessibility::TextSegment aTextSegment
;
98 gint start
= 0, end
= 0;
100 if( !rTextSegment
.SegmentText
.isEmpty() )
102 switch(boundary_type
)
104 case ATK_TEXT_BOUNDARY_CHAR
:
105 if ((rTextSegment
.SegmentEnd
- rTextSegment
.SegmentStart
) == 1
106 && rtl::isSurrogate(rTextSegment
.SegmentText
[0]))
109 case ATK_TEXT_BOUNDARY_LINE_START
:
110 case ATK_TEXT_BOUNDARY_LINE_END
:
111 case ATK_TEXT_BOUNDARY_SENTENCE_START
:
112 start
= rTextSegment
.SegmentStart
;
113 end
= rTextSegment
.SegmentEnd
;
114 aString
= rTextSegment
.SegmentText
;
117 // the OOo break iterator behaves as SENTENCE_START
118 case ATK_TEXT_BOUNDARY_SENTENCE_END
:
119 start
= rTextSegment
.SegmentStart
;
120 end
= rTextSegment
.SegmentEnd
;
124 if( end
> 0 && end
< pText
->getCharacterCount() - 1 )
127 aString
= pText
->getTextRange(start
, end
);
130 case ATK_TEXT_BOUNDARY_WORD_START
:
131 start
= rTextSegment
.SegmentStart
;
133 // Determine the start index of the next segment
134 aTextSegment
= pText
->getTextBehindIndex(rTextSegment
.SegmentEnd
,
135 text_type_from_boundary(boundary_type
));
136 if( !aTextSegment
.SegmentText
.isEmpty() )
137 end
= aTextSegment
.SegmentStart
;
139 end
= pText
->getCharacterCount();
141 aString
= pText
->getTextRange(start
, end
);
144 case ATK_TEXT_BOUNDARY_WORD_END
:
145 end
= rTextSegment
.SegmentEnd
;
147 // Determine the end index of the previous segment
148 aTextSegment
= pText
->getTextBeforeIndex(rTextSegment
.SegmentStart
,
149 text_type_from_boundary(boundary_type
));
150 if( !aTextSegment
.SegmentText
.isEmpty() )
151 start
= aTextSegment
.SegmentEnd
;
155 aString
= pText
->getTextRange(start
, end
);
163 *start_offset
= start
;
166 return OUStringToGChar(aString
);
169 /*****************************************************************************/
171 /// @throws uno::RuntimeException
172 static css::uno::Reference
<css::accessibility::XAccessibleText
>
173 getText( AtkText
*pText
)
175 AtkObjectWrapper
*pWrap
= ATK_OBJECT_WRAPPER( pText
);
178 if( !pWrap
->mpText
.is() )
180 pWrap
->mpText
.set(pWrap
->mpContext
, css::uno::UNO_QUERY
);
183 return pWrap
->mpText
;
186 return css::uno::Reference
<css::accessibility::XAccessibleText
>();
189 /*****************************************************************************/
191 /// @throws uno::RuntimeException
192 static css::uno::Reference
<css::accessibility::XAccessibleTextMarkup
>
193 getTextMarkup( AtkText
*pText
)
195 AtkObjectWrapper
*pWrap
= ATK_OBJECT_WRAPPER( pText
);
198 if( !pWrap
->mpTextMarkup
.is() )
200 pWrap
->mpTextMarkup
.set(pWrap
->mpContext
, css::uno::UNO_QUERY
);
203 return pWrap
->mpTextMarkup
;
206 return css::uno::Reference
<css::accessibility::XAccessibleTextMarkup
>();
209 /*****************************************************************************/
211 /// @throws uno::RuntimeException
212 static css::uno::Reference
<css::accessibility::XAccessibleTextAttributes
>
213 getTextAttributes( AtkText
*pText
)
215 AtkObjectWrapper
*pWrap
= ATK_OBJECT_WRAPPER( pText
);
218 if( !pWrap
->mpTextAttributes
.is() )
220 pWrap
->mpTextAttributes
.set(pWrap
->mpContext
, css::uno::UNO_QUERY
);
223 return pWrap
->mpTextAttributes
;
226 return css::uno::Reference
<css::accessibility::XAccessibleTextAttributes
>();
229 /*****************************************************************************/
231 /// @throws uno::RuntimeException
232 static css::uno::Reference
<css::accessibility::XAccessibleMultiLineText
>
233 getMultiLineText( AtkText
*pText
)
235 AtkObjectWrapper
*pWrap
= ATK_OBJECT_WRAPPER( pText
);
238 if( !pWrap
->mpMultiLineText
.is() )
240 pWrap
->mpMultiLineText
.set(pWrap
->mpContext
, css::uno::UNO_QUERY
);
243 return pWrap
->mpMultiLineText
;
246 return css::uno::Reference
<css::accessibility::XAccessibleMultiLineText
>();
249 /*****************************************************************************/
254 text_wrapper_get_text (AtkText
*text
,
258 gchar
* ret
= nullptr;
260 g_return_val_if_fail( (end_offset
== -1) || (end_offset
>= start_offset
), nullptr );
263 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
268 sal_Int32 n
= pText
->getCharacterCount();
270 if( start_offset
< n
)
272 if( -1 == end_offset
)
273 aText
= pText
->getTextRange(start_offset
, n
- start_offset
);
275 aText
= pText
->getTextRange(start_offset
, end_offset
);
278 ret
= g_strdup( OUStringToOString(aText
, RTL_TEXTENCODING_UTF8
).getStr() );
281 catch(const uno::Exception
&) {
282 g_warning( "Exception in getText()" );
289 text_wrapper_get_text_after_offset (AtkText
*text
,
291 AtkTextBoundary boundary_type
,
296 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
300 accessibility::TextSegment aTextSegment
= pText
->getTextBehindIndex(offset
, text_type_from_boundary(boundary_type
));
301 return adjust_boundaries(pText
, aTextSegment
, boundary_type
, start_offset
, end_offset
);
304 catch(const uno::Exception
&) {
305 g_warning( "Exception in get_text_after_offset()" );
312 text_wrapper_get_text_at_offset (AtkText
*text
,
314 AtkTextBoundary boundary_type
,
319 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
323 /* If the user presses the 'End' key, the caret will be placed behind the last character,
324 * which is the same index as the first character of the next line. In atk the magic offset
325 * '-2' is used to cover this special case.
329 (ATK_TEXT_BOUNDARY_LINE_START
== boundary_type
||
330 ATK_TEXT_BOUNDARY_LINE_END
== boundary_type
)
334 css::accessibility::XAccessibleMultiLineText
> pMultiLineText
335 = getMultiLineText( text
);
336 if( pMultiLineText
.is() )
338 accessibility::TextSegment aTextSegment
= pMultiLineText
->getTextAtLineWithCaret();
339 return adjust_boundaries(pText
, aTextSegment
, boundary_type
, start_offset
, end_offset
);
343 accessibility::TextSegment aTextSegment
= pText
->getTextAtIndex(offset
, text_type_from_boundary(boundary_type
));
344 return adjust_boundaries(pText
, aTextSegment
, boundary_type
, start_offset
, end_offset
);
347 catch(const uno::Exception
&) {
348 g_warning( "Exception in get_text_at_offset()" );
355 text_wrapper_get_character_at_offset (AtkText
*text
,
359 gunichar uc
= 0xFFFFFFFF;
361 gchar
* char_as_string
=
362 text_wrapper_get_text_at_offset(text
, offset
, ATK_TEXT_BOUNDARY_CHAR
,
366 uc
= g_utf8_get_char( char_as_string
);
367 g_free( char_as_string
);
374 text_wrapper_get_text_before_offset (AtkText
*text
,
376 AtkTextBoundary boundary_type
,
381 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
385 accessibility::TextSegment aTextSegment
= pText
->getTextBeforeIndex(offset
, text_type_from_boundary(boundary_type
));
386 return adjust_boundaries(pText
, aTextSegment
, boundary_type
, start_offset
, end_offset
);
389 catch(const uno::Exception
&) {
390 g_warning( "Exception in text_before_offset()" );
397 text_wrapper_get_caret_offset (AtkText
*text
)
402 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
405 offset
= pText
->getCaretPosition();
407 catch(const uno::Exception
&) {
408 g_warning( "Exception in getCaretPosition()" );
415 text_wrapper_set_caret_offset (AtkText
*text
,
419 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
422 return pText
->setCaretPosition( offset
);
424 catch(const uno::Exception
&) {
425 g_warning( "Exception in setCaretPosition()" );
432 static AtkAttributeSet
*
433 handle_text_markup_as_run_attribute( css::uno::Reference
<css::accessibility::XAccessibleTextMarkup
> const & pTextMarkup
,
434 const gint nTextMarkupType
,
436 AtkAttributeSet
* pSet
,
440 const gint
nTextMarkupCount( pTextMarkup
->getTextMarkupCount( nTextMarkupType
) );
441 if ( nTextMarkupCount
> 0 )
443 for ( gint nTextMarkupIndex
= 0;
444 nTextMarkupIndex
< nTextMarkupCount
;
447 accessibility::TextSegment aTextSegment
=
448 pTextMarkup
->getTextMarkup( nTextMarkupIndex
, nTextMarkupType
);
449 const gint nStartOffsetTextMarkup
= aTextSegment
.SegmentStart
;
450 const gint nEndOffsetTextMarkup
= aTextSegment
.SegmentEnd
;
451 if ( nStartOffsetTextMarkup
<= offset
)
453 if ( offset
< nEndOffsetTextMarkup
)
455 // text markup at <offset>
456 *start_offset
= ::std::max( *start_offset
,
457 nStartOffsetTextMarkup
);
458 *end_offset
= ::std::min( *end_offset
,
459 nEndOffsetTextMarkup
);
460 switch ( nTextMarkupType
)
462 case css::text::TextMarkupType::SPELLCHECK
:
464 pSet
= attribute_set_prepend_misspelled( pSet
);
467 case css::text::TextMarkupType::TRACK_CHANGE_INSERTION
:
469 pSet
= attribute_set_prepend_tracked_change_insertion( pSet
);
472 case css::text::TextMarkupType::TRACK_CHANGE_DELETION
:
474 pSet
= attribute_set_prepend_tracked_change_deletion( pSet
);
477 case css::text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE
:
479 pSet
= attribute_set_prepend_tracked_change_formatchange( pSet
);
487 break; // no further iteration needed.
491 *start_offset
= ::std::max( *start_offset
,
492 nEndOffsetTextMarkup
);
493 // continue iteration.
498 *end_offset
= ::std::min( *end_offset
,
499 nStartOffsetTextMarkup
);
500 break; // no further iteration.
502 } // eof iteration over text markups
508 static AtkAttributeSet
*
509 text_wrapper_get_run_attributes( AtkText
*text
,
514 AtkAttributeSet
*pSet
= nullptr;
517 bool bOffsetsAreValid
= false;
519 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
523 uno::Sequence
< beans::PropertyValue
> aAttributeList
;
525 css::uno::Reference
<css::accessibility::XAccessibleTextAttributes
>
526 pTextAttributes
= getTextAttributes( text
);
527 if(pTextAttributes
.is()) // Text attributes are available for paragraphs only
529 aAttributeList
= pTextAttributes
->getRunAttributes( offset
, uno::Sequence
< OUString
> () );
531 else // For other text objects use character attributes
533 aAttributeList
= pText
->getCharacterAttributes( offset
, uno::Sequence
< OUString
> () );
536 pSet
= attribute_set_new_from_property_values( aAttributeList
, true, text
);
538 // - always provide start_offset and end_offset
540 accessibility::TextSegment aTextSegment
=
541 pText
->getTextAtIndex(offset
, accessibility::AccessibleTextType::ATTRIBUTE_RUN
);
543 *start_offset
= aTextSegment
.SegmentStart
;
545 // Do _not_ increment the end_offset provide by <accessibility::TextSegment> instance
546 *end_offset
= aTextSegment
.SegmentEnd
;
547 bOffsetsAreValid
= true;
551 // Special handling for misspelled text
553 // - add special handling for tracked changes and refactor the
554 // corresponding code for handling misspelled text.
555 css::uno::Reference
<css::accessibility::XAccessibleTextMarkup
>
556 pTextMarkup
= getTextMarkup( text
);
557 if( pTextMarkup
.is() )
559 // Get attribute run here if it hasn't been done before
560 if (!bOffsetsAreValid
&& pText
.is())
562 accessibility::TextSegment aAttributeTextSegment
=
563 pText
->getTextAtIndex(offset
, accessibility::AccessibleTextType::ATTRIBUTE_RUN
);
564 *start_offset
= aAttributeTextSegment
.SegmentStart
;
565 *end_offset
= aAttributeTextSegment
.SegmentEnd
;
567 // handle misspelled text
568 pSet
= handle_text_markup_as_run_attribute(
570 css::text::TextMarkupType::SPELLCHECK
,
571 offset
, pSet
, start_offset
, end_offset
);
572 // handle tracked changes
573 pSet
= handle_text_markup_as_run_attribute(
575 css::text::TextMarkupType::TRACK_CHANGE_INSERTION
,
576 offset
, pSet
, start_offset
, end_offset
);
577 pSet
= handle_text_markup_as_run_attribute(
579 css::text::TextMarkupType::TRACK_CHANGE_DELETION
,
580 offset
, pSet
, start_offset
, end_offset
);
581 pSet
= handle_text_markup_as_run_attribute(
583 css::text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE
,
584 offset
, pSet
, start_offset
, end_offset
);
587 catch(const uno::Exception
&){
589 g_warning( "Exception in get_run_attributes()" );
593 atk_attribute_set_free( pSet
);
601 /*****************************************************************************/
603 static AtkAttributeSet
*
604 text_wrapper_get_default_attributes( AtkText
*text
)
606 AtkAttributeSet
*pSet
= nullptr;
609 css::uno::Reference
<css::accessibility::XAccessibleTextAttributes
>
610 pTextAttributes
= getTextAttributes( text
);
611 if( pTextAttributes
.is() )
613 uno::Sequence
< beans::PropertyValue
> aAttributeList
=
614 pTextAttributes
->getDefaultAttributes( uno::Sequence
< OUString
> () );
616 pSet
= attribute_set_new_from_property_values( aAttributeList
, false, text
);
619 catch(const uno::Exception
&) {
621 g_warning( "Exception in get_default_attributes()" );
625 atk_attribute_set_free( pSet
);
633 /*****************************************************************************/
636 text_wrapper_get_character_extents( AtkText
*text
,
642 AtkCoordType coords
)
644 *x
= *y
= *width
= *height
= -1;
647 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
651 awt::Rectangle aRect
= pText
->getCharacterBounds( offset
);
656 if (coords
== ATK_XY_SCREEN
|| coords
== ATK_XY_WINDOW
)
658 g_return_if_fail( ATK_IS_COMPONENT( text
) );
661 atk_component_get_extents(ATK_COMPONENT(text
), &origin_x
, &origin_y
, &nWidth
, &nHeight
, coords
);
664 *x
= aRect
.X
+ origin_x
;
665 *y
= aRect
.Y
+ origin_y
;
666 *width
= aRect
.Width
;
667 *height
= aRect
.Height
;
670 catch(const uno::Exception
&) {
671 g_warning( "Exception in getCharacterBounds" );
676 text_wrapper_get_character_count (AtkText
*text
)
681 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
684 rv
= pText
->getCharacterCount();
686 catch(const uno::Exception
&) {
687 g_warning( "Exception in getCharacterCount" );
694 text_wrapper_get_offset_at_point (AtkText
*text
,
700 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
707 if (coords
== ATK_XY_SCREEN
|| coords
== ATK_XY_WINDOW
)
709 g_return_val_if_fail( ATK_IS_COMPONENT( text
), -1 );
712 atk_component_get_extents(ATK_COMPONENT(text
), &origin_x
, &origin_y
, &nWidth
, &nHeight
, coords
);
715 return pText
->getIndexAtPoint( awt::Point(x
- origin_x
, y
- origin_y
) );
718 catch(const uno::Exception
&) {
719 g_warning( "Exception in getIndexAtPoint" );
725 // FIXME: the whole series of selections API is problematic ...
728 text_wrapper_get_n_selections (AtkText
*text
)
733 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
736 rv
= ( pText
->getSelectionEnd() > pText
->getSelectionStart() ) ? 1 : 0;
738 catch(const uno::Exception
&) {
739 g_warning( "Exception in getSelectionEnd() or getSelectionStart()" );
746 text_wrapper_get_selection (AtkText
*text
,
751 g_return_val_if_fail( selection_num
== 0, FALSE
);
754 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
758 *start_offset
= pText
->getSelectionStart();
759 *end_offset
= pText
->getSelectionEnd();
761 return OUStringToGChar( pText
->getSelectedText() );
764 catch(const uno::Exception
&) {
765 g_warning( "Exception in getSelectionEnd(), getSelectionStart() or getSelectedText()" );
772 text_wrapper_add_selection (AtkText
*text
,
776 // FIXME: can we try to be more compatible by expanding an
777 // existing adjacent selection ?
780 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
783 return pText
->setSelection( start_offset
, end_offset
); // ?
785 catch(const uno::Exception
&) {
786 g_warning( "Exception in setSelection()" );
793 text_wrapper_remove_selection (AtkText
*text
,
796 g_return_val_if_fail( selection_num
== 0, FALSE
);
799 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
802 return pText
->setSelection( 0, 0 ); // ?
804 catch(const uno::Exception
&) {
805 g_warning( "Exception in setSelection()" );
812 text_wrapper_set_selection (AtkText
*text
,
817 g_return_val_if_fail( selection_num
== 0, FALSE
);
820 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
823 return pText
->setSelection( start_offset
, end_offset
);
825 catch(const uno::Exception
&) {
826 g_warning( "Exception in setSelection()" );
832 #if ATK_CHECK_VERSION(2,32,0)
834 text_wrapper_scroll_substring_to(AtkText
*text
,
837 AtkScrollType scroll_type
)
840 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
844 return pText
->scrollSubstringTo( start_offset
, end_offset
,
845 scroll_type_from_scroll_type( scroll_type
) );
847 catch(const uno::Exception
&) {
848 g_warning( "Exception in scrollSubstringTo()" );
858 textIfaceInit (AtkTextIface
*iface
)
860 g_return_if_fail (iface
!= nullptr);
862 iface
->get_text
= text_wrapper_get_text
;
863 iface
->get_character_at_offset
= text_wrapper_get_character_at_offset
;
864 iface
->get_text_before_offset
= text_wrapper_get_text_before_offset
;
865 iface
->get_text_at_offset
= text_wrapper_get_text_at_offset
;
866 iface
->get_text_after_offset
= text_wrapper_get_text_after_offset
;
867 iface
->get_caret_offset
= text_wrapper_get_caret_offset
;
868 iface
->set_caret_offset
= text_wrapper_set_caret_offset
;
869 iface
->get_character_count
= text_wrapper_get_character_count
;
870 iface
->get_n_selections
= text_wrapper_get_n_selections
;
871 iface
->get_selection
= text_wrapper_get_selection
;
872 iface
->add_selection
= text_wrapper_add_selection
;
873 iface
->remove_selection
= text_wrapper_remove_selection
;
874 iface
->set_selection
= text_wrapper_set_selection
;
875 iface
->get_run_attributes
= text_wrapper_get_run_attributes
;
876 iface
->get_default_attributes
= text_wrapper_get_default_attributes
;
877 iface
->get_character_extents
= text_wrapper_get_character_extents
;
878 iface
->get_offset_at_point
= text_wrapper_get_offset_at_point
;
879 #if ATK_CHECK_VERSION(2,32,0)
880 iface
->scroll_substring_to
= text_wrapper_scroll_substring_to
;
884 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */