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 : #include "tbzoomsliderctrl.hxx"
20 : #include <vcl/image.hxx>
21 : #include <vcl/toolbox.hxx>
22 : #include <vcl/virdev.hxx>
23 : #include <vcl/svapp.hxx>
24 : #include <vcl/gradient.hxx>
25 : #include <vcl/settings.hxx>
26 : #include <svl/itemset.hxx>
27 : #include <sfx2/viewfrm.hxx>
28 : #include <sfx2/objsh.hxx>
29 : #include <svx/zoomslideritem.hxx>
30 : #include <svx/dialmgr.hxx>
31 : #include <svx/dialogs.hrc>
32 : #include <set>
33 : #include "docsh.hxx"
34 : #include "stlpool.hxx"
35 : #include "scitems.hxx"
36 : #include "printfun.hxx"
37 :
38 :
39 : // class ScZoomSliderControl ---------------------------------------
40 :
41 :
42 0 : SFX_IMPL_TOOLBOX_CONTROL( ScZoomSliderControl, SvxZoomSliderItem );
43 :
44 0 : ScZoomSliderControl::ScZoomSliderControl(
45 : sal_uInt16 nSlotId,
46 : sal_uInt16 nId,
47 : ToolBox& rTbx )
48 0 : :SfxToolBoxControl( nSlotId, nId, rTbx )
49 : {
50 0 : rTbx.Invalidate();
51 0 : }
52 :
53 0 : ScZoomSliderControl::~ScZoomSliderControl()
54 : {
55 :
56 0 : }
57 :
58 0 : void ScZoomSliderControl::StateChanged( sal_uInt16 /*nSID*/, SfxItemState eState,
59 : const SfxPoolItem* pState )
60 : {
61 0 : sal_uInt16 nId = GetId();
62 0 : ToolBox& rTbx = GetToolBox();
63 0 : ScZoomSliderWnd* pBox = (ScZoomSliderWnd*)(rTbx.GetItemWindow( nId ));
64 : OSL_ENSURE( pBox ,"Control not found!" );
65 :
66 0 : if ( SFX_ITEM_AVAILABLE != eState || pState->ISA( SfxVoidItem ) )
67 : {
68 0 : SvxZoomSliderItem aZoomSliderItem( 100 );
69 0 : pBox->Disable();
70 0 : pBox->UpdateFromItem( &aZoomSliderItem );
71 : }
72 : else
73 : {
74 0 : pBox->Enable();
75 : OSL_ENSURE( pState->ISA( SvxZoomSliderItem ), "invalid item type" );
76 0 : const SvxZoomSliderItem* pZoomSliderItem = dynamic_cast< const SvxZoomSliderItem* >( pState );
77 :
78 : OSL_ENSURE( pZoomSliderItem, "Sc::ScZoomSliderControl::StateChanged(), wrong item type!" );
79 0 : if( pZoomSliderItem )
80 0 : pBox->UpdateFromItem( pZoomSliderItem );
81 : }
82 0 : }
83 :
84 0 : Window* ScZoomSliderControl::CreateItemWindow( Window *pParent )
85 : {
86 : // #i98000# Don't try to get a value via SfxViewFrame::Current here.
87 : // The view's value is always notified via StateChanged later.
88 : ScZoomSliderWnd* pSlider = new ScZoomSliderWnd( pParent,
89 0 : ::com::sun::star::uno::Reference< ::com::sun::star::frame::XDispatchProvider >( m_xFrame->getController(),
90 0 : ::com::sun::star::uno::UNO_QUERY ), m_xFrame, 100 );
91 0 : return pSlider;
92 : }
93 :
94 0 : struct ScZoomSliderWnd::ScZoomSliderWnd_Impl
95 : {
96 : sal_uInt16 mnCurrentZoom;
97 : sal_uInt16 mnMinZoom;
98 : sal_uInt16 mnMaxZoom;
99 : sal_uInt16 mnSliderCenter;
100 : std::vector< long > maSnappingPointOffsets;
101 : std::vector< sal_uInt16 > maSnappingPointZooms;
102 : Image maSliderButton;
103 : Image maIncreaseButton;
104 : Image maDecreaseButton;
105 : bool mbValuesSet;
106 : bool mbOmitPaint;
107 :
108 0 : ScZoomSliderWnd_Impl( sal_uInt16 nCurrentZoom ) :
109 : mnCurrentZoom( nCurrentZoom ),
110 : mnMinZoom( 10 ),
111 : mnMaxZoom( 400 ),
112 : mnSliderCenter( 100 ),
113 : maSnappingPointOffsets(),
114 : maSnappingPointZooms(),
115 : maSliderButton(),
116 : maIncreaseButton(),
117 : maDecreaseButton(),
118 : mbValuesSet( true ),
119 0 : mbOmitPaint( false )
120 : {
121 :
122 0 : }
123 : };
124 :
125 : const long nButtonWidth = 10;
126 : const long nButtonHeight = 10;
127 : const long nIncDecWidth = 11;
128 : const long nIncDecHeight = 11;
129 : const long nSliderHeight = 2;
130 : const long nSliderWidth = 4;
131 : const long nSnappingHeight = 4;
132 : const long nSliderXOffset = 20;
133 : const long nSnappingEpsilon = 5; // snapping epsilon in pixels
134 : const long nSnappingPointsMinDist = nSnappingEpsilon; // minimum distance of two adjacent snapping points
135 :
136 :
137 0 : sal_uInt16 ScZoomSliderWnd::Offset2Zoom( long nOffset ) const
138 : {
139 0 : Size aSliderWindowSize = GetOutputSizePixel();
140 0 : const long nControlWidth = aSliderWindowSize.Width();
141 0 : sal_uInt16 nRet = 0;
142 :
143 0 : if( nOffset < nSliderXOffset )
144 0 : return mpImpl->mnMinZoom;
145 0 : if( nOffset > nControlWidth - nSliderXOffset )
146 0 : return mpImpl->mnMaxZoom;
147 :
148 : // check for snapping points:
149 0 : sal_uInt16 nCount = 0;
150 0 : std::vector< long >::iterator aSnappingPointIter;
151 0 : for ( aSnappingPointIter = mpImpl->maSnappingPointOffsets.begin();
152 0 : aSnappingPointIter != mpImpl->maSnappingPointOffsets.end();
153 : ++aSnappingPointIter )
154 : {
155 0 : const long nCurrent = *aSnappingPointIter;
156 0 : if ( std::abs(nCurrent - nOffset) < nSnappingEpsilon )
157 : {
158 0 : nOffset = nCurrent;
159 0 : nRet = mpImpl->maSnappingPointZooms[ nCount ];
160 0 : break;
161 : }
162 0 : ++nCount;
163 : }
164 :
165 0 : if( 0 == nRet )
166 : {
167 0 : if( nOffset < nControlWidth / 2 )
168 : {
169 : // first half of slider
170 0 : const long nFirstHalfRange = mpImpl->mnSliderCenter - mpImpl->mnMinZoom;
171 0 : const long nHalfSliderWidth = nControlWidth/2 - nSliderXOffset;
172 0 : const long nZoomPerSliderPixel = (1000 * nFirstHalfRange) / nHalfSliderWidth;
173 0 : const long nOffsetToSliderLeft = nOffset - nSliderXOffset;
174 0 : nRet = mpImpl->mnMinZoom + sal_uInt16( nOffsetToSliderLeft * nZoomPerSliderPixel / 1000 );
175 : }
176 : else
177 : {
178 : // second half of slider
179 0 : const long nSecondHalfRange = mpImpl->mnMaxZoom - mpImpl->mnSliderCenter;
180 0 : const long nHalfSliderWidth = nControlWidth/2 - nSliderXOffset;
181 0 : const long nZoomPerSliderPixel = 1000 * nSecondHalfRange / nHalfSliderWidth;
182 0 : const long nOffsetToSliderCenter = nOffset - nControlWidth/2;
183 0 : nRet = mpImpl->mnSliderCenter + sal_uInt16( nOffsetToSliderCenter * nZoomPerSliderPixel / 1000 );
184 : }
185 : }
186 :
187 0 : if( nRet < mpImpl->mnMinZoom )
188 0 : return mpImpl->mnMinZoom;
189 :
190 0 : else if( nRet > mpImpl->mnMaxZoom )
191 0 : return mpImpl->mnMaxZoom;
192 :
193 0 : return nRet;
194 : }
195 :
196 0 : long ScZoomSliderWnd::Zoom2Offset( sal_uInt16 nCurrentZoom ) const
197 : {
198 0 : Size aSliderWindowSize = GetOutputSizePixel();
199 0 : const long nControlWidth = aSliderWindowSize.Width();
200 0 : long nRect = nSliderXOffset;
201 :
202 0 : const long nHalfSliderWidth = nControlWidth/2 - nSliderXOffset;
203 0 : if( nCurrentZoom <= mpImpl->mnSliderCenter )
204 : {
205 0 : nCurrentZoom = nCurrentZoom - mpImpl->mnMinZoom;
206 0 : const long nFirstHalfRange = mpImpl->mnSliderCenter - mpImpl->mnMinZoom;
207 0 : const long nSliderPixelPerZoomPercent = 1000 * nHalfSliderWidth / nFirstHalfRange;
208 0 : const long nOffset = (nSliderPixelPerZoomPercent * nCurrentZoom) / 1000;
209 0 : nRect += nOffset;
210 : }
211 : else
212 : {
213 0 : nCurrentZoom = nCurrentZoom - mpImpl->mnSliderCenter;
214 0 : const long nSecondHalfRange = mpImpl->mnMaxZoom - mpImpl->mnSliderCenter;
215 0 : const long nSliderPixelPerZoomPercent = 1000 * nHalfSliderWidth / nSecondHalfRange;
216 0 : const long nOffset = (nSliderPixelPerZoomPercent * nCurrentZoom) / 1000;
217 0 : nRect += nHalfSliderWidth + nOffset;
218 : }
219 0 : return nRect;
220 : }
221 :
222 :
223 0 : ScZoomSliderWnd::ScZoomSliderWnd( Window* pParent, const ::com::sun::star::uno::Reference< ::com::sun::star::frame::XDispatchProvider >& rDispatchProvider,
224 : const ::com::sun::star::uno::Reference< ::com::sun::star::frame::XFrame >& _xFrame , sal_uInt16 nCurrentZoom ):
225 : Window( pParent ),
226 0 : mpImpl( new ScZoomSliderWnd_Impl( nCurrentZoom ) ),
227 : aLogicalSize( 115, 40 ),
228 : m_xDispatchProvider( rDispatchProvider ),
229 0 : m_xFrame( _xFrame )
230 : {
231 0 : mpImpl->maSliderButton = Image( SVX_RES( RID_SVXBMP_SLIDERBUTTON ) );
232 0 : mpImpl->maIncreaseButton = Image( SVX_RES( RID_SVXBMP_SLIDERINCREASE ) );
233 0 : mpImpl->maDecreaseButton = Image( SVX_RES( RID_SVXBMP_SLIDERDECREASE ) );
234 0 : Size aSliderSize = LogicToPixel( Size( aLogicalSize), MapMode( MAP_10TH_MM ) );
235 0 : SetSizePixel( Size( aSliderSize.Width() * nSliderWidth-1, aSliderSize.Height() + nSliderHeight ) );
236 0 : }
237 :
238 0 : ScZoomSliderWnd::~ScZoomSliderWnd()
239 : {
240 0 : delete mpImpl;
241 0 : }
242 :
243 0 : void ScZoomSliderWnd::MouseButtonDown( const MouseEvent& rMEvt )
244 : {
245 0 : if ( !mpImpl->mbValuesSet )
246 0 : return ;
247 0 : Size aSliderWindowSize = GetOutputSizePixel();
248 :
249 0 : const Point aPoint = rMEvt.GetPosPixel();
250 :
251 0 : const long nButtonLeftOffset = ( nSliderXOffset - nIncDecWidth )/2;
252 0 : const long nButtonRightOffset = ( nSliderXOffset + nIncDecWidth )/2;
253 :
254 0 : const long nOldZoom = mpImpl->mnCurrentZoom;
255 :
256 : // click to - button
257 0 : if ( aPoint.X() >= nButtonLeftOffset && aPoint.X() <= nButtonRightOffset )
258 : {
259 0 : mpImpl->mnCurrentZoom = mpImpl->mnCurrentZoom - 5;
260 : }
261 : // click to + button
262 0 : else if ( aPoint.X() >= aSliderWindowSize.Width() - nSliderXOffset + nButtonLeftOffset &&
263 0 : aPoint.X() <= aSliderWindowSize.Width() - nSliderXOffset + nButtonRightOffset )
264 : {
265 0 : mpImpl->mnCurrentZoom = mpImpl->mnCurrentZoom + 5;
266 : }
267 0 : else if( aPoint.X() >= nSliderXOffset && aPoint.X() <= aSliderWindowSize.Width() - nSliderXOffset )
268 : {
269 0 : mpImpl->mnCurrentZoom = Offset2Zoom( aPoint.X() );
270 : }
271 :
272 0 : if( mpImpl->mnCurrentZoom < mpImpl->mnMinZoom )
273 0 : mpImpl->mnCurrentZoom = mpImpl->mnMinZoom;
274 0 : else if( mpImpl->mnCurrentZoom > mpImpl->mnMaxZoom )
275 0 : mpImpl->mnCurrentZoom = mpImpl->mnMaxZoom;
276 :
277 0 : if( nOldZoom == mpImpl->mnCurrentZoom )
278 0 : return ;
279 :
280 0 : Rectangle aRect( Point( 0, 0 ), aSliderWindowSize );
281 :
282 0 : Paint( aRect );
283 0 : mpImpl->mbOmitPaint = true;
284 :
285 0 : SvxZoomSliderItem aZoomSliderItem( mpImpl->mnCurrentZoom );
286 :
287 0 : ::com::sun::star::uno::Any a;
288 0 : aZoomSliderItem.QueryValue( a );
289 :
290 0 : ::com::sun::star::uno::Sequence< ::com::sun::star::beans::PropertyValue > aArgs( 1 );
291 0 : aArgs[0].Name = "ScalingFactor";
292 0 : aArgs[0].Value = a;
293 :
294 0 : SfxToolBoxControl::Dispatch( m_xDispatchProvider, OUString(".uno:ScalingFactor"), aArgs );
295 :
296 0 : mpImpl->mbOmitPaint = false;
297 : }
298 :
299 0 : void ScZoomSliderWnd::MouseMove( const MouseEvent& rMEvt )
300 : {
301 0 : if ( !mpImpl->mbValuesSet )
302 0 : return ;
303 :
304 0 : Size aSliderWindowSize = GetOutputSizePixel();
305 0 : const long nControlWidth = aSliderWindowSize.Width();
306 0 : const short nButtons = rMEvt.GetButtons();
307 :
308 : // check mouse move with button pressed
309 0 : if ( 1 == nButtons )
310 : {
311 0 : const Point aPoint = rMEvt.GetPosPixel();
312 :
313 0 : if ( aPoint.X() >= nSliderXOffset && aPoint.X() <= nControlWidth - nSliderXOffset )
314 : {
315 0 : mpImpl->mnCurrentZoom = Offset2Zoom( aPoint.X() );
316 :
317 0 : Rectangle aRect( Point( 0, 0 ), aSliderWindowSize );
318 0 : Paint( aRect );
319 :
320 0 : mpImpl->mbOmitPaint = true; // optimization: paint before executing command,
321 :
322 : // commit state change
323 0 : SvxZoomSliderItem aZoomSliderItem( mpImpl->mnCurrentZoom );
324 :
325 0 : ::com::sun::star::uno::Any a;
326 0 : aZoomSliderItem.QueryValue( a );
327 :
328 0 : ::com::sun::star::uno::Sequence< ::com::sun::star::beans::PropertyValue > aArgs( 1 );
329 0 : aArgs[0].Name = "ScalingFactor";
330 0 : aArgs[0].Value = a;
331 :
332 0 : SfxToolBoxControl::Dispatch( m_xDispatchProvider, OUString(".uno:ScalingFactor"), aArgs );
333 :
334 0 : mpImpl->mbOmitPaint = false;
335 : }
336 : }
337 : }
338 :
339 0 : void ScZoomSliderWnd::UpdateFromItem( const SvxZoomSliderItem* pZoomSliderItem )
340 : {
341 0 : if( pZoomSliderItem )
342 : {
343 0 : mpImpl->mnCurrentZoom = pZoomSliderItem->GetValue();
344 0 : mpImpl->mnMinZoom = pZoomSliderItem->GetMinZoom();
345 0 : mpImpl->mnMaxZoom = pZoomSliderItem->GetMaxZoom();
346 :
347 : OSL_ENSURE( mpImpl->mnMinZoom <= mpImpl->mnCurrentZoom &&
348 : mpImpl->mnMinZoom < mpImpl->mnSliderCenter &&
349 : mpImpl->mnMaxZoom >= mpImpl->mnCurrentZoom &&
350 : mpImpl->mnMaxZoom > mpImpl->mnSliderCenter,
351 : "Looks like the zoom slider item is corrupted" );
352 0 : const com::sun::star::uno::Sequence < sal_Int32 > rSnappingPoints = pZoomSliderItem->GetSnappingPoints();
353 0 : mpImpl->maSnappingPointOffsets.clear();
354 0 : mpImpl->maSnappingPointZooms.clear();
355 :
356 : // get all snapping points:
357 0 : std::set< sal_uInt16 > aTmpSnappingPoints;
358 0 : for ( sal_uInt16 j = 0; j < rSnappingPoints.getLength(); ++j )
359 : {
360 0 : const sal_Int32 nSnappingPoint = rSnappingPoints[j];
361 0 : aTmpSnappingPoints.insert( (sal_uInt16)nSnappingPoint );
362 : }
363 :
364 : // remove snapping points that are to close to each other:
365 0 : std::set< sal_uInt16 >::iterator aSnappingPointIter;
366 0 : long nLastOffset = 0;
367 :
368 0 : for ( aSnappingPointIter = aTmpSnappingPoints.begin(); aSnappingPointIter != aTmpSnappingPoints.end(); ++aSnappingPointIter )
369 : {
370 0 : const sal_uInt16 nCurrent = *aSnappingPointIter;
371 0 : const long nCurrentOffset = Zoom2Offset( nCurrent );
372 :
373 0 : if ( nCurrentOffset - nLastOffset >= nSnappingPointsMinDist )
374 : {
375 0 : mpImpl->maSnappingPointOffsets.push_back( nCurrentOffset );
376 0 : mpImpl->maSnappingPointZooms.push_back( nCurrent );
377 0 : nLastOffset = nCurrentOffset;
378 : }
379 0 : }
380 : }
381 :
382 0 : Size aSliderWindowSize = GetOutputSizePixel();
383 0 : Rectangle aRect( Point( 0, 0 ), aSliderWindowSize );
384 :
385 0 : if ( !mpImpl->mbOmitPaint )
386 0 : Paint(aRect);
387 0 : }
388 :
389 0 : void ScZoomSliderWnd::Paint( const Rectangle& rRect )
390 : {
391 0 : DoPaint( rRect );
392 0 : }
393 :
394 0 : void ScZoomSliderWnd::DoPaint( const Rectangle& /*rRect*/ )
395 : {
396 0 : if( mpImpl->mbOmitPaint )
397 0 : return;
398 :
399 0 : Size aSliderWindowSize = GetOutputSizePixel();
400 0 : Rectangle aRect( Point( 0, 0 ), aSliderWindowSize );
401 :
402 0 : VirtualDevice* pVDev = new VirtualDevice( *this );
403 0 : pVDev->SetOutputSizePixel( aSliderWindowSize );
404 :
405 0 : Rectangle aSlider = aRect;
406 :
407 0 : aSlider.Top() += ( aSliderWindowSize.Height() - nSliderHeight )/2 - 1;
408 0 : aSlider.Bottom() = aSlider.Top() + nSliderHeight;
409 0 : aSlider.Left() += nSliderXOffset;
410 0 : aSlider.Right() -= nSliderXOffset;
411 :
412 0 : Rectangle aFirstLine( aSlider );
413 0 : aFirstLine.Bottom() = aFirstLine.Top();
414 :
415 0 : Rectangle aSecondLine( aSlider );
416 0 : aSecondLine.Top() = aSecondLine.Bottom();
417 :
418 0 : Rectangle aLeft( aSlider );
419 0 : aLeft.Right() = aLeft.Left();
420 :
421 0 : Rectangle aRight( aSlider );
422 0 : aRight.Left() = aRight.Right();
423 :
424 : // draw VirtualDevice's background color
425 0 : Color aStartColor,aEndColor;
426 0 : aStartColor = GetSettings().GetStyleSettings().GetFaceColor();
427 0 : aEndColor = GetSettings().GetStyleSettings().GetFaceColor();
428 0 : if( aEndColor.IsDark() )
429 0 : aStartColor = aEndColor;
430 :
431 0 : Gradient g;
432 0 : g.SetAngle( 0 );
433 0 : g.SetStyle( GradientStyle_LINEAR );
434 :
435 0 : g.SetStartColor( aStartColor );
436 0 : g.SetEndColor( aEndColor );
437 0 : pVDev->DrawGradient( aRect, g );
438 :
439 : // draw slider
440 0 : pVDev->SetLineColor( Color ( COL_WHITE ) );
441 0 : pVDev->DrawRect( aSecondLine );
442 0 : pVDev->DrawRect( aRight );
443 :
444 0 : pVDev->SetLineColor( Color( COL_GRAY ) );
445 0 : pVDev->DrawRect( aFirstLine );
446 0 : pVDev->DrawRect( aLeft );
447 :
448 : // draw snapping points:
449 0 : std::vector< long >::iterator aSnappingPointIter;
450 0 : for ( aSnappingPointIter = mpImpl->maSnappingPointOffsets.begin();
451 0 : aSnappingPointIter != mpImpl->maSnappingPointOffsets.end();
452 : ++aSnappingPointIter )
453 : {
454 0 : pVDev->SetLineColor( Color( COL_GRAY ) );
455 0 : Rectangle aSnapping( aRect );
456 0 : aSnapping.Bottom() = aSlider.Top();
457 0 : aSnapping.Top() = aSnapping.Bottom() - nSnappingHeight;
458 0 : aSnapping.Left() += *aSnappingPointIter;
459 0 : aSnapping.Right() = aSnapping.Left();
460 0 : pVDev->DrawRect( aSnapping );
461 :
462 0 : aSnapping.Top() += nSnappingHeight + nSliderHeight;
463 0 : aSnapping.Bottom() += nSnappingHeight + nSliderHeight;
464 0 : pVDev->DrawRect( aSnapping );
465 : }
466 :
467 : // draw slider button
468 0 : Point aImagePoint = aRect.TopLeft();
469 0 : aImagePoint.X() += Zoom2Offset( mpImpl->mnCurrentZoom );
470 0 : aImagePoint.X() -= nButtonWidth/2;
471 0 : aImagePoint.Y() += ( aSliderWindowSize.Height() - nButtonHeight)/2;
472 0 : pVDev->DrawImage( aImagePoint, mpImpl->maSliderButton );
473 :
474 : // draw decrease button
475 0 : aImagePoint = aRect.TopLeft();
476 0 : aImagePoint.X() += (nSliderXOffset - nIncDecWidth)/2;
477 0 : aImagePoint.Y() += ( aSliderWindowSize.Height() - nIncDecHeight)/2;
478 0 : pVDev->DrawImage( aImagePoint, mpImpl->maDecreaseButton );
479 :
480 : // draw increase button
481 0 : aImagePoint.X() = aRect.TopLeft().X() + aSliderWindowSize.Width() - nIncDecWidth - (nSliderXOffset - nIncDecWidth)/2;
482 0 : pVDev->DrawImage( aImagePoint, mpImpl->maIncreaseButton );
483 :
484 0 : DrawOutDev( Point(0, 0), aSliderWindowSize, Point(0, 0), aSliderWindowSize, *pVDev );
485 :
486 0 : delete pVDev;
487 :
488 : }
489 :
490 : /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|