Add mechanism to inform the ScrollAnimator when Scrollbars are added
[WebKit-https.git] / Source / WebCore / platform / mac / ScrollbarThemeMac.mm
1 /*
2  * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
24  */
25
26 #include "config.h"
27 #include "ScrollbarThemeMac.h"
28
29 #include "ImageBuffer.h"
30 #include "LocalCurrentGraphicsContext.h"
31 #include "PlatformMouseEvent.h"
32 #include "ScrollAnimatorMac.h"
33 #include "ScrollView.h"
34 #include <Carbon/Carbon.h>
35 #include <wtf/HashMap.h>
36 #include <wtf/StdLibExtras.h>
37 #include <wtf/UnusedParam.h>
38
39 #if !defined(BUILDING_ON_TIGER) && !defined(BUILDING_ON_LEOPARD) && !defined(BUILDING_ON_SNOW_LEOPARD)
40 #define USE_WK_SCROLLBAR_PAINTER
41 #endif
42
43 // FIXME: There are repainting problems due to Aqua scroll bar buttons' visual overflow.
44
45 using namespace std;
46 using namespace WebCore;
47
48 namespace WebCore {
49
50 #if defined(USE_WK_SCROLLBAR_PAINTER)
51 typedef HashMap<Scrollbar*, RetainPtr<WKScrollbarPainterRef> > ScrollbarPainterMap;
52 #else
53 typedef HashSet<Scrollbar*> ScrollbarPainterMap;
54 #endif
55
56 static ScrollbarPainterMap* scrollbarMap()
57 {
58     static ScrollbarPainterMap* map = new ScrollbarPainterMap;
59     return map;
60 }
61     
62 }
63
64 @interface ScrollbarPrefsObserver : NSObject
65 {
66
67 }
68
69 + (void)registerAsObserver;
70 + (void)appearancePrefsChanged:(NSNotification*)theNotification;
71 + (void)behaviorPrefsChanged:(NSNotification*)theNotification;
72
73 @end
74
75 @implementation ScrollbarPrefsObserver
76
77 + (void)appearancePrefsChanged:(NSNotification*)unusedNotification
78 {
79     UNUSED_PARAM(unusedNotification);
80
81     static_cast<ScrollbarThemeMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
82     if (scrollbarMap()->isEmpty())
83         return;
84     ScrollbarPainterMap::iterator end = scrollbarMap()->end();
85     for (ScrollbarPainterMap::iterator it = scrollbarMap()->begin(); it != end; ++it) {
86 #if defined(USE_WK_SCROLLBAR_PAINTER)
87         it->first->styleChanged();
88         it->first->invalidate();
89 #else
90         (*it)->styleChanged();
91         (*it)->invalidate();
92 #endif
93     }
94 }
95
96 + (void)behaviorPrefsChanged:(NSNotification*)unusedNotification
97 {
98     UNUSED_PARAM(unusedNotification);
99
100     static_cast<ScrollbarThemeMac*>(ScrollbarTheme::nativeTheme())->preferencesChanged();
101 }
102
103 + (void)registerAsObserver
104 {
105     [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(appearancePrefsChanged:) name:@"AppleAquaScrollBarVariantChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately];
106     [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(behaviorPrefsChanged:) name:@"AppleNoRedisplayAppearancePreferenceChanged" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorCoalesce];
107 }
108
109 @end
110
111 namespace WebCore {
112
113 ScrollbarTheme* ScrollbarTheme::nativeTheme()
114 {
115     DEFINE_STATIC_LOCAL(ScrollbarThemeMac, theme, ());
116     return &theme;
117 }
118
119 // FIXME: Get these numbers from CoreUI.
120 static int cRealButtonLength[] = { 28, 21 };
121 static int cButtonHitInset[] = { 3, 2 };
122 // cRealButtonLength - cButtonInset
123 static int cButtonLength[] = { 14, 10 };
124 #if !defined(USE_WK_SCROLLBAR_PAINTER)
125 static int cScrollbarThickness[] = { 15, 11 };
126 static int cButtonInset[] = { 14, 11 };
127 static int cThumbMinLength[] = { 26, 20 };
128 #endif
129
130 static int cOuterButtonLength[] = { 16, 14 }; // The outer button in a double button pair is a bit bigger.
131 static int cOuterButtonOverlap = 2;
132
133 static float gInitialButtonDelay = 0.5f;
134 static float gAutoscrollButtonDelay = 0.05f;
135 static bool gJumpOnTrackClick = false;
136
137 #if defined(USE_WK_SCROLLBAR_PAINTER)
138 static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsNone;
139 #else
140 static ScrollbarButtonsPlacement gButtonPlacement = ScrollbarButtonsDoubleEnd;
141 #endif
142
143 static void updateArrowPlacement()
144 {
145     NSString *buttonPlacement = [[NSUserDefaults standardUserDefaults] objectForKey:@"AppleScrollBarVariant"];
146     if ([buttonPlacement isEqualToString:@"Single"])
147         gButtonPlacement = ScrollbarButtonsSingle;
148     else if ([buttonPlacement isEqualToString:@"DoubleMin"])
149         gButtonPlacement = ScrollbarButtonsDoubleStart;
150     else if ([buttonPlacement isEqualToString:@"DoubleBoth"])
151         gButtonPlacement = ScrollbarButtonsDoubleBoth;
152     else {
153 #if defined(USE_WK_SCROLLBAR_PAINTER)
154         gButtonPlacement = ScrollbarButtonsNone;
155 #else
156         gButtonPlacement = ScrollbarButtonsDoubleEnd;
157 #endif
158     }
159 }
160
161 void ScrollbarThemeMac::registerScrollbar(Scrollbar* scrollbar)
162 {
163 #if defined(USE_WK_SCROLLBAR_PAINTER)
164     bool isHorizontal = scrollbar->orientation() == HorizontalScrollbar;
165     WKScrollbarPainterRef scrollbarPainter = wkMakeScrollbarPainter(scrollbar->controlSize(), isHorizontal);
166     scrollbarMap()->add(scrollbar, scrollbarPainter);
167 #else
168     scrollbarMap()->add(scrollbar);
169 #endif
170 }
171
172 void ScrollbarThemeMac::unregisterScrollbar(Scrollbar* scrollbar)
173 {
174
175     scrollbarMap()->remove(scrollbar);
176 }
177
178 #if defined(USE_WK_SCROLLBAR_PAINTER_AND_CONTROLLER)
179 void ScrollbarThemeMac::setNewPainterForScrollbar(Scrollbar* scrollbar, WKScrollbarPainterRef newPainter)
180 {
181     scrollbarMap()->set(scrollbar, newPainter);
182 }
183
184 WKScrollbarPainterRef ScrollbarThemeMac::painterForScrollbar(Scrollbar* scrollbar)
185 {
186     return scrollbarMap()->get(scrollbar).get();
187 }
188 #endif
189
190 ScrollbarThemeMac::ScrollbarThemeMac()
191 {
192     static bool initialized;
193     if (!initialized) {
194         initialized = true;
195         [ScrollbarPrefsObserver registerAsObserver];
196         preferencesChanged();
197     }
198 }
199
200 ScrollbarThemeMac::~ScrollbarThemeMac()
201 {
202 }
203
204 void ScrollbarThemeMac::preferencesChanged()
205 {
206     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
207     [defaults synchronize];
208     updateArrowPlacement();
209     gInitialButtonDelay = [defaults floatForKey:@"NSScrollerButtonDelay"];
210     gAutoscrollButtonDelay = [defaults floatForKey:@"NSScrollerButtonPeriod"];
211     gJumpOnTrackClick = [defaults boolForKey:@"AppleScrollerPagingBehavior"];
212 }
213
214 int ScrollbarThemeMac::scrollbarThickness(ScrollbarControlSize controlSize)
215 {
216 #if defined(USE_WK_SCROLLBAR_PAINTER)
217     return wkScrollbarThickness(controlSize);
218 #else
219     return cScrollbarThickness[controlSize];
220 #endif
221 }
222
223 bool ScrollbarThemeMac::usesOverlayScrollbars() const
224 {
225 #if defined(USE_WK_SCROLLBAR_PAINTER)
226     return wkScrollbarPainterUsesOverlayScrollers();
227 #else
228     return false;
229 #endif
230 }
231
232 double ScrollbarThemeMac::initialAutoscrollTimerDelay()
233 {
234     return gInitialButtonDelay;
235 }
236
237 double ScrollbarThemeMac::autoscrollTimerDelay()
238 {
239     return gAutoscrollButtonDelay;
240 }
241     
242 ScrollbarButtonsPlacement ScrollbarThemeMac::buttonsPlacement() const
243 {
244     return gButtonPlacement;
245 }
246
247 bool ScrollbarThemeMac::hasButtons(Scrollbar* scrollbar)
248 {
249     return scrollbar->enabled() && gButtonPlacement != ScrollbarButtonsNone
250              && (scrollbar->orientation() == HorizontalScrollbar
251              ? scrollbar->width()
252              : scrollbar->height()) >= 2 * (cRealButtonLength[scrollbar->controlSize()] - cButtonHitInset[scrollbar->controlSize()]);
253 }
254
255 bool ScrollbarThemeMac::hasThumb(Scrollbar* scrollbar)
256 {
257     int minLengthForThumb;
258 #if defined(USE_WK_SCROLLBAR_PAINTER)
259     minLengthForThumb = wkScrollbarMinimumTotalLengthNeededForThumb(scrollbarMap()->get(scrollbar).get());
260 #else
261     minLengthForThumb = 2 * cButtonInset[scrollbar->controlSize()] + cThumbMinLength[scrollbar->controlSize()] + 1;
262 #endif
263     return scrollbar->enabled() && (scrollbar->orientation() == HorizontalScrollbar ? 
264              scrollbar->width() : 
265              scrollbar->height()) >= minLengthForThumb;
266 }
267
268 static IntRect buttonRepaintRect(const IntRect& buttonRect, ScrollbarOrientation orientation, ScrollbarControlSize controlSize, bool start)
269 {
270     ASSERT(gButtonPlacement != ScrollbarButtonsNone);
271
272     IntRect paintRect(buttonRect);
273     if (orientation == HorizontalScrollbar) {
274         paintRect.setWidth(cRealButtonLength[controlSize]);
275         if (!start)
276             paintRect.setX(buttonRect.x() - (cRealButtonLength[controlSize] - buttonRect.width()));
277     } else {
278         paintRect.setHeight(cRealButtonLength[controlSize]);
279         if (!start)
280             paintRect.setY(buttonRect.y() - (cRealButtonLength[controlSize] - buttonRect.height()));
281     }
282
283     return paintRect;
284 }
285
286 IntRect ScrollbarThemeMac::backButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
287 {
288     IntRect result;
289     
290     if (part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd))
291         return result;
292     
293     if (part == BackButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsSingle))
294         return result;
295         
296     int thickness = scrollbarThickness(scrollbar->controlSize());
297     bool outerButton = part == BackButtonStartPart && (buttonsPlacement() == ScrollbarButtonsDoubleStart || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
298     if (outerButton) {
299         if (scrollbar->orientation() == HorizontalScrollbar)
300             result = IntRect(scrollbar->x(), scrollbar->y(), cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0, thickness);
301         else
302             result = IntRect(scrollbar->x(), scrollbar->y(), thickness, cOuterButtonLength[scrollbar->controlSize()] + painting ? cOuterButtonOverlap : 0);
303         return result;
304     }
305     
306     // Our repaint rect is slightly larger, since we are a button that is adjacent to the track.
307     if (scrollbar->orientation() == HorizontalScrollbar) {
308         int start = part == BackButtonStartPart ? scrollbar->x() : scrollbar->x() + scrollbar->width() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
309         result = IntRect(start, scrollbar->y(), cButtonLength[scrollbar->controlSize()], thickness);
310     } else {
311         int start = part == BackButtonStartPart ? scrollbar->y() : scrollbar->y() + scrollbar->height() - cOuterButtonLength[scrollbar->controlSize()] - cButtonLength[scrollbar->controlSize()];
312         result = IntRect(scrollbar->x(), start, thickness, cButtonLength[scrollbar->controlSize()]);
313     }
314     
315     if (painting)
316         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == BackButtonStartPart);
317     return result;
318 }
319
320 IntRect ScrollbarThemeMac::forwardButtonRect(Scrollbar* scrollbar, ScrollbarPart part, bool painting)
321 {
322     IntRect result;
323     
324     if (part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleStart))
325         return result;
326     
327     if (part == ForwardButtonStartPart && (buttonsPlacement() == ScrollbarButtonsNone || buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsSingle))
328         return result;
329         
330     int thickness = scrollbarThickness(scrollbar->controlSize());
331     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
332     int buttonLength = cButtonLength[scrollbar->controlSize()];
333     
334     bool outerButton = part == ForwardButtonEndPart && (buttonsPlacement() == ScrollbarButtonsDoubleEnd || buttonsPlacement() == ScrollbarButtonsDoubleBoth);
335     if (outerButton) {
336         if (scrollbar->orientation() == HorizontalScrollbar) {
337             result = IntRect(scrollbar->x() + scrollbar->width() - outerButtonLength, scrollbar->y(), outerButtonLength, thickness);
338             if (painting)
339                 result.inflateX(cOuterButtonOverlap);
340         } else {
341             result = IntRect(scrollbar->x(), scrollbar->y() + scrollbar->height() - outerButtonLength, thickness, outerButtonLength);
342             if (painting)
343                 result.inflateY(cOuterButtonOverlap);
344         }
345         return result;
346     }
347     
348     if (scrollbar->orientation() == HorizontalScrollbar) {
349         int start = part == ForwardButtonEndPart ? scrollbar->x() + scrollbar->width() - buttonLength : scrollbar->x() + outerButtonLength;
350         result = IntRect(start, scrollbar->y(), buttonLength, thickness);
351     } else {
352         int start = part == ForwardButtonEndPart ? scrollbar->y() + scrollbar->height() - buttonLength : scrollbar->y() + outerButtonLength;
353         result = IntRect(scrollbar->x(), start, thickness, buttonLength);
354     }
355     if (painting)
356         return buttonRepaintRect(result, scrollbar->orientation(), scrollbar->controlSize(), part == ForwardButtonStartPart);
357     return result;
358 }
359
360 IntRect ScrollbarThemeMac::trackRect(Scrollbar* scrollbar, bool painting)
361 {
362     if (painting || !hasButtons(scrollbar))
363         return scrollbar->frameRect();
364     
365     IntRect result;
366     int thickness = scrollbarThickness(scrollbar->controlSize());
367     int startWidth = 0;
368     int endWidth = 0;
369     int outerButtonLength = cOuterButtonLength[scrollbar->controlSize()];
370     int buttonLength = cButtonLength[scrollbar->controlSize()];
371     int doubleButtonLength = outerButtonLength + buttonLength;
372     switch (buttonsPlacement()) {
373         case ScrollbarButtonsSingle:
374             startWidth = buttonLength;
375             endWidth = buttonLength;
376             break;
377         case ScrollbarButtonsDoubleStart:
378             startWidth = doubleButtonLength;
379             break;
380         case ScrollbarButtonsDoubleEnd:
381             endWidth = doubleButtonLength;
382             break;
383         case ScrollbarButtonsDoubleBoth:
384             startWidth = doubleButtonLength;
385             endWidth = doubleButtonLength;
386             break;
387         default:
388             break;
389     }
390     
391     int totalWidth = startWidth + endWidth;
392     if (scrollbar->orientation() == HorizontalScrollbar)
393         return IntRect(scrollbar->x() + startWidth, scrollbar->y(), scrollbar->width() - totalWidth, thickness);
394     return IntRect(scrollbar->x(), scrollbar->y() + startWidth, thickness, scrollbar->height() - totalWidth);
395 }
396
397 int ScrollbarThemeMac::minimumThumbLength(Scrollbar* scrollbar)
398 {
399 #if defined(USE_WK_SCROLLBAR_PAINTER)
400     return wkScrollbarMinimumThumbLength(scrollbarMap()->get(scrollbar).get());
401 #else
402     return cThumbMinLength[scrollbar->controlSize()];
403 #endif
404 }
405
406 bool ScrollbarThemeMac::shouldCenterOnThumb(Scrollbar*, const PlatformMouseEvent& evt)
407 {
408     if (evt.button() != LeftButton)
409         return false;
410     if (gJumpOnTrackClick)
411         return !evt.altKey();
412     return evt.altKey();
413 }
414
415 static int scrollbarPartToHIPressedState(ScrollbarPart part)
416 {
417     switch (part) {
418         case BackButtonStartPart:
419             return kThemeTopOutsideArrowPressed;
420         case BackButtonEndPart:
421             return kThemeTopOutsideArrowPressed; // This does not make much sense.  For some reason the outside constant is required.
422         case ForwardButtonStartPart:
423             return kThemeTopInsideArrowPressed;
424         case ForwardButtonEndPart:
425             return kThemeBottomOutsideArrowPressed;
426         case ThumbPart:
427             return kThemeThumbPressed;
428         default:
429             return 0;
430     }
431 }
432
433 bool ScrollbarThemeMac::paint(Scrollbar* scrollbar, GraphicsContext* context, const IntRect& damageRect)
434 {
435 #if defined(USE_WK_SCROLLBAR_PAINTER)
436     float value = 0.0f;
437     float totalSize = 0.0f;
438
439     if (scrollbar->currentPos() < 0) {
440         // Scrolled past the top.
441         value = 0.0f;
442         totalSize = scrollbar->totalSize() - scrollbar->currentPos();
443     } else if (scrollbar->visibleSize() + scrollbar->currentPos() > scrollbar->totalSize()) {
444         // Scrolled past the bottom.
445         value = 1.0f;
446         totalSize = scrollbar->visibleSize() + scrollbar->currentPos();
447     } else {
448         // Within the bounds of the scrollable area.
449         value = scrollbar->currentPos() / scrollbar->maximum();
450         totalSize = scrollbar->totalSize();
451     }
452
453     context->save();
454     context->clip(damageRect);
455     context->translate(scrollbar->frameRect().x(), scrollbar->frameRect().y());
456     LocalCurrentGraphicsContext localContext(context);
457     wkScrollbarPainterPaint(scrollbarMap()->get(scrollbar).get(),
458                             scrollbar->enabled(),
459                             value,
460                             static_cast<CGFloat>(scrollbar->visibleSize()) / totalSize,
461                             scrollbar->frameRect());
462     context->restore();
463     return true;
464 #endif
465
466     HIThemeTrackDrawInfo trackInfo;
467     trackInfo.version = 0;
468     trackInfo.kind = scrollbar->controlSize() == RegularScrollbar ? kThemeMediumScrollBar : kThemeSmallScrollBar;
469     trackInfo.bounds = scrollbar->frameRect();
470
471     float maximum = 0.0f;
472     float position = 0.0f;
473     if (scrollbar->currentPos() < 0) {
474         // Scrolled past the top.
475         maximum = (scrollbar->totalSize() - scrollbar->currentPos()) - scrollbar->visibleSize();
476         position = 0;
477     } else if (scrollbar->visibleSize() + scrollbar->currentPos() > scrollbar->totalSize()) {
478         // Scrolled past the bottom.
479         maximum = scrollbar->currentPos();
480         position = maximum;
481     } else {
482         // Within the bounds of the scrollable area.
483         maximum = scrollbar->maximum();
484         position = scrollbar->currentPos();
485     }
486
487     trackInfo.min = 0;
488     trackInfo.max = static_cast<int>(maximum);
489     trackInfo.value = static_cast<int>(position);
490
491     trackInfo.trackInfo.scrollbar.viewsize = scrollbar->visibleSize();
492     trackInfo.attributes = 0;
493     if (scrollbar->orientation() == HorizontalScrollbar)
494         trackInfo.attributes |= kThemeTrackHorizontal;
495
496     if (!scrollbar->enabled())
497         trackInfo.enableState = kThemeTrackDisabled;
498     else
499         trackInfo.enableState = scrollbar->scrollableArea()->isActive() ? kThemeTrackActive : kThemeTrackInactive;
500
501     if (hasThumb(scrollbar))
502         trackInfo.attributes |= kThemeTrackShowThumb;
503     else if (!hasButtons(scrollbar))
504         trackInfo.enableState = kThemeTrackNothingToScroll;
505     trackInfo.trackInfo.scrollbar.pressState = scrollbarPartToHIPressedState(scrollbar->pressedPart());
506     
507     // The Aqua scrollbar is buggy when rotated and scaled.  We will just draw into a bitmap if we detect a scale or rotation.
508     const AffineTransform& currentCTM = context->getCTM();
509     bool canDrawDirectly = currentCTM.isIdentityOrTranslationOrFlipped();
510     if (canDrawDirectly)
511         HIThemeDrawTrack(&trackInfo, 0, context->platformContext(), kHIThemeOrientationNormal);
512     else {
513         trackInfo.bounds = IntRect(IntPoint(), scrollbar->frameRect().size());
514         
515         IntRect bufferRect(scrollbar->frameRect());
516         bufferRect.intersect(damageRect);
517         bufferRect.move(-scrollbar->frameRect().x(), -scrollbar->frameRect().y());
518         
519         OwnPtr<ImageBuffer> imageBuffer = ImageBuffer::create(bufferRect.size());
520         if (!imageBuffer)
521             return true;
522         
523         HIThemeDrawTrack(&trackInfo, 0, imageBuffer->context()->platformContext(), kHIThemeOrientationNormal);
524         context->drawImageBuffer(imageBuffer.get(), ColorSpaceDeviceRGB, scrollbar->frameRect().location());
525     }
526
527     return true;
528 }
529
530 }
531