1 /* 2 Open Tracker License 3 4 Terms and Conditions 5 6 Copyright (c) 1991-2000, Be Incorporated. All rights reserved. 7 8 Permission is hereby granted, free of charge, to any person obtaining a copy of 9 this software and associated documentation files (the "Software"), to deal in 10 the Software without restriction, including without limitation the rights to 11 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 12 of the Software, and to permit persons to whom the Software is furnished to do 13 so, subject to the following conditions: 14 15 The above copyright notice and this permission notice applies to all licensees 16 and shall be included in all copies or substantial portions of the Software. 17 18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY, 20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 21 BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 22 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION 23 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 25 Except as contained in this notice, the name of Be Incorporated shall not be 26 used in advertising or otherwise to promote the sale, use or other dealings in 27 this Software without prior written authorization from Be Incorporated. 28 29 Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or registered trademarks 30 of Be Incorporated in the United States and other countries. Other brand product 31 names are registered trademarks or trademarks of their respective holders. 32 All rights reserved. 33 */ 34 35 36 #include "TextWidget.h" 37 38 #include <string.h> 39 #include <stdlib.h> 40 41 #include <Alert.h> 42 #include <Catalog.h> 43 #include <Clipboard.h> 44 #include <Debug.h> 45 #include <Directory.h> 46 #include <MessageFilter.h> 47 #include <ScrollView.h> 48 #include <TextView.h> 49 #include <Volume.h> 50 #include <Window.h> 51 52 #include "Attributes.h" 53 #include "ContainerWindow.h" 54 #include "Commands.h" 55 #include "FSUtils.h" 56 #include "PoseView.h" 57 #include "Utilities.h" 58 59 60 #undef B_TRANSLATION_CONTEXT 61 #define B_TRANSLATION_CONTEXT "TextWidget" 62 63 64 const float kWidthMargin = 20; 65 66 67 // #pragma mark - BTextWidget 68 69 70 BTextWidget::BTextWidget(Model* model, BColumn* column, BPoseView* view) 71 : 72 fText(WidgetAttributeText::NewWidgetText(model, column, view)), 73 fAttrHash(column->AttrHash()), 74 fAlignment(column->Alignment()), 75 fEditable(column->Editable()), 76 fVisible(true), 77 fActive(false), 78 fSymLink(model->IsSymLink()), 79 fMaxWidth(0), 80 fLastClickedTime(0) 81 { 82 } 83 84 85 BTextWidget::~BTextWidget() 86 { 87 if (fLastClickedTime != 0) 88 fParams.poseView->SetTextWidgetToCheck(NULL, this); 89 90 delete fText; 91 } 92 93 94 int 95 BTextWidget::Compare(const BTextWidget& with, BPoseView* view) const 96 { 97 return fText->Compare(*with.fText, view); 98 } 99 100 101 const char* 102 BTextWidget::Text(const BPoseView* view) const 103 { 104 StringAttributeText* textAttribute 105 = dynamic_cast<StringAttributeText*>(fText); 106 if (textAttribute == NULL) 107 return NULL; 108 109 return textAttribute->ValueAsText(view); 110 } 111 112 113 float 114 BTextWidget::TextWidth(const BPoseView* pose) const 115 { 116 return fText->Width(pose); 117 } 118 119 120 float 121 BTextWidget::PreferredWidth(const BPoseView* pose) const 122 { 123 return fText->PreferredWidth(pose) + 1; 124 } 125 126 127 BRect 128 BTextWidget::ColumnRect(BPoint poseLoc, const BColumn* column, 129 const BPoseView* view) 130 { 131 if (view->ViewMode() != kListMode) { 132 // ColumnRect only makes sense in list view, return 133 // CalcRect otherwise 134 return CalcRect(poseLoc, column, view); 135 } 136 BRect result; 137 result.left = column->Offset() + poseLoc.x; 138 result.right = result.left + column->Width(); 139 result.bottom = poseLoc.y 140 + roundf((view->ListElemHeight() + view->FontHeight()) / 2); 141 result.top = result.bottom - view->FontHeight(); 142 return result; 143 } 144 145 146 BRect 147 BTextWidget::CalcRectCommon(BPoint poseLoc, const BColumn* column, 148 const BPoseView* view, float textWidth) 149 { 150 BRect result; 151 float viewWidth = textWidth; 152 153 if (view->ViewMode() == kListMode) { 154 viewWidth = std::min(column->Width(), textWidth); 155 156 poseLoc.x += column->Offset(); 157 158 switch (fAlignment) { 159 case B_ALIGN_LEFT: 160 result.left = poseLoc.x; 161 result.right = result.left + 1 + viewWidth; 162 break; 163 164 case B_ALIGN_CENTER: 165 result.left = poseLoc.x 166 + roundf((column->Width() - viewWidth) / 2); 167 if (result.left < 0) 168 result.left = 0; 169 170 result.right = result.left + 1 + viewWidth; 171 break; 172 173 case B_ALIGN_RIGHT: 174 result.right = poseLoc.x + column->Width(); 175 result.left = result.right - 1 - viewWidth; 176 if (result.left < 0) 177 result.left = 0; 178 break; 179 180 default: 181 TRESPASS(); 182 break; 183 } 184 185 result.bottom = poseLoc.y 186 + roundf((view->ListElemHeight() + view->FontHeight()) / 2); 187 } else { 188 viewWidth = std::min(view->StringWidth("M") * 30, textWidth); 189 if (view->ViewMode() == kIconMode) { 190 // icon mode 191 result.left = poseLoc.x 192 + roundf((view->IconSizeInt() - viewWidth) / 2); 193 result.bottom = poseLoc.y + view->IconPoseHeight(); 194 } else { 195 // mini icon mode 196 result.left = poseLoc.x + B_MINI_ICON + kMiniIconSeparator; 197 result.bottom = poseLoc.y 198 + roundf((B_MINI_ICON + view->FontHeight()) / 2); 199 } 200 201 result.right = result.left + viewWidth; 202 } 203 204 result.top = result.bottom - view->FontHeight(); 205 206 return result; 207 } 208 209 210 BRect 211 BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column, 212 const BPoseView* view) 213 { 214 return CalcRectCommon(poseLoc, column, view, fText->Width(view)); 215 } 216 217 218 BRect 219 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column, 220 const BPoseView* view) 221 { 222 return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth()); 223 } 224 225 226 BRect 227 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column, 228 const BPoseView* view) 229 { 230 BRect result = CalcRect(poseLoc, column, view); 231 if (result.Width() < kWidthMargin) { 232 // if resulting rect too narrow, make it a bit wider 233 // for comfortable clicking 234 if (column != NULL && column->Width() < kWidthMargin) 235 result.right = result.left + column->Width(); 236 else 237 result.right = result.left + kWidthMargin; 238 } 239 240 return result; 241 } 242 243 244 void 245 BTextWidget::CheckExpiration() 246 { 247 if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) { 248 bigtime_t doubleClickSpeed; 249 get_click_speed(&doubleClickSpeed); 250 251 bigtime_t delta = system_time() - fLastClickedTime; 252 253 if (delta > doubleClickSpeed) { 254 // at least 'doubleClickSpeed' microseconds ellapsed and no click 255 // was registered since. 256 fLastClickedTime = 0; 257 StartEdit(fParams.bounds, fParams.poseView, fParams.pose); 258 } 259 } else { 260 fLastClickedTime = 0; 261 fParams.poseView->SetTextWidgetToCheck(NULL); 262 } 263 } 264 265 266 void 267 BTextWidget::CancelWait() 268 { 269 fLastClickedTime = 0; 270 fParams.poseView->SetTextWidgetToCheck(NULL); 271 } 272 273 274 void 275 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint) 276 { 277 // Register the time of that click. The PoseView, through its Pulse() 278 // will allow us to StartEdit() if no other click have been registered since 279 // then. 280 281 // TODO: re-enable modifiers, one should be enough 282 view->SetTextWidgetToCheck(NULL); 283 if (IsEditable() && pose->IsSelected()) { 284 bigtime_t doubleClickSpeed; 285 get_click_speed(&doubleClickSpeed); 286 287 if (fLastClickedTime == 0) { 288 fLastClickedTime = system_time(); 289 if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime()) 290 fLastClickedTime = 0; 291 } else 292 fLastClickedTime = 0; 293 294 if (fLastClickedTime == 0) 295 return; 296 297 view->SetTextWidgetToCheck(this); 298 299 fParams.pose = pose; 300 fParams.bounds = bounds; 301 fParams.poseView = view; 302 } else 303 fLastClickedTime = 0; 304 } 305 306 307 static filter_result 308 TextViewKeyDownFilter(BMessage* message, BHandler**, BMessageFilter* filter) 309 { 310 uchar key; 311 if (message->FindInt8("byte", (int8*)&key) != B_OK) 312 return B_DISPATCH_MESSAGE; 313 314 ThrowOnAssert(filter != NULL); 315 316 BContainerWindow* window = dynamic_cast<BContainerWindow*>( 317 filter->Looper()); 318 ThrowOnAssert(window != NULL); 319 320 BPoseView* view = window->PoseView(); 321 ThrowOnAssert(view != NULL); 322 323 if (key == B_RETURN || key == B_ESCAPE) { 324 view->CommitActivePose(key == B_RETURN); 325 return B_SKIP_MESSAGE; 326 } 327 328 if (key == B_TAB) { 329 if (view->ActivePose()) { 330 if (message->FindInt32("modifiers") & B_SHIFT_KEY) 331 view->ActivePose()->EditPreviousWidget(view); 332 else 333 view->ActivePose()->EditNextWidget(view); 334 } 335 336 return B_SKIP_MESSAGE; 337 } 338 339 // the BTextView doesn't respect window borders when resizing itself; 340 // we try to work-around this "bug" here. 341 342 // find the text editing view 343 BView* scrollView = view->FindView("BorderView"); 344 if (scrollView != NULL) { 345 BTextView* textView = dynamic_cast<BTextView*>( 346 scrollView->FindView("WidgetTextView")); 347 if (textView != NULL) { 348 ASSERT(view->ActiveTextWidget() != NULL); 349 float maxWidth = view->ActiveTextWidget()->MaxWidth(); 350 bool tooWide = textView->TextRect().Width() > maxWidth; 351 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 352 } 353 } 354 355 return B_DISPATCH_MESSAGE; 356 } 357 358 359 static filter_result 360 TextViewPasteFilter(BMessage* message, BHandler**, BMessageFilter* filter) 361 { 362 ThrowOnAssert(filter != NULL); 363 364 BContainerWindow* window = dynamic_cast<BContainerWindow*>( 365 filter->Looper()); 366 ThrowOnAssert(window != NULL); 367 368 BPoseView* view = window->PoseView(); 369 ThrowOnAssert(view != NULL); 370 371 // the BTextView doesn't respect window borders when resizing itself; 372 // we try to work-around this "bug" here. 373 374 // find the text editing view 375 BView* scrollView = view->FindView("BorderView"); 376 if (scrollView != NULL) { 377 BTextView* textView = dynamic_cast<BTextView*>( 378 scrollView->FindView("WidgetTextView")); 379 if (textView != NULL) { 380 float textWidth = textView->TextRect().Width(); 381 382 // subtract out selected text region width 383 int32 start, finish; 384 textView->GetSelection(&start, &finish); 385 if (start != finish) { 386 BRegion selectedRegion; 387 textView->GetTextRegion(start, finish, &selectedRegion); 388 textWidth -= selectedRegion.Frame().Width(); 389 } 390 391 // add pasted text width 392 if (be_clipboard->Lock()) { 393 BMessage* clip = be_clipboard->Data(); 394 if (clip != NULL) { 395 const char* text = NULL; 396 ssize_t length = 0; 397 398 if (clip->FindData("text/plain", B_MIME_TYPE, 399 (const void**)&text, &length) == B_OK) { 400 textWidth += textView->StringWidth(text); 401 } 402 } 403 404 be_clipboard->Unlock(); 405 } 406 407 // check if pasted text is too wide 408 ASSERT(view->ActiveTextWidget() != NULL); 409 float maxWidth = view->ActiveTextWidget()->MaxWidth(); 410 bool tooWide = textWidth > maxWidth; 411 412 if (tooWide) { 413 // resize text view to max width 414 415 // move scroll view if not left aligned 416 float oldWidth = textView->Bounds().Width(); 417 float newWidth = maxWidth; 418 float right = oldWidth - newWidth; 419 420 if (textView->Alignment() == B_ALIGN_CENTER) 421 scrollView->MoveBy(roundf(right / 2), 0); 422 else if (textView->Alignment() == B_ALIGN_RIGHT) 423 scrollView->MoveBy(right, 0); 424 425 // resize scroll view 426 float grow = newWidth - oldWidth; 427 scrollView->ResizeBy(grow, 0); 428 } 429 430 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 431 } 432 } 433 434 return B_DISPATCH_MESSAGE; 435 } 436 437 438 void 439 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose) 440 { 441 view->SetTextWidgetToCheck(NULL, this); 442 if (!IsEditable() || IsActive()) 443 return; 444 445 BEntry entry(pose->TargetModel()->EntryRef()); 446 if (entry.InitCheck() == B_OK 447 && !ConfirmChangeIfWellKnownDirectory(&entry, kRename)) { 448 return; 449 } 450 451 view->SetActiveTextWidget(this); 452 453 // TODO fix text rect being off by a pixel on some files 454 455 BRect rect(bounds); 456 rect.OffsetBy(view->ViewMode() == kListMode ? -1 : 1, -4); 457 BTextView* textView = new BTextView(rect, "WidgetTextView", rect, 458 be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW); 459 460 textView->SetWordWrap(false); 461 textView->SetInsets(2, 2, 2, 2); 462 DisallowMetaKeys(textView); 463 fText->SetUpEditing(textView); 464 465 textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter)); 466 textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter)); 467 468 // get full text length 469 rect.right = rect.left + textView->LineWidth(); 470 rect.bottom = rect.top + textView->LineHeight() - 1 + 4; 471 472 if (view->ViewMode() == kListMode) { 473 // limit max width to column width in list mode 474 BColumn* column = view->ColumnFor(fAttrHash); 475 ASSERT(column != NULL); 476 fMaxWidth = column->Width(); 477 } else { 478 // limit max width to 30em in icon and mini icon mode 479 fMaxWidth = textView->StringWidth("M") * 30; 480 481 if (textView->LineWidth() > fMaxWidth) { 482 // compensate for text going over right inset 483 rect.OffsetBy(-2, 0); 484 } 485 } 486 487 // resize textView 488 textView->MoveTo(rect.LeftTop()); 489 textView->ResizeTo(std::min(fMaxWidth, rect.Width()), rect.Height()); 490 textView->SetTextRect(rect); 491 492 // set alignment before adding textView so it doesn't redraw 493 switch (view->ViewMode()) { 494 case kIconMode: 495 textView->SetAlignment(B_ALIGN_CENTER); 496 break; 497 498 case kMiniIconMode: 499 textView->SetAlignment(B_ALIGN_LEFT); 500 break; 501 502 case kListMode: 503 textView->SetAlignment(fAlignment); 504 break; 505 } 506 507 BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0, 508 false, false, B_PLAIN_BORDER); 509 view->AddChild(scrollView); 510 511 bool tooWide = textView->TextRect().Width() > fMaxWidth; 512 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 513 514 view->SetActivePose(pose); 515 // tell view about pose 516 SetActive(true); 517 // for widget 518 519 textView->SelectAll(); 520 textView->ScrollToSelection(); 521 // scroll to beginning so that text is visible 522 textView->MakeFocus(); 523 524 // make this text widget invisible while we edit it 525 SetVisible(false); 526 527 ASSERT(view->Window() != NULL); 528 // how can I not have a Window here??? 529 530 if (view->Window()) { 531 // force immediate redraw so TextView appears instantly 532 view->Window()->UpdateIfNeeded(); 533 } 534 } 535 536 537 void 538 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view, 539 BPose* pose, int32 poseIndex) 540 { 541 view->SetActiveTextWidget(NULL); 542 543 // find the text editing view 544 BView* scrollView = view->FindView("BorderView"); 545 ASSERT(scrollView != NULL); 546 if (scrollView == NULL) 547 return; 548 549 BTextView* textView = dynamic_cast<BTextView*>( 550 scrollView->FindView("WidgetTextView")); 551 ASSERT(textView != NULL); 552 if (textView == NULL) 553 return; 554 555 BColumn* column = view->ColumnFor(fAttrHash); 556 ASSERT(column != NULL); 557 if (column == NULL) 558 return; 559 560 if (saveChanges && fText->CommitEditedText(textView)) { 561 // we have an actual change, re-sort 562 view->CheckPoseSortOrder(pose, poseIndex); 563 } 564 565 // make text widget visible again 566 SetVisible(true); 567 view->Invalidate(ColumnRect(poseLoc, column, view)); 568 569 // force immediate redraw so TEView disappears 570 scrollView->RemoveSelf(); 571 delete scrollView; 572 573 ASSERT(view->Window() != NULL); 574 view->Window()->UpdateIfNeeded(); 575 view->MakeFocus(); 576 577 SetActive(false); 578 } 579 580 581 void 582 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column, 583 BPoseView* view, bool visible) 584 { 585 BRect oldRect; 586 if (view->ViewMode() != kListMode) 587 oldRect = CalcOldRect(loc, column, view); 588 589 if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view) 590 && visible) { 591 BRect invalRect(ColumnRect(loc, column, view)); 592 if (view->ViewMode() != kListMode) 593 invalRect = invalRect | oldRect; 594 view->Invalidate(invalRect); 595 } 596 } 597 598 599 void 600 BTextWidget::SelectAll(BPoseView* view) 601 { 602 BTextView* text = dynamic_cast<BTextView*>( 603 view->FindView("WidgetTextView")); 604 if (text != NULL) 605 text->SelectAll(); 606 } 607 608 609 void 610 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view, 611 BView* drawView, bool selected, uint32 clipboardMode, BPoint offset, 612 bool direct) 613 { 614 textRect.OffsetBy(offset); 615 616 if (direct) { 617 // draw selection box if selected 618 if (selected) { 619 drawView->SetDrawingMode(B_OP_COPY); 620 // eraseRect.OffsetBy(offset); 621 // drawView->FillRect(eraseRect, B_SOLID_LOW); 622 drawView->FillRect(textRect, B_SOLID_LOW); 623 } else 624 drawView->SetDrawingMode(B_OP_OVER); 625 626 // set high color 627 rgb_color highColor; 628 // for active views, the selection is drawn as inverse text (background color for the text, 629 // solid black for the background). 630 // For inactive windows, the text is drawn normally, then the selection rect is 631 // alpha-blended on top of it. 632 // This all happens in BPose::Draw before and after calling this function, here we are 633 // only concerned with setting the correct color for the text. 634 if (selected && view->Window()->IsActive()) 635 highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR); 636 else 637 highColor = view->DeskTextColor(); 638 639 if (clipboardMode == kMoveSelectionTo && !selected) { 640 drawView->SetDrawingMode(B_OP_ALPHA); 641 drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 642 highColor.alpha = 64; 643 } 644 drawView->SetHighColor(highColor); 645 } 646 647 BPoint loc; 648 loc.y = textRect.bottom - view->FontInfo().descent; 649 loc.x = textRect.left + 1; 650 651 const char* fittingText = fText->FittingText(view); 652 653 // TODO: Comparing view and drawView here to avoid rendering 654 // the text outline when producing a drag bitmap. The check is 655 // not fully correct, since an offscreen view is also used in some 656 // other rare cases (something to do with columns). But for now, this 657 // fixes the broken drag bitmaps when dragging icons from the Desktop. 658 if (!selected && view == drawView && view->WidgetTextOutline()) { 659 // draw a halo around the text by using the "false bold" 660 // feature for text rendering. Either black or white is used for 661 // the glow (whatever acts as contrast) with a some alpha value, 662 drawView->SetDrawingMode(B_OP_ALPHA); 663 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 664 665 BFont font; 666 drawView->GetFont(&font); 667 668 rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR); 669 if (view->IsDesktopWindow()) 670 textColor = view->DeskTextColor(); 671 672 if (textColor.Brightness() < 100) { 673 // dark text on light outline 674 rgb_color glowColor = ui_color(B_SHINE_COLOR); 675 676 font.SetFalseBoldWidth(2.0); 677 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 678 glowColor.alpha = 30; 679 drawView->SetHighColor(glowColor); 680 681 drawView->DrawString(fittingText, loc); 682 683 font.SetFalseBoldWidth(1.0); 684 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 685 glowColor.alpha = 65; 686 drawView->SetHighColor(glowColor); 687 688 drawView->DrawString(fittingText, loc); 689 690 font.SetFalseBoldWidth(0.0); 691 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 692 } else { 693 // light text on dark outline 694 rgb_color outlineColor = kBlack; 695 696 font.SetFalseBoldWidth(1.0); 697 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 698 outlineColor.alpha = 30; 699 drawView->SetHighColor(outlineColor); 700 701 drawView->DrawString(fittingText, loc); 702 703 font.SetFalseBoldWidth(0.0); 704 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 705 706 outlineColor.alpha = 200; 707 drawView->SetHighColor(outlineColor); 708 709 drawView->DrawString(fittingText, loc + BPoint(1, 1)); 710 } 711 712 drawView->SetDrawingMode(B_OP_OVER); 713 drawView->SetHighColor(textColor); 714 } 715 716 drawView->DrawString(fittingText, loc); 717 718 if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) { 719 // TODO: 720 // this should be exported to the WidgetAttribute class, probably 721 // by having a per widget kind style 722 if (direct) { 723 rgb_color underlineColor = drawView->HighColor(); 724 underlineColor.alpha = 180; 725 drawView->SetHighColor(underlineColor); 726 drawView->SetDrawingMode(B_OP_ALPHA); 727 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 728 } 729 730 textRect.right = textRect.left + fText->Width(view); 731 // only underline text part 732 drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(), 733 B_MIXED_COLORS); 734 } 735 } 736