1 /* 2 * Copyright 2001-2020 Haiku, Inc. All rights reserved. 3 * Distributed under the terms of the MIT license. 4 * 5 * Authors: 6 * Stephan Aßmus, superstippi@gmx.de 7 * Stefano Ceccherini, burton666@libero.it 8 * DarkWyrm, bpmagic@columbus.rr.com 9 * Marc Flerackers, mflerackers@androme.be 10 * John Scipione, jscipione@gmail.com 11 */ 12 13 14 #include <ScrollBar.h> 15 16 #include <algorithm> 17 18 #include <math.h> 19 #include <stdio.h> 20 #include <stdlib.h> 21 #include <string.h> 22 23 #include <ControlLook.h> 24 #include <LayoutUtils.h> 25 #include <Message.h> 26 #include <OS.h> 27 #include <Shape.h> 28 #include <Window.h> 29 30 #include <binary_compatibility/Interface.h> 31 32 33 //#define TRACE_SCROLLBAR 34 #ifdef TRACE_SCROLLBAR 35 # define TRACE(x...) printf(x) 36 #else 37 # define TRACE(x...) 38 #endif 39 40 41 typedef enum { 42 ARROW_LEFT = 0, 43 ARROW_RIGHT, 44 ARROW_UP, 45 ARROW_DOWN, 46 ARROW_NONE 47 } arrow_direction; 48 49 50 #define SCROLL_BAR_MAXIMUM_KNOB_SIZE 50 51 #define SCROLL_BAR_MINIMUM_KNOB_SIZE 9 52 53 #define SBC_SCROLLBYVALUE 0 54 #define SBC_SETDOUBLE 1 55 #define SBC_SETPROPORTIONAL 2 56 #define SBC_SETSTYLE 3 57 58 // Quick constants for determining which arrow is down and are defined with 59 // respect to double arrow mode. ARROW1 and ARROW4 refer to the outer pair of 60 // arrows and ARROW2 and ARROW3 refer to the inner ones. ARROW1 points left/up 61 // and ARROW4 points right/down. 62 #define ARROW1 0 63 #define ARROW2 1 64 #define ARROW3 2 65 #define ARROW4 3 66 #define THUMB 4 67 #define NOARROW -1 68 69 70 static const bigtime_t kRepeatDelay = 300000; 71 72 73 // Because the R5 version kept a lot of data on server-side, we need to kludge 74 // our way into binary compatibility 75 class BScrollBar::Private { 76 public: 77 Private(BScrollBar* scrollBar) 78 : 79 fScrollBar(scrollBar), 80 fEnabled(true), 81 fRepeaterThread(-1), 82 fExitRepeater(false), 83 fRepeaterDelay(0), 84 fThumbFrame(0.0, 0.0, -1.0, -1.0), 85 fDoRepeat(false), 86 fClickOffset(0.0, 0.0), 87 fThumbInc(0.0), 88 fStopValue(0.0), 89 fUpArrowsEnabled(true), 90 fDownArrowsEnabled(true), 91 fBorderHighlighted(false), 92 fButtonDown(NOARROW) 93 { 94 #ifdef TEST_MODE 95 fScrollBarInfo.proportional = true; 96 fScrollBarInfo.double_arrows = true; 97 fScrollBarInfo.knob = 0; 98 fScrollBarInfo.min_knob_size = 15; 99 #else 100 get_scroll_bar_info(&fScrollBarInfo); 101 #endif 102 103 fScrollBarInfo.min_knob_size = (int32)(fScrollBarInfo.min_knob_size * 104 (be_plain_font->Size() / 12.0f)); 105 } 106 107 ~Private() 108 { 109 if (fRepeaterThread >= 0) { 110 status_t dummy; 111 fExitRepeater = true; 112 wait_for_thread(fRepeaterThread, &dummy); 113 } 114 } 115 116 void DrawScrollBarButton(BScrollBar* owner, arrow_direction direction, 117 BRect frame, bool down = false); 118 119 static int32 button_repeater_thread(void* data); 120 121 int32 ButtonRepeaterThread(); 122 123 BScrollBar* fScrollBar; 124 bool fEnabled; 125 126 // TODO: This should be a static, initialized by 127 // _init_interface_kit() at application startup-time, 128 // like BMenu::sMenuInfo 129 scroll_bar_info fScrollBarInfo; 130 131 thread_id fRepeaterThread; 132 volatile bool fExitRepeater; 133 bigtime_t fRepeaterDelay; 134 135 BRect fThumbFrame; 136 volatile bool fDoRepeat; 137 BPoint fClickOffset; 138 139 float fThumbInc; 140 float fStopValue; 141 142 bool fUpArrowsEnabled; 143 bool fDownArrowsEnabled; 144 145 bool fBorderHighlighted; 146 147 int8 fButtonDown; 148 }; 149 150 151 // This thread is spawned when a button is initially pushed and repeatedly scrolls 152 // the scrollbar by a little bit after a short delay 153 int32 154 BScrollBar::Private::button_repeater_thread(void* data) 155 { 156 BScrollBar::Private* privateData = (BScrollBar::Private*)data; 157 return privateData->ButtonRepeaterThread(); 158 } 159 160 161 int32 162 BScrollBar::Private::ButtonRepeaterThread() 163 { 164 // Wait a bit before auto scrolling starts. As long as the user releases 165 // and presses the button again while the repeat delay has not yet 166 // triggered, the value is pushed into the future, so we need to loop such 167 // that repeating starts at exactly the correct delay after the last 168 // button press. 169 while (fRepeaterDelay > system_time() && !fExitRepeater) 170 snooze_until(fRepeaterDelay, B_SYSTEM_TIMEBASE); 171 172 // repeat loop 173 while (!fExitRepeater) { 174 if (fScrollBar->LockLooper()) { 175 if (fDoRepeat) { 176 float value = fScrollBar->Value() + fThumbInc; 177 if (fButtonDown == NOARROW) { 178 // in this case we want to stop when we're under the mouse 179 if (fThumbInc > 0.0 && value <= fStopValue) 180 fScrollBar->SetValue(value); 181 if (fThumbInc < 0.0 && value >= fStopValue) 182 fScrollBar->SetValue(value); 183 } else 184 fScrollBar->SetValue(value); 185 } 186 187 fScrollBar->UnlockLooper(); 188 } 189 190 snooze(25000); 191 } 192 193 // tell scrollbar we're gone 194 if (fScrollBar->LockLooper()) { 195 fRepeaterThread = -1; 196 fScrollBar->UnlockLooper(); 197 } 198 199 return 0; 200 } 201 202 203 // #pragma mark - BScrollBar 204 205 206 BScrollBar::BScrollBar(BRect frame, const char* name, BView* target, 207 float min, float max, orientation direction) 208 : 209 BView(frame, name, B_FOLLOW_NONE, 210 B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS), 211 fMin(min), 212 fMax(max), 213 fSmallStep(1.0f), 214 fLargeStep(10.0f), 215 fValue(0), 216 fProportion(0.0f), 217 fTarget(NULL), 218 fOrientation(direction) 219 { 220 SetViewColor(B_TRANSPARENT_COLOR); 221 222 fPrivateData = new BScrollBar::Private(this); 223 224 SetTarget(target); 225 SetEventMask(B_NO_POINTER_HISTORY); 226 227 _UpdateThumbFrame(); 228 _UpdateArrowButtons(); 229 230 SetResizingMode(direction == B_VERTICAL 231 ? B_FOLLOW_TOP_BOTTOM | B_FOLLOW_RIGHT 232 : B_FOLLOW_LEFT_RIGHT | B_FOLLOW_BOTTOM); 233 } 234 235 236 BScrollBar::BScrollBar(const char* name, BView* target, 237 float min, float max, orientation direction) 238 : 239 BView(name, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS), 240 fMin(min), 241 fMax(max), 242 fSmallStep(1.0f), 243 fLargeStep(10.0f), 244 fValue(0), 245 fProportion(0.0f), 246 fTarget(NULL), 247 fOrientation(direction) 248 { 249 SetViewColor(B_TRANSPARENT_COLOR); 250 251 fPrivateData = new BScrollBar::Private(this); 252 253 SetTarget(target); 254 SetEventMask(B_NO_POINTER_HISTORY); 255 256 _UpdateThumbFrame(); 257 _UpdateArrowButtons(); 258 } 259 260 261 BScrollBar::BScrollBar(BMessage* data) 262 : 263 BView(data), 264 fTarget(NULL) 265 { 266 fPrivateData = new BScrollBar::Private(this); 267 268 // TODO: Does the BeOS implementation try to find the target 269 // by name again? Does it archive the name at all? 270 if (data->FindFloat("_range", 0, &fMin) < B_OK) 271 fMin = 0.0f; 272 273 if (data->FindFloat("_range", 1, &fMax) < B_OK) 274 fMax = 0.0f; 275 276 if (data->FindFloat("_steps", 0, &fSmallStep) < B_OK) 277 fSmallStep = 1.0f; 278 279 if (data->FindFloat("_steps", 1, &fLargeStep) < B_OK) 280 fLargeStep = 10.0f; 281 282 if (data->FindFloat("_val", &fValue) < B_OK) 283 fValue = 0.0; 284 285 int32 orientation; 286 if (data->FindInt32("_orient", &orientation) < B_OK) { 287 fOrientation = B_VERTICAL; 288 } else 289 fOrientation = (enum orientation)orientation; 290 291 if ((Flags() & B_SUPPORTS_LAYOUT) == 0) { 292 // just to make sure 293 SetResizingMode(fOrientation == B_VERTICAL 294 ? B_FOLLOW_TOP_BOTTOM | B_FOLLOW_RIGHT 295 : B_FOLLOW_LEFT_RIGHT | B_FOLLOW_BOTTOM); 296 } 297 298 if (data->FindFloat("_prop", &fProportion) < B_OK) 299 fProportion = 0.0; 300 301 _UpdateThumbFrame(); 302 _UpdateArrowButtons(); 303 } 304 305 306 BScrollBar::~BScrollBar() 307 { 308 SetTarget((BView*)NULL); 309 delete fPrivateData; 310 } 311 312 313 BArchivable* 314 BScrollBar::Instantiate(BMessage* data) 315 { 316 if (validate_instantiation(data, "BScrollBar")) 317 return new BScrollBar(data); 318 return NULL; 319 } 320 321 322 status_t 323 BScrollBar::Archive(BMessage* data, bool deep) const 324 { 325 status_t err = BView::Archive(data, deep); 326 if (err != B_OK) 327 return err; 328 329 err = data->AddFloat("_range", fMin); 330 if (err != B_OK) 331 return err; 332 333 err = data->AddFloat("_range", fMax); 334 if (err != B_OK) 335 return err; 336 337 err = data->AddFloat("_steps", fSmallStep); 338 if (err != B_OK) 339 return err; 340 341 err = data->AddFloat("_steps", fLargeStep); 342 if (err != B_OK) 343 return err; 344 345 err = data->AddFloat("_val", fValue); 346 if (err != B_OK) 347 return err; 348 349 err = data->AddInt32("_orient", (int32)fOrientation); 350 if (err != B_OK) 351 return err; 352 353 err = data->AddFloat("_prop", fProportion); 354 355 return err; 356 } 357 358 359 void 360 BScrollBar::AllAttached() 361 { 362 BView::AllAttached(); 363 } 364 365 366 void 367 BScrollBar::AllDetached() 368 { 369 BView::AllDetached(); 370 } 371 372 373 void 374 BScrollBar::AttachedToWindow() 375 { 376 BView::AttachedToWindow(); 377 } 378 379 380 void 381 BScrollBar::DetachedFromWindow() 382 { 383 BView::DetachedFromWindow(); 384 } 385 386 387 void 388 BScrollBar::Draw(BRect updateRect) 389 { 390 rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR); 391 392 uint32 flags = 0; 393 bool scrollingEnabled = fMin < fMax 394 && fProportion >= 0.0f && fProportion < 1.0f; 395 if (scrollingEnabled) 396 flags |= BControlLook::B_PARTIALLY_ACTIVATED; 397 398 if (!fPrivateData->fEnabled || !scrollingEnabled) 399 flags |= BControlLook::B_DISABLED; 400 401 bool isFocused = fPrivateData->fBorderHighlighted; 402 if (isFocused) 403 flags |= BControlLook::B_FOCUSED; 404 405 bool doubleArrows = _DoubleArrows(); 406 407 // draw border 408 BRect rect = Bounds(); 409 be_control_look->DrawScrollBarBorder(this, rect, updateRect, base, flags, 410 fOrientation); 411 412 // inset past border 413 rect.InsetBy(1, 1); 414 415 // draw arrows buttons 416 BRect thumbBG = rect; 417 if (fOrientation == B_HORIZONTAL) { 418 BRect buttonFrame(rect.left, rect.top, 419 rect.left + rect.Height(), rect.bottom); 420 421 be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect, 422 base, flags | (fPrivateData->fButtonDown == ARROW1 423 ? BControlLook::B_ACTIVATED : 0), 424 BControlLook::B_LEFT_ARROW, fOrientation, 425 fPrivateData->fButtonDown == ARROW1); 426 427 if (doubleArrows) { 428 buttonFrame.OffsetBy(rect.Height() + 1, 0.0f); 429 be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect, 430 base, flags | (fPrivateData->fButtonDown == ARROW2 431 ? BControlLook::B_ACTIVATED : 0), 432 BControlLook::B_RIGHT_ARROW, fOrientation, 433 fPrivateData->fButtonDown == ARROW2); 434 435 buttonFrame.OffsetTo(rect.right - ((rect.Height() * 2) + 1), 436 rect.top); 437 be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect, 438 base, flags | (fPrivateData->fButtonDown == ARROW3 439 ? BControlLook::B_ACTIVATED : 0), 440 BControlLook::B_LEFT_ARROW, fOrientation, 441 fPrivateData->fButtonDown == ARROW3); 442 443 thumbBG.left += rect.Height() * 2 + 2; 444 thumbBG.right -= rect.Height() * 2 + 2; 445 } else { 446 thumbBG.left += rect.Height() + 1; 447 thumbBG.right -= rect.Height() + 1; 448 } 449 450 buttonFrame.OffsetTo(rect.right - rect.Height(), rect.top); 451 be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect, 452 base, flags | (fPrivateData->fButtonDown == ARROW4 453 ? BControlLook::B_ACTIVATED : 0), 454 BControlLook::B_RIGHT_ARROW, fOrientation, 455 fPrivateData->fButtonDown == ARROW4); 456 } else { 457 BRect buttonFrame(rect.left, rect.top, rect.right, 458 rect.top + rect.Width()); 459 460 be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect, 461 base, flags | (fPrivateData->fButtonDown == ARROW1 462 ? BControlLook::B_ACTIVATED : 0), 463 BControlLook::B_UP_ARROW, fOrientation, 464 fPrivateData->fButtonDown == ARROW1); 465 466 if (doubleArrows) { 467 buttonFrame.OffsetBy(0, rect.Width() + 1); 468 be_control_look->DrawScrollBarButton(this, buttonFrame, 469 updateRect, base, flags | (fPrivateData->fButtonDown == ARROW2 470 ? BControlLook::B_ACTIVATED : 0), 471 BControlLook::B_DOWN_ARROW, fOrientation, 472 fPrivateData->fButtonDown == ARROW2); 473 474 buttonFrame.OffsetTo(rect.left, rect.bottom 475 - ((rect.Width() * 2) + 1)); 476 be_control_look->DrawScrollBarButton(this, buttonFrame, 477 updateRect, base, flags | (fPrivateData->fButtonDown == ARROW3 478 ? BControlLook::B_ACTIVATED : 0), 479 BControlLook::B_UP_ARROW, fOrientation, 480 fPrivateData->fButtonDown == ARROW3); 481 482 thumbBG.top += rect.Width() * 2 + 2; 483 thumbBG.bottom -= rect.Width() * 2 + 2; 484 } else { 485 thumbBG.top += rect.Width() + 1; 486 thumbBG.bottom -= rect.Width() + 1; 487 } 488 489 buttonFrame.OffsetTo(rect.left, rect.bottom - rect.Width()); 490 be_control_look->DrawScrollBarButton(this, buttonFrame, updateRect, 491 base, flags | (fPrivateData->fButtonDown == ARROW4 492 ? BControlLook::B_ACTIVATED : 0), 493 BControlLook::B_DOWN_ARROW, fOrientation, 494 fPrivateData->fButtonDown == ARROW4); 495 } 496 497 // fill background besides the thumb 498 rect = fPrivateData->fThumbFrame; 499 if (fOrientation == B_HORIZONTAL) { 500 BRect leftOfThumb(thumbBG.left, thumbBG.top, 501 rect.left - 1, thumbBG.bottom); 502 BRect rightOfThumb(rect.right + 1, thumbBG.top, 503 thumbBG.right, thumbBG.bottom); 504 be_control_look->DrawScrollBarBackground(this, leftOfThumb, 505 rightOfThumb, updateRect, base, flags, fOrientation); 506 } else { 507 BRect topOfThumb(thumbBG.left, thumbBG.top, 508 thumbBG.right, rect.top - 1); 509 BRect bottomOfThumb(thumbBG.left, rect.bottom + 1, 510 thumbBG.right, thumbBG.bottom); 511 be_control_look->DrawScrollBarBackground(this, topOfThumb, 512 bottomOfThumb, updateRect, base, flags, fOrientation); 513 } 514 515 // draw thumb 516 rect = fPrivateData->fThumbFrame; 517 be_control_look->DrawScrollBarThumb(this, rect, updateRect, 518 ui_color(B_SCROLL_BAR_THUMB_COLOR), flags, fOrientation, 519 fPrivateData->fScrollBarInfo.knob); 520 } 521 522 523 void 524 BScrollBar::FrameMoved(BPoint newPosition) 525 { 526 BView::FrameMoved(newPosition); 527 } 528 529 530 void 531 BScrollBar::FrameResized(float newWidth, float newHeight) 532 { 533 _UpdateThumbFrame(); 534 } 535 536 537 void 538 BScrollBar::MessageReceived(BMessage* message) 539 { 540 switch(message->what) { 541 case B_VALUE_CHANGED: 542 { 543 int32 value; 544 if (message->FindInt32("value", &value) == B_OK) 545 ValueChanged(value); 546 547 break; 548 } 549 550 case B_MOUSE_WHEEL_CHANGED: 551 { 552 // Must handle this here since BView checks for the existence of 553 // scrollbars, which a scrollbar itself does not have 554 float deltaX = 0.0f; 555 float deltaY = 0.0f; 556 message->FindFloat("be:wheel_delta_x", &deltaX); 557 message->FindFloat("be:wheel_delta_y", &deltaY); 558 559 if (deltaX == 0.0f && deltaY == 0.0f) 560 break; 561 562 if (deltaX != 0.0f && deltaY == 0.0f) 563 deltaY = deltaX; 564 565 ScrollWithMouseWheelDelta(this, deltaY); 566 } 567 568 default: 569 BView::MessageReceived(message); 570 } 571 } 572 573 574 void 575 BScrollBar::MouseDown(BPoint where) 576 { 577 if (!fPrivateData->fEnabled || fMin == fMax) 578 return; 579 580 SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS); 581 582 int32 buttons; 583 if (Looper() == NULL || Looper()->CurrentMessage() == NULL 584 || Looper()->CurrentMessage()->FindInt32("buttons", &buttons) != B_OK) { 585 buttons = B_PRIMARY_MOUSE_BUTTON; 586 } 587 588 if (buttons & B_SECONDARY_MOUSE_BUTTON) { 589 // special absolute scrolling: move thumb to where we clicked 590 fPrivateData->fButtonDown = THUMB; 591 fPrivateData->fClickOffset 592 = fPrivateData->fThumbFrame.LeftTop() - where; 593 if (Orientation() == B_HORIZONTAL) { 594 fPrivateData->fClickOffset.x 595 = -fPrivateData->fThumbFrame.Width() / 2; 596 } else { 597 fPrivateData->fClickOffset.y 598 = -fPrivateData->fThumbFrame.Height() / 2; 599 } 600 601 SetValue(_ValueFor(where + fPrivateData->fClickOffset)); 602 return; 603 } 604 605 // hit test for the thumb 606 if (fPrivateData->fThumbFrame.Contains(where)) { 607 fPrivateData->fButtonDown = THUMB; 608 fPrivateData->fClickOffset 609 = fPrivateData->fThumbFrame.LeftTop() - where; 610 Invalidate(fPrivateData->fThumbFrame); 611 return; 612 } 613 614 // hit test for arrows or empty area 615 float scrollValue = 0.0; 616 617 // pressing the shift key scrolls faster 618 float buttonStepSize 619 = (modifiers() & B_SHIFT_KEY) != 0 ? fLargeStep : fSmallStep; 620 621 fPrivateData->fButtonDown = _ButtonFor(where); 622 switch (fPrivateData->fButtonDown) { 623 case ARROW1: 624 scrollValue = -buttonStepSize; 625 break; 626 627 case ARROW2: 628 scrollValue = buttonStepSize; 629 break; 630 631 case ARROW3: 632 scrollValue = -buttonStepSize; 633 break; 634 635 case ARROW4: 636 scrollValue = buttonStepSize; 637 break; 638 639 case NOARROW: 640 // we hit the empty area, figure out which side of the thumb 641 if (fOrientation == B_VERTICAL) { 642 if (where.y < fPrivateData->fThumbFrame.top) 643 scrollValue = -fLargeStep; 644 else 645 scrollValue = fLargeStep; 646 } else { 647 if (where.x < fPrivateData->fThumbFrame.left) 648 scrollValue = -fLargeStep; 649 else 650 scrollValue = fLargeStep; 651 } 652 _UpdateTargetValue(where); 653 break; 654 } 655 if (scrollValue != 0.0) { 656 SetValue(fValue + scrollValue); 657 Invalidate(_ButtonRectFor(fPrivateData->fButtonDown)); 658 659 // launch the repeat thread 660 if (fPrivateData->fRepeaterThread == -1) { 661 fPrivateData->fExitRepeater = false; 662 fPrivateData->fRepeaterDelay = system_time() + kRepeatDelay; 663 fPrivateData->fThumbInc = scrollValue; 664 fPrivateData->fDoRepeat = true; 665 fPrivateData->fRepeaterThread = spawn_thread( 666 fPrivateData->button_repeater_thread, "scroll repeater", 667 B_NORMAL_PRIORITY, fPrivateData); 668 resume_thread(fPrivateData->fRepeaterThread); 669 } else { 670 fPrivateData->fExitRepeater = false; 671 fPrivateData->fRepeaterDelay = system_time() + kRepeatDelay; 672 fPrivateData->fDoRepeat = true; 673 } 674 } 675 } 676 677 678 void 679 BScrollBar::MouseMoved(BPoint where, uint32 code, const BMessage* dragMessage) 680 { 681 if (!fPrivateData->fEnabled || fMin >= fMax || fProportion >= 1.0f 682 || fProportion < 0.0f) { 683 return; 684 } 685 686 if (fPrivateData->fButtonDown != NOARROW) { 687 if (fPrivateData->fButtonDown == THUMB) { 688 SetValue(_ValueFor(where + fPrivateData->fClickOffset)); 689 } else { 690 // suspend the repeating if the mouse is not over the button 691 bool repeat = _ButtonRectFor(fPrivateData->fButtonDown).Contains( 692 where); 693 if (fPrivateData->fDoRepeat != repeat) { 694 fPrivateData->fDoRepeat = repeat; 695 Invalidate(_ButtonRectFor(fPrivateData->fButtonDown)); 696 } 697 } 698 } else { 699 // update the value at which we want to stop repeating 700 if (fPrivateData->fDoRepeat) { 701 _UpdateTargetValue(where); 702 // we might have to turn arround 703 if ((fValue < fPrivateData->fStopValue 704 && fPrivateData->fThumbInc < 0) 705 || (fValue > fPrivateData->fStopValue 706 && fPrivateData->fThumbInc > 0)) { 707 fPrivateData->fThumbInc = -fPrivateData->fThumbInc; 708 } 709 } 710 } 711 } 712 713 714 void 715 BScrollBar::MouseUp(BPoint where) 716 { 717 if (fPrivateData->fButtonDown == THUMB) 718 Invalidate(fPrivateData->fThumbFrame); 719 else 720 Invalidate(_ButtonRectFor(fPrivateData->fButtonDown)); 721 722 fPrivateData->fButtonDown = NOARROW; 723 fPrivateData->fExitRepeater = true; 724 fPrivateData->fDoRepeat = false; 725 } 726 727 728 void 729 BScrollBar::WindowActivated(bool active) 730 { 731 fPrivateData->fEnabled = active; 732 Invalidate(); 733 } 734 735 736 void 737 BScrollBar::SetValue(float value) 738 { 739 if (value > fMax) 740 value = fMax; 741 else if (value < fMin) 742 value = fMin; 743 else if (isnan(value) || isinf(value)) 744 return; 745 746 value = roundf(value); 747 if (value == fValue) 748 return; 749 750 TRACE("BScrollBar(%s)::SetValue(%.1f)\n", Name(), value); 751 752 fValue = value; 753 754 _UpdateThumbFrame(); 755 _UpdateArrowButtons(); 756 757 ValueChanged(fValue); 758 } 759 760 761 float 762 BScrollBar::Value() const 763 { 764 return fValue; 765 } 766 767 768 void 769 BScrollBar::ValueChanged(float newValue) 770 { 771 TRACE("BScrollBar(%s)::ValueChanged(%.1f)\n", Name(), newValue); 772 773 if (fTarget != NULL) { 774 // cache target bounds 775 BRect targetBounds = fTarget->Bounds(); 776 // if vertical, check bounds top and scroll if different from newValue 777 if (fOrientation == B_VERTICAL && targetBounds.top != newValue) 778 fTarget->ScrollBy(0.0, newValue - targetBounds.top); 779 780 // if horizontal, check bounds left and scroll if different from newValue 781 if (fOrientation == B_HORIZONTAL && targetBounds.left != newValue) 782 fTarget->ScrollBy(newValue - targetBounds.left, 0.0); 783 } 784 785 TRACE(" -> %.1f\n", newValue); 786 787 SetValue(newValue); 788 } 789 790 791 void 792 BScrollBar::SetProportion(float value) 793 { 794 if (value < 0.0f) 795 value = 0.0f; 796 else if (value > 1.0f) 797 value = 1.0f; 798 799 if (value == fProportion) 800 return; 801 802 TRACE("BScrollBar(%s)::SetProportion(%.1f)\n", Name(), value); 803 804 bool oldEnabled = fPrivateData->fEnabled && fMin < fMax 805 && fProportion < 1.0f && fProportion >= 0.0f; 806 807 fProportion = value; 808 809 bool newEnabled = fPrivateData->fEnabled && fMin < fMax 810 && fProportion < 1.0f && fProportion >= 0.0f; 811 812 _UpdateThumbFrame(); 813 814 if (oldEnabled != newEnabled) 815 Invalidate(); 816 } 817 818 819 float 820 BScrollBar::Proportion() const 821 { 822 return fProportion; 823 } 824 825 826 void 827 BScrollBar::SetRange(float min, float max) 828 { 829 if (min > max || isnanf(min) || isnanf(max) 830 || isinff(min) || isinff(max)) { 831 min = 0.0f; 832 max = 0.0f; 833 } 834 835 min = roundf(min); 836 max = roundf(max); 837 838 if (fMin == min && fMax == max) 839 return; 840 841 TRACE("BScrollBar(%s)::SetRange(min=%.1f, max=%.1f)\n", Name(), min, max); 842 843 fMin = min; 844 fMax = max; 845 846 if (fValue < fMin || fValue > fMax) 847 SetValue(fValue); 848 else { 849 _UpdateThumbFrame(); 850 Invalidate(); 851 } 852 } 853 854 855 void 856 BScrollBar::GetRange(float* min, float* max) const 857 { 858 if (min != NULL) 859 *min = fMin; 860 861 if (max != NULL) 862 *max = fMax; 863 } 864 865 866 void 867 BScrollBar::SetSteps(float smallStep, float largeStep) 868 { 869 // Under R5, steps can be set only after being attached to a window, 870 // probably because the data is kept server-side. We'll just remove 871 // that limitation... :P 872 873 // The BeBook also says that we need to specify an integer value even 874 // though the step values are floats. For the moment, we'll just make 875 // sure that they are integers 876 smallStep = roundf(smallStep); 877 largeStep = roundf(largeStep); 878 if (fSmallStep == smallStep && fLargeStep == largeStep) 879 return; 880 881 TRACE("BScrollBar(%s)::SetSteps(small=%.1f, large=%.1f)\n", Name(), 882 smallStep, largeStep); 883 884 fSmallStep = smallStep; 885 fLargeStep = largeStep; 886 887 if (fProportion == 0.0) { 888 // special case, proportion is based on fLargeStep if it was never 889 // set, so it means we need to invalidate here 890 _UpdateThumbFrame(); 891 Invalidate(); 892 } 893 894 // TODO: test use of fractional values and make them work properly if 895 // they don't 896 } 897 898 899 void 900 BScrollBar::GetSteps(float* smallStep, float* largeStep) const 901 { 902 if (smallStep != NULL) 903 *smallStep = fSmallStep; 904 905 if (largeStep != NULL) 906 *largeStep = fLargeStep; 907 } 908 909 910 void 911 BScrollBar::SetTarget(BView* target) 912 { 913 if (fTarget) { 914 // unset the previous target's scrollbar pointer 915 if (fOrientation == B_VERTICAL) 916 fTarget->fVerScroller = NULL; 917 else 918 fTarget->fHorScroller = NULL; 919 } 920 921 fTarget = target; 922 if (fTarget) { 923 if (fOrientation == B_VERTICAL) 924 fTarget->fVerScroller = this; 925 else 926 fTarget->fHorScroller = this; 927 } 928 } 929 930 931 void 932 BScrollBar::SetTarget(const char* targetName) 933 { 934 // NOTE 1: BeOS implementation crashes for targetName == NULL 935 // NOTE 2: BeOS implementation also does not modify the target 936 // if it can't be found 937 if (targetName == NULL) 938 return; 939 940 if (Window() == NULL) 941 debugger("Method requires window and doesn't have one"); 942 943 BView* target = Window()->FindView(targetName); 944 if (target != NULL) 945 SetTarget(target); 946 } 947 948 949 BView* 950 BScrollBar::Target() const 951 { 952 return fTarget; 953 } 954 955 956 void 957 BScrollBar::SetOrientation(orientation direction) 958 { 959 if (fOrientation == direction) 960 return; 961 962 fOrientation = direction; 963 InvalidateLayout(); 964 Invalidate(); 965 } 966 967 968 orientation 969 BScrollBar::Orientation() const 970 { 971 return fOrientation; 972 } 973 974 975 status_t 976 BScrollBar::SetBorderHighlighted(bool highlight) 977 { 978 if (fPrivateData->fBorderHighlighted == highlight) 979 return B_OK; 980 981 fPrivateData->fBorderHighlighted = highlight; 982 983 BRect dirty(Bounds()); 984 if (fOrientation == B_HORIZONTAL) 985 dirty.bottom = dirty.top; 986 else 987 dirty.right = dirty.left; 988 989 Invalidate(dirty); 990 991 return B_OK; 992 } 993 994 995 void 996 BScrollBar::GetPreferredSize(float* _width, float* _height) 997 { 998 const float scale = std::max(be_plain_font->Size() / 12.0f, 1.0f); 999 if (fOrientation == B_VERTICAL) { 1000 if (_width) 1001 *_width = B_V_SCROLL_BAR_WIDTH * scale; 1002 1003 if (_height) 1004 *_height = _MinSize().Height(); 1005 } else if (fOrientation == B_HORIZONTAL) { 1006 if (_width) 1007 *_width = _MinSize().Width(); 1008 1009 if (_height) 1010 *_height = B_H_SCROLL_BAR_HEIGHT * scale; 1011 } 1012 } 1013 1014 1015 void 1016 BScrollBar::ResizeToPreferred() 1017 { 1018 BView::ResizeToPreferred(); 1019 } 1020 1021 1022 1023 void 1024 BScrollBar::MakeFocus(bool focus) 1025 { 1026 BView::MakeFocus(focus); 1027 } 1028 1029 1030 BSize 1031 BScrollBar::MinSize() 1032 { 1033 return BLayoutUtils::ComposeSize(ExplicitMinSize(), _MinSize()); 1034 } 1035 1036 1037 BSize 1038 BScrollBar::MaxSize() 1039 { 1040 BSize maxSize; 1041 GetPreferredSize(&maxSize.width, &maxSize.height); 1042 if (fOrientation == B_HORIZONTAL) 1043 maxSize.width = B_SIZE_UNLIMITED; 1044 else 1045 maxSize.height = B_SIZE_UNLIMITED; 1046 return BLayoutUtils::ComposeSize(ExplicitMaxSize(), maxSize); 1047 } 1048 1049 1050 BSize 1051 BScrollBar::PreferredSize() 1052 { 1053 BSize preferredSize; 1054 GetPreferredSize(&preferredSize.width, &preferredSize.height); 1055 return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), preferredSize); 1056 } 1057 1058 1059 status_t 1060 BScrollBar::GetSupportedSuites(BMessage* message) 1061 { 1062 return BView::GetSupportedSuites(message); 1063 } 1064 1065 1066 BHandler* 1067 BScrollBar::ResolveSpecifier(BMessage* message, int32 index, 1068 BMessage* specifier, int32 what, const char* property) 1069 { 1070 return BView::ResolveSpecifier(message, index, specifier, what, property); 1071 } 1072 1073 1074 status_t 1075 BScrollBar::Perform(perform_code code, void* _data) 1076 { 1077 switch (code) { 1078 case PERFORM_CODE_MIN_SIZE: 1079 ((perform_data_min_size*)_data)->return_value 1080 = BScrollBar::MinSize(); 1081 1082 return B_OK; 1083 1084 case PERFORM_CODE_MAX_SIZE: 1085 ((perform_data_max_size*)_data)->return_value 1086 = BScrollBar::MaxSize(); 1087 1088 return B_OK; 1089 1090 case PERFORM_CODE_PREFERRED_SIZE: 1091 ((perform_data_preferred_size*)_data)->return_value 1092 = BScrollBar::PreferredSize(); 1093 1094 return B_OK; 1095 1096 case PERFORM_CODE_LAYOUT_ALIGNMENT: 1097 ((perform_data_layout_alignment*)_data)->return_value 1098 = BScrollBar::LayoutAlignment(); 1099 1100 return B_OK; 1101 1102 case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH: 1103 ((perform_data_has_height_for_width*)_data)->return_value 1104 = BScrollBar::HasHeightForWidth(); 1105 1106 return B_OK; 1107 1108 case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH: 1109 { 1110 perform_data_get_height_for_width* data 1111 = (perform_data_get_height_for_width*)_data; 1112 BScrollBar::GetHeightForWidth(data->width, &data->min, &data->max, 1113 &data->preferred); 1114 1115 return B_OK; 1116 } 1117 1118 case PERFORM_CODE_SET_LAYOUT: 1119 { 1120 perform_data_set_layout* data = (perform_data_set_layout*)_data; 1121 BScrollBar::SetLayout(data->layout); 1122 1123 return B_OK; 1124 } 1125 1126 case PERFORM_CODE_LAYOUT_INVALIDATED: 1127 { 1128 perform_data_layout_invalidated* data 1129 = (perform_data_layout_invalidated*)_data; 1130 BScrollBar::LayoutInvalidated(data->descendants); 1131 1132 return B_OK; 1133 } 1134 1135 case PERFORM_CODE_DO_LAYOUT: 1136 { 1137 BScrollBar::DoLayout(); 1138 1139 return B_OK; 1140 } 1141 } 1142 1143 return BView::Perform(code, _data); 1144 } 1145 1146 1147 void BScrollBar::_ReservedScrollBar1() {} 1148 void BScrollBar::_ReservedScrollBar2() {} 1149 void BScrollBar::_ReservedScrollBar3() {} 1150 void BScrollBar::_ReservedScrollBar4() {} 1151 1152 1153 BScrollBar& 1154 BScrollBar::operator=(const BScrollBar&) 1155 { 1156 return *this; 1157 } 1158 1159 1160 bool 1161 BScrollBar::_DoubleArrows() const 1162 { 1163 if (!fPrivateData->fScrollBarInfo.double_arrows) 1164 return false; 1165 1166 // if there is not enough room, switch to single arrows even though 1167 // double arrows is specified 1168 if (fOrientation == B_HORIZONTAL) { 1169 return Bounds().Width() > (Bounds().Height() + 1) * 4 1170 + fPrivateData->fScrollBarInfo.min_knob_size * 2; 1171 } else { 1172 return Bounds().Height() > (Bounds().Width() + 1) * 4 1173 + fPrivateData->fScrollBarInfo.min_knob_size * 2; 1174 } 1175 } 1176 1177 1178 void 1179 BScrollBar::_UpdateThumbFrame() 1180 { 1181 BRect bounds = Bounds(); 1182 bounds.InsetBy(1.0, 1.0); 1183 1184 BRect oldFrame = fPrivateData->fThumbFrame; 1185 fPrivateData->fThumbFrame = bounds; 1186 float minSize = fPrivateData->fScrollBarInfo.min_knob_size; 1187 float maxSize; 1188 float buttonSize; 1189 1190 // assume square buttons 1191 if (fOrientation == B_VERTICAL) { 1192 maxSize = bounds.Height(); 1193 buttonSize = bounds.Width() + 1.0; 1194 } else { 1195 maxSize = bounds.Width(); 1196 buttonSize = bounds.Height() + 1.0; 1197 } 1198 1199 if (_DoubleArrows()) { 1200 // subtract the size of four buttons 1201 maxSize -= buttonSize * 4; 1202 } else { 1203 // subtract the size of two buttons 1204 maxSize -= buttonSize * 2; 1205 } 1206 // visual adjustments (room for darker line between thumb and buttons) 1207 maxSize--; 1208 1209 float thumbSize; 1210 if (fPrivateData->fScrollBarInfo.proportional) { 1211 float proportion = fProportion; 1212 if (fMin >= fMax || proportion > 1.0 || proportion < 0.0) 1213 proportion = 1.0; 1214 1215 if (proportion == 0.0) { 1216 // Special case a proportion of 0.0, use the large step value 1217 // in that case (NOTE: fMin == fMax already handled above) 1218 // This calculation is based on the assumption that "large step" 1219 // scrolls by one "page size". 1220 proportion = fLargeStep / (2 * (fMax - fMin)); 1221 if (proportion > 1.0) 1222 proportion = 1.0; 1223 } 1224 thumbSize = maxSize * proportion; 1225 if (thumbSize < minSize) 1226 thumbSize = minSize; 1227 } else 1228 thumbSize = minSize; 1229 1230 thumbSize = floorf(thumbSize + 0.5); 1231 thumbSize--; 1232 1233 // the thumb can be scrolled within the remaining area "maxSize - thumbSize - 1.0" 1234 float offset = 0.0; 1235 if (fMax > fMin) { 1236 offset = floorf(((fValue - fMin) / (fMax - fMin)) 1237 * (maxSize - thumbSize - 1.0)); 1238 } 1239 1240 if (_DoubleArrows()) { 1241 offset += buttonSize * 2; 1242 } else 1243 offset += buttonSize; 1244 1245 // visual adjustments (room for darker line between thumb and buttons) 1246 offset++; 1247 1248 if (fOrientation == B_VERTICAL) { 1249 fPrivateData->fThumbFrame.bottom = fPrivateData->fThumbFrame.top 1250 + thumbSize; 1251 fPrivateData->fThumbFrame.OffsetBy(0.0, offset); 1252 } else { 1253 fPrivateData->fThumbFrame.right = fPrivateData->fThumbFrame.left 1254 + thumbSize; 1255 fPrivateData->fThumbFrame.OffsetBy(offset, 0.0); 1256 } 1257 1258 if (Window() != NULL) { 1259 BRect invalid = oldFrame.IsValid() 1260 ? oldFrame | fPrivateData->fThumbFrame 1261 : fPrivateData->fThumbFrame; 1262 // account for those two dark lines 1263 if (fOrientation == B_HORIZONTAL) 1264 invalid.InsetBy(-2.0, 0.0); 1265 else 1266 invalid.InsetBy(0.0, -2.0); 1267 1268 Invalidate(invalid); 1269 } 1270 } 1271 1272 1273 float 1274 BScrollBar::_ValueFor(BPoint where) const 1275 { 1276 BRect bounds = Bounds(); 1277 bounds.InsetBy(1.0f, 1.0f); 1278 1279 float offset; 1280 float thumbSize; 1281 float maxSize; 1282 float buttonSize; 1283 1284 if (fOrientation == B_VERTICAL) { 1285 offset = where.y; 1286 thumbSize = fPrivateData->fThumbFrame.Height(); 1287 maxSize = bounds.Height(); 1288 buttonSize = bounds.Width() + 1.0f; 1289 } else { 1290 offset = where.x; 1291 thumbSize = fPrivateData->fThumbFrame.Width(); 1292 maxSize = bounds.Width(); 1293 buttonSize = bounds.Height() + 1.0f; 1294 } 1295 1296 if (_DoubleArrows()) { 1297 // subtract the size of four buttons 1298 maxSize -= buttonSize * 4; 1299 // convert point to inside of area between buttons 1300 offset -= buttonSize * 2; 1301 } else { 1302 // subtract the size of two buttons 1303 maxSize -= buttonSize * 2; 1304 // convert point to inside of area between buttons 1305 offset -= buttonSize; 1306 } 1307 // visual adjustments (room for darker line between thumb and buttons) 1308 maxSize--; 1309 offset++; 1310 1311 return roundf(fMin + (offset / (maxSize - thumbSize) 1312 * (fMax - fMin + 1.0f))); 1313 } 1314 1315 1316 int32 1317 BScrollBar::_ButtonFor(BPoint where) const 1318 { 1319 BRect bounds = Bounds(); 1320 bounds.InsetBy(1.0f, 1.0f); 1321 1322 float buttonSize = fOrientation == B_VERTICAL 1323 ? bounds.Width() + 1.0f 1324 : bounds.Height() + 1.0f; 1325 1326 BRect rect(bounds.left, bounds.top, 1327 bounds.left + buttonSize, bounds.top + buttonSize); 1328 1329 if (fOrientation == B_VERTICAL) { 1330 if (rect.Contains(where)) 1331 return ARROW1; 1332 1333 if (_DoubleArrows()) { 1334 rect.OffsetBy(0.0, buttonSize); 1335 if (rect.Contains(where)) 1336 return ARROW2; 1337 1338 rect.OffsetTo(bounds.left, bounds.bottom - 2 * buttonSize); 1339 if (rect.Contains(where)) 1340 return ARROW3; 1341 } 1342 rect.OffsetTo(bounds.left, bounds.bottom - buttonSize); 1343 if (rect.Contains(where)) 1344 return ARROW4; 1345 } else { 1346 if (rect.Contains(where)) 1347 return ARROW1; 1348 1349 if (_DoubleArrows()) { 1350 rect.OffsetBy(buttonSize, 0.0); 1351 if (rect.Contains(where)) 1352 return ARROW2; 1353 1354 rect.OffsetTo(bounds.right - 2 * buttonSize, bounds.top); 1355 if (rect.Contains(where)) 1356 return ARROW3; 1357 } 1358 rect.OffsetTo(bounds.right - buttonSize, bounds.top); 1359 if (rect.Contains(where)) 1360 return ARROW4; 1361 } 1362 1363 return NOARROW; 1364 } 1365 1366 1367 BRect 1368 BScrollBar::_ButtonRectFor(int32 button) const 1369 { 1370 BRect bounds = Bounds(); 1371 bounds.InsetBy(1.0f, 1.0f); 1372 1373 float buttonSize = fOrientation == B_VERTICAL 1374 ? bounds.Width() + 1.0f 1375 : bounds.Height() + 1.0f; 1376 1377 BRect rect(bounds.left, bounds.top, 1378 bounds.left + buttonSize - 1.0f, bounds.top + buttonSize - 1.0f); 1379 1380 if (fOrientation == B_VERTICAL) { 1381 switch (button) { 1382 case ARROW1: 1383 break; 1384 1385 case ARROW2: 1386 rect.OffsetBy(0.0, buttonSize); 1387 break; 1388 1389 case ARROW3: 1390 rect.OffsetTo(bounds.left, bounds.bottom - 2 * buttonSize + 1); 1391 break; 1392 1393 case ARROW4: 1394 rect.OffsetTo(bounds.left, bounds.bottom - buttonSize + 1); 1395 break; 1396 } 1397 } else { 1398 switch (button) { 1399 case ARROW1: 1400 break; 1401 1402 case ARROW2: 1403 rect.OffsetBy(buttonSize, 0.0); 1404 break; 1405 1406 case ARROW3: 1407 rect.OffsetTo(bounds.right - 2 * buttonSize + 1, bounds.top); 1408 break; 1409 1410 case ARROW4: 1411 rect.OffsetTo(bounds.right - buttonSize + 1, bounds.top); 1412 break; 1413 } 1414 } 1415 1416 return rect; 1417 } 1418 1419 1420 void 1421 BScrollBar::_UpdateTargetValue(BPoint where) 1422 { 1423 if (fOrientation == B_VERTICAL) { 1424 fPrivateData->fStopValue = _ValueFor(BPoint(where.x, where.y 1425 - fPrivateData->fThumbFrame.Height() / 2.0)); 1426 } else { 1427 fPrivateData->fStopValue = _ValueFor(BPoint(where.x 1428 - fPrivateData->fThumbFrame.Width() / 2.0, where.y)); 1429 } 1430 } 1431 1432 1433 void 1434 BScrollBar::_UpdateArrowButtons() 1435 { 1436 bool upEnabled = fValue > fMin; 1437 if (fPrivateData->fUpArrowsEnabled != upEnabled) { 1438 fPrivateData->fUpArrowsEnabled = upEnabled; 1439 Invalidate(_ButtonRectFor(ARROW1)); 1440 if (_DoubleArrows()) 1441 Invalidate(_ButtonRectFor(ARROW3)); 1442 } 1443 1444 bool downEnabled = fValue < fMax; 1445 if (fPrivateData->fDownArrowsEnabled != downEnabled) { 1446 fPrivateData->fDownArrowsEnabled = downEnabled; 1447 Invalidate(_ButtonRectFor(ARROW4)); 1448 if (_DoubleArrows()) 1449 Invalidate(_ButtonRectFor(ARROW2)); 1450 } 1451 } 1452 1453 1454 status_t 1455 control_scrollbar(scroll_bar_info* info, BScrollBar* bar) 1456 { 1457 if (bar == NULL || info == NULL) 1458 return B_BAD_VALUE; 1459 1460 if (bar->fPrivateData->fScrollBarInfo.double_arrows 1461 != info->double_arrows) { 1462 bar->fPrivateData->fScrollBarInfo.double_arrows = info->double_arrows; 1463 1464 int8 multiplier = (info->double_arrows) ? 1 : -1; 1465 1466 if (bar->fOrientation == B_VERTICAL) { 1467 bar->fPrivateData->fThumbFrame.OffsetBy(0, multiplier 1468 * B_H_SCROLL_BAR_HEIGHT); 1469 } else { 1470 bar->fPrivateData->fThumbFrame.OffsetBy(multiplier 1471 * B_V_SCROLL_BAR_WIDTH, 0); 1472 } 1473 } 1474 1475 bar->fPrivateData->fScrollBarInfo.proportional = info->proportional; 1476 1477 // TODO: Figure out how proportional relates to the size of the thumb 1478 1479 // TODO: Add redraw code to reflect the changes 1480 1481 if (info->knob >= 0 && info->knob <= 2) 1482 bar->fPrivateData->fScrollBarInfo.knob = info->knob; 1483 else 1484 return B_BAD_VALUE; 1485 1486 if (info->min_knob_size >= SCROLL_BAR_MINIMUM_KNOB_SIZE 1487 && info->min_knob_size <= SCROLL_BAR_MAXIMUM_KNOB_SIZE) { 1488 bar->fPrivateData->fScrollBarInfo.min_knob_size = info->min_knob_size; 1489 } else 1490 return B_BAD_VALUE; 1491 1492 return B_OK; 1493 } 1494 1495 1496 BSize 1497 BScrollBar::_MinSize() const 1498 { 1499 BSize minSize; 1500 if (fOrientation == B_HORIZONTAL) { 1501 minSize.width = 2 * B_V_SCROLL_BAR_WIDTH 1502 + 2 * fPrivateData->fScrollBarInfo.min_knob_size; 1503 minSize.height = B_H_SCROLL_BAR_HEIGHT; 1504 } else { 1505 minSize.width = B_V_SCROLL_BAR_WIDTH; 1506 minSize.height = 2 * B_H_SCROLL_BAR_HEIGHT 1507 + 2 * fPrivateData->fScrollBarInfo.min_knob_size; 1508 } 1509 1510 return minSize; 1511 } 1512