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 for ( gint nTextMarkupIndex
= 0;
442 nTextMarkupIndex
< nTextMarkupCount
;
445 accessibility::TextSegment aTextSegment
=
446 pTextMarkup
->getTextMarkup( nTextMarkupIndex
, nTextMarkupType
);
447 const gint nStartOffsetTextMarkup
= aTextSegment
.SegmentStart
;
448 const gint nEndOffsetTextMarkup
= aTextSegment
.SegmentEnd
;
449 if ( nStartOffsetTextMarkup
<= offset
)
451 if ( offset
< nEndOffsetTextMarkup
)
453 // text markup at <offset>
454 *start_offset
= ::std::max( *start_offset
,
455 nStartOffsetTextMarkup
);
456 *end_offset
= ::std::min( *end_offset
,
457 nEndOffsetTextMarkup
);
458 switch ( nTextMarkupType
)
460 case css::text::TextMarkupType::SPELLCHECK
:
462 pSet
= attribute_set_prepend_misspelled( pSet
);
465 case css::text::TextMarkupType::TRACK_CHANGE_INSERTION
:
467 pSet
= attribute_set_prepend_tracked_change_insertion( pSet
);
470 case css::text::TextMarkupType::TRACK_CHANGE_DELETION
:
472 pSet
= attribute_set_prepend_tracked_change_deletion( pSet
);
475 case css::text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE
:
477 pSet
= attribute_set_prepend_tracked_change_formatchange( pSet
);
485 break; // no further iteration needed.
489 *start_offset
= ::std::max( *start_offset
,
490 nEndOffsetTextMarkup
);
491 // continue iteration.
496 *end_offset
= ::std::min( *end_offset
,
497 nStartOffsetTextMarkup
);
498 break; // no further iteration.
505 static AtkAttributeSet
*
506 text_wrapper_get_run_attributes( AtkText
*text
,
511 AtkAttributeSet
*pSet
= nullptr;
514 bool bOffsetsAreValid
= false;
516 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
520 uno::Sequence
< beans::PropertyValue
> aAttributeList
;
522 css::uno::Reference
<css::accessibility::XAccessibleTextAttributes
>
523 pTextAttributes
= getTextAttributes( text
);
524 if(pTextAttributes
.is()) // Text attributes are available for paragraphs only
526 aAttributeList
= pTextAttributes
->getRunAttributes( offset
, uno::Sequence
< OUString
> () );
528 else // For other text objects use character attributes
530 aAttributeList
= pText
->getCharacterAttributes( offset
, uno::Sequence
< OUString
> () );
533 pSet
= attribute_set_new_from_property_values( aAttributeList
, true, text
);
535 // - always provide start_offset and end_offset
537 accessibility::TextSegment aTextSegment
=
538 pText
->getTextAtIndex(offset
, accessibility::AccessibleTextType::ATTRIBUTE_RUN
);
540 *start_offset
= aTextSegment
.SegmentStart
;
542 // Do _not_ increment the end_offset provide by <accessibility::TextSegment> instance
543 *end_offset
= aTextSegment
.SegmentEnd
;
544 bOffsetsAreValid
= true;
548 // Special handling for misspelled text
550 // - add special handling for tracked changes and refactor the
551 // corresponding code for handling misspelled text.
552 css::uno::Reference
<css::accessibility::XAccessibleTextMarkup
>
553 pTextMarkup
= getTextMarkup( text
);
554 if( pTextMarkup
.is() )
556 // Get attribute run here if it hasn't been done before
557 if (!bOffsetsAreValid
&& pText
.is())
559 accessibility::TextSegment aAttributeTextSegment
=
560 pText
->getTextAtIndex(offset
, accessibility::AccessibleTextType::ATTRIBUTE_RUN
);
561 *start_offset
= aAttributeTextSegment
.SegmentStart
;
562 *end_offset
= aAttributeTextSegment
.SegmentEnd
;
564 // handle misspelled text
565 pSet
= handle_text_markup_as_run_attribute(
567 css::text::TextMarkupType::SPELLCHECK
,
568 offset
, pSet
, start_offset
, end_offset
);
569 // handle tracked changes
570 pSet
= handle_text_markup_as_run_attribute(
572 css::text::TextMarkupType::TRACK_CHANGE_INSERTION
,
573 offset
, pSet
, start_offset
, end_offset
);
574 pSet
= handle_text_markup_as_run_attribute(
576 css::text::TextMarkupType::TRACK_CHANGE_DELETION
,
577 offset
, pSet
, start_offset
, end_offset
);
578 pSet
= handle_text_markup_as_run_attribute(
580 css::text::TextMarkupType::TRACK_CHANGE_FORMATCHANGE
,
581 offset
, pSet
, start_offset
, end_offset
);
584 catch(const uno::Exception
&){
586 g_warning( "Exception in get_run_attributes()" );
590 atk_attribute_set_free( pSet
);
598 /*****************************************************************************/
600 static AtkAttributeSet
*
601 text_wrapper_get_default_attributes( AtkText
*text
)
603 AtkAttributeSet
*pSet
= nullptr;
606 css::uno::Reference
<css::accessibility::XAccessibleTextAttributes
>
607 pTextAttributes
= getTextAttributes( text
);
608 if( pTextAttributes
.is() )
610 uno::Sequence
< beans::PropertyValue
> aAttributeList
=
611 pTextAttributes
->getDefaultAttributes( uno::Sequence
< OUString
> () );
613 pSet
= attribute_set_new_from_property_values( aAttributeList
, false, text
);
616 catch(const uno::Exception
&) {
618 g_warning( "Exception in get_default_attributes()" );
622 atk_attribute_set_free( pSet
);
630 /*****************************************************************************/
633 text_wrapper_get_character_extents( AtkText
*text
,
639 AtkCoordType coords
)
641 *x
= *y
= *width
= *height
= -1;
644 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
648 awt::Rectangle aRect
= pText
->getCharacterBounds( offset
);
653 if (coords
== ATK_XY_SCREEN
|| coords
== ATK_XY_WINDOW
)
655 g_return_if_fail( ATK_IS_COMPONENT( text
) );
658 atk_component_get_extents(ATK_COMPONENT(text
), &origin_x
, &origin_y
, &nWidth
, &nHeight
, coords
);
661 *x
= aRect
.X
+ origin_x
;
662 *y
= aRect
.Y
+ origin_y
;
663 *width
= aRect
.Width
;
664 *height
= aRect
.Height
;
667 catch(const uno::Exception
&) {
668 g_warning( "Exception in getCharacterBounds" );
673 text_wrapper_get_character_count (AtkText
*text
)
678 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
681 rv
= pText
->getCharacterCount();
683 catch(const uno::Exception
&) {
684 g_warning( "Exception in getCharacterCount" );
691 text_wrapper_get_offset_at_point (AtkText
*text
,
697 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
704 if (coords
== ATK_XY_SCREEN
|| coords
== ATK_XY_WINDOW
)
706 g_return_val_if_fail( ATK_IS_COMPONENT( text
), -1 );
709 atk_component_get_extents(ATK_COMPONENT(text
), &origin_x
, &origin_y
, &nWidth
, &nHeight
, coords
);
712 return pText
->getIndexAtPoint( awt::Point(x
- origin_x
, y
- origin_y
) );
715 catch(const uno::Exception
&) {
716 g_warning( "Exception in getIndexAtPoint" );
722 // FIXME: the whole series of selections API is problematic ...
725 text_wrapper_get_n_selections (AtkText
*text
)
730 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
733 rv
= ( pText
->getSelectionEnd() > pText
->getSelectionStart() ) ? 1 : 0;
735 catch(const uno::Exception
&) {
736 g_warning( "Exception in getSelectionEnd() or getSelectionStart()" );
743 text_wrapper_get_selection (AtkText
*text
,
748 g_return_val_if_fail( selection_num
== 0, FALSE
);
751 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
755 *start_offset
= pText
->getSelectionStart();
756 *end_offset
= pText
->getSelectionEnd();
758 return OUStringToGChar( pText
->getSelectedText() );
761 catch(const uno::Exception
&) {
762 g_warning( "Exception in getSelectionEnd(), getSelectionStart() or getSelectedText()" );
769 text_wrapper_add_selection (AtkText
*text
,
773 // FIXME: can we try to be more compatible by expanding an
774 // existing adjacent selection ?
777 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
780 return pText
->setSelection( start_offset
, end_offset
); // ?
782 catch(const uno::Exception
&) {
783 g_warning( "Exception in setSelection()" );
790 text_wrapper_remove_selection (AtkText
*text
,
793 g_return_val_if_fail( selection_num
== 0, FALSE
);
796 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
799 return pText
->setSelection( 0, 0 ); // ?
801 catch(const uno::Exception
&) {
802 g_warning( "Exception in setSelection()" );
809 text_wrapper_set_selection (AtkText
*text
,
814 g_return_val_if_fail( selection_num
== 0, FALSE
);
817 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
820 return pText
->setSelection( start_offset
, end_offset
);
822 catch(const uno::Exception
&) {
823 g_warning( "Exception in setSelection()" );
829 #if ATK_CHECK_VERSION(2,32,0)
831 text_wrapper_scroll_substring_to(AtkText
*text
,
834 AtkScrollType scroll_type
)
837 css::uno::Reference
<css::accessibility::XAccessibleText
> pText
841 return pText
->scrollSubstringTo( start_offset
, end_offset
,
842 scroll_type_from_scroll_type( scroll_type
) );
844 catch(const uno::Exception
&) {
845 g_warning( "Exception in scrollSubstringTo()" );
855 textIfaceInit (gpointer iface_
, gpointer
)
857 auto const iface
= static_cast<AtkTextIface
*>(iface_
);
858 g_return_if_fail (iface
!= nullptr);
860 iface
->get_text
= text_wrapper_get_text
;
861 iface
->get_character_at_offset
= text_wrapper_get_character_at_offset
;
862 iface
->get_text_before_offset
= text_wrapper_get_text_before_offset
;
863 iface
->get_text_at_offset
= text_wrapper_get_text_at_offset
;
864 iface
->get_text_after_offset
= text_wrapper_get_text_after_offset
;
865 iface
->get_caret_offset
= text_wrapper_get_caret_offset
;
866 iface
->set_caret_offset
= text_wrapper_set_caret_offset
;
867 iface
->get_character_count
= text_wrapper_get_character_count
;
868 iface
->get_n_selections
= text_wrapper_get_n_selections
;
869 iface
->get_selection
= text_wrapper_get_selection
;
870 iface
->add_selection
= text_wrapper_add_selection
;
871 iface
->remove_selection
= text_wrapper_remove_selection
;
872 iface
->set_selection
= text_wrapper_set_selection
;
873 iface
->get_run_attributes
= text_wrapper_get_run_attributes
;
874 iface
->get_default_attributes
= text_wrapper_get_default_attributes
;
875 iface
->get_character_extents
= text_wrapper_get_character_extents
;
876 iface
->get_offset_at_point
= text_wrapper_get_offset_at_point
;
877 #if ATK_CHECK_VERSION(2,32,0)
878 iface
->scroll_substring_to
= text_wrapper_scroll_substring_to
;
882 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */