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 - floorf(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 + 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 + viewWidth; 171 break; 172 173 case B_ALIGN_RIGHT: 174 result.right = poseLoc.x + column->Width(); 175 result.left = result.right - 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 } else { 194 // mini icon mode 195 result.left = poseLoc.x + view->IconSizeInt() + kMiniIconSeparator; 196 } 197 result.bottom = poseLoc.y + view->IconPoseHeight(); 198 199 result.right = result.left + viewWidth; 200 } 201 202 result.top = result.bottom - floorf(view->FontHeight()); 203 204 return result; 205 } 206 207 208 BRect 209 BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column, 210 const BPoseView* view) 211 { 212 return CalcRectCommon(poseLoc, column, view, fText->Width(view)); 213 } 214 215 216 BRect 217 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column, 218 const BPoseView* view) 219 { 220 return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth()); 221 } 222 223 224 BRect 225 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column, 226 const BPoseView* view) 227 { 228 BRect result = CalcRect(poseLoc, column, view); 229 if (result.Width() < kWidthMargin) { 230 // if resulting rect too narrow, make it a bit wider 231 // for comfortable clicking 232 if (column != NULL && column->Width() < kWidthMargin) 233 result.right = result.left + column->Width(); 234 else 235 result.right = result.left + kWidthMargin; 236 } 237 238 return result; 239 } 240 241 242 void 243 BTextWidget::CheckExpiration() 244 { 245 if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) { 246 bigtime_t doubleClickSpeed; 247 get_click_speed(&doubleClickSpeed); 248 249 bigtime_t delta = system_time() - fLastClickedTime; 250 251 if (delta > doubleClickSpeed) { 252 // at least 'doubleClickSpeed' microseconds ellapsed and no click 253 // was registered since. 254 fLastClickedTime = 0; 255 StartEdit(fParams.bounds, fParams.poseView, fParams.pose); 256 } 257 } else { 258 fLastClickedTime = 0; 259 fParams.poseView->SetTextWidgetToCheck(NULL); 260 } 261 } 262 263 264 void 265 BTextWidget::CancelWait() 266 { 267 fLastClickedTime = 0; 268 fParams.poseView->SetTextWidgetToCheck(NULL); 269 } 270 271 272 void 273 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint) 274 { 275 // Register the time of that click. The PoseView, through its Pulse() 276 // will allow us to StartEdit() if no other click have been registered since 277 // then. 278 279 // TODO: re-enable modifiers, one should be enough 280 view->SetTextWidgetToCheck(NULL); 281 if (IsEditable() && pose->IsSelected()) { 282 bigtime_t doubleClickSpeed; 283 get_click_speed(&doubleClickSpeed); 284 285 if (fLastClickedTime == 0) { 286 fLastClickedTime = system_time(); 287 if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime()) 288 fLastClickedTime = 0; 289 } else 290 fLastClickedTime = 0; 291 292 if (fLastClickedTime == 0) 293 return; 294 295 view->SetTextWidgetToCheck(this); 296 297 fParams.pose = pose; 298 fParams.bounds = bounds; 299 fParams.poseView = view; 300 } else 301 fLastClickedTime = 0; 302 } 303 304 305 static filter_result 306 TextViewKeyDownFilter(BMessage* message, BHandler**, BMessageFilter* filter) 307 { 308 uchar key; 309 if (message->FindInt8("byte", (int8*)&key) != B_OK) 310 return B_DISPATCH_MESSAGE; 311 312 ThrowOnAssert(filter != NULL); 313 314 BContainerWindow* window = dynamic_cast<BContainerWindow*>( 315 filter->Looper()); 316 ThrowOnAssert(window != NULL); 317 318 BPoseView* view = window->PoseView(); 319 ThrowOnAssert(view != NULL); 320 321 if (key == B_RETURN || key == B_ESCAPE) { 322 view->CommitActivePose(key == B_RETURN); 323 return B_SKIP_MESSAGE; 324 } 325 326 if (key == B_TAB) { 327 if (view->ActivePose()) { 328 if (message->FindInt32("modifiers") & B_SHIFT_KEY) 329 view->ActivePose()->EditPreviousWidget(view); 330 else 331 view->ActivePose()->EditNextWidget(view); 332 } 333 334 return B_SKIP_MESSAGE; 335 } 336 337 // the BTextView doesn't respect window borders when resizing itself; 338 // we try to work-around this "bug" here. 339 340 // find the text editing view 341 BView* scrollView = view->FindView("BorderView"); 342 if (scrollView != NULL) { 343 BTextView* textView = dynamic_cast<BTextView*>( 344 scrollView->FindView("WidgetTextView")); 345 if (textView != NULL) { 346 ASSERT(view->ActiveTextWidget() != NULL); 347 float maxWidth = view->ActiveTextWidget()->MaxWidth(); 348 bool tooWide = textView->TextRect().Width() > maxWidth; 349 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 350 } 351 } 352 353 return B_DISPATCH_MESSAGE; 354 } 355 356 357 static filter_result 358 TextViewPasteFilter(BMessage* message, BHandler**, BMessageFilter* filter) 359 { 360 ThrowOnAssert(filter != NULL); 361 362 BContainerWindow* window = dynamic_cast<BContainerWindow*>( 363 filter->Looper()); 364 ThrowOnAssert(window != NULL); 365 366 BPoseView* view = window->PoseView(); 367 ThrowOnAssert(view != NULL); 368 369 // the BTextView doesn't respect window borders when resizing itself; 370 // we try to work-around this "bug" here. 371 372 // find the text editing view 373 BView* scrollView = view->FindView("BorderView"); 374 if (scrollView != NULL) { 375 BTextView* textView = dynamic_cast<BTextView*>( 376 scrollView->FindView("WidgetTextView")); 377 if (textView != NULL) { 378 float textWidth = textView->TextRect().Width(); 379 380 // subtract out selected text region width 381 int32 start, finish; 382 textView->GetSelection(&start, &finish); 383 if (start != finish) { 384 BRegion selectedRegion; 385 textView->GetTextRegion(start, finish, &selectedRegion); 386 textWidth -= selectedRegion.Frame().Width(); 387 } 388 389 // add pasted text width 390 if (be_clipboard->Lock()) { 391 BMessage* clip = be_clipboard->Data(); 392 if (clip != NULL) { 393 const char* text = NULL; 394 ssize_t length = 0; 395 396 if (clip->FindData("text/plain", B_MIME_TYPE, 397 (const void**)&text, &length) == B_OK) { 398 textWidth += textView->StringWidth(text); 399 } 400 } 401 402 be_clipboard->Unlock(); 403 } 404 405 // check if pasted text is too wide 406 ASSERT(view->ActiveTextWidget() != NULL); 407 float maxWidth = view->ActiveTextWidget()->MaxWidth(); 408 bool tooWide = textWidth > maxWidth; 409 410 if (tooWide) { 411 // resize text view to max width 412 413 // move scroll view if not left aligned 414 float oldWidth = textView->Bounds().Width(); 415 float newWidth = maxWidth; 416 float right = oldWidth - newWidth; 417 418 if (textView->Alignment() == B_ALIGN_CENTER) 419 scrollView->MoveBy(roundf(right / 2), 0); 420 else if (textView->Alignment() == B_ALIGN_RIGHT) 421 scrollView->MoveBy(right, 0); 422 423 // resize scroll view 424 float grow = newWidth - oldWidth; 425 scrollView->ResizeBy(grow, 0); 426 } 427 428 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 429 } 430 } 431 432 return B_DISPATCH_MESSAGE; 433 } 434 435 436 void 437 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose) 438 { 439 view->SetTextWidgetToCheck(NULL, this); 440 if (!IsEditable() || IsActive()) 441 return; 442 443 BEntry entry(pose->TargetModel()->EntryRef()); 444 if (entry.InitCheck() == B_OK 445 && !ConfirmChangeIfWellKnownDirectory(&entry, kRename)) { 446 return; 447 } 448 449 view->SetActiveTextWidget(this); 450 451 // TODO fix text rect being off by a pixel on some files 452 453 BRect rect(bounds); 454 rect.OffsetBy(view->ViewMode() == kListMode ? -1 : 1, -4); 455 BTextView* textView = new BTextView(rect, "WidgetTextView", rect, 456 be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW); 457 458 textView->SetWordWrap(false); 459 textView->SetInsets(2, 2, 2, 2); 460 DisallowMetaKeys(textView); 461 fText->SetUpEditing(textView); 462 463 textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter)); 464 textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter)); 465 466 // get full text length 467 rect.right = rect.left + textView->LineWidth(); 468 rect.bottom = rect.top + textView->LineHeight() - 1 + 4; 469 470 if (view->ViewMode() == kListMode) { 471 // limit max width to column width in list mode 472 BColumn* column = view->ColumnFor(fAttrHash); 473 ASSERT(column != NULL); 474 fMaxWidth = column->Width(); 475 } else { 476 // limit max width to 30em in icon and mini icon mode 477 fMaxWidth = textView->StringWidth("M") * 30; 478 479 if (textView->LineWidth() > fMaxWidth 480 || view->ViewMode() == kMiniIconMode) { 481 // compensate for text going over right inset 482 rect.OffsetBy(-2, 0); 483 } 484 } 485 486 // resize textView 487 textView->MoveTo(rect.LeftTop()); 488 textView->ResizeTo(std::min(fMaxWidth, rect.Width()), rect.Height()); 489 textView->SetTextRect(rect); 490 491 // set alignment before adding textView so it doesn't redraw 492 switch (view->ViewMode()) { 493 case kIconMode: 494 textView->SetAlignment(B_ALIGN_CENTER); 495 break; 496 497 case kMiniIconMode: 498 textView->SetAlignment(B_ALIGN_LEFT); 499 break; 500 501 case kListMode: 502 textView->SetAlignment(fAlignment); 503 break; 504 } 505 506 BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0, 507 false, false, B_PLAIN_BORDER); 508 view->AddChild(scrollView); 509 510 bool tooWide = textView->TextRect().Width() > fMaxWidth; 511 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 512 513 view->SetActivePose(pose); 514 // tell view about pose 515 SetActive(true); 516 // for widget 517 518 textView->SelectAll(); 519 textView->ScrollToSelection(); 520 // scroll to beginning so that text is visible 521 textView->MakeFocus(); 522 523 // make this text widget invisible while we edit it 524 SetVisible(false); 525 526 ASSERT(view->Window() != NULL); 527 // how can I not have a Window here??? 528 529 if (view->Window()) { 530 // force immediate redraw so TextView appears instantly 531 view->Window()->UpdateIfNeeded(); 532 } 533 } 534 535 536 void 537 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view, 538 BPose* pose, int32 poseIndex) 539 { 540 view->SetActiveTextWidget(NULL); 541 542 // find the text editing view 543 BView* scrollView = view->FindView("BorderView"); 544 ASSERT(scrollView != NULL); 545 if (scrollView == NULL) 546 return; 547 548 BTextView* textView = dynamic_cast<BTextView*>( 549 scrollView->FindView("WidgetTextView")); 550 ASSERT(textView != NULL); 551 if (textView == NULL) 552 return; 553 554 BColumn* column = view->ColumnFor(fAttrHash); 555 ASSERT(column != NULL); 556 if (column == NULL) 557 return; 558 559 if (saveChanges && fText->CommitEditedText(textView)) { 560 // we have an actual change, re-sort 561 view->CheckPoseSortOrder(pose, poseIndex); 562 } 563 564 // make text widget visible again 565 SetVisible(true); 566 view->Invalidate(ColumnRect(poseLoc, column, view)); 567 568 // force immediate redraw so TEView disappears 569 scrollView->RemoveSelf(); 570 delete scrollView; 571 572 ASSERT(view->Window() != NULL); 573 view->Window()->UpdateIfNeeded(); 574 view->MakeFocus(); 575 576 SetActive(false); 577 } 578 579 580 void 581 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column, 582 BPoseView* view, bool visible) 583 { 584 BRect oldRect; 585 if (view->ViewMode() != kListMode) 586 oldRect = CalcOldRect(loc, column, view); 587 588 if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view) 589 && visible) { 590 BRect invalRect(ColumnRect(loc, column, view)); 591 if (view->ViewMode() != kListMode) 592 invalRect = invalRect | oldRect; 593 view->Invalidate(invalRect); 594 } 595 } 596 597 598 void 599 BTextWidget::SelectAll(BPoseView* view) 600 { 601 BTextView* text = dynamic_cast<BTextView*>( 602 view->FindView("WidgetTextView")); 603 if (text != NULL) 604 text->SelectAll(); 605 } 606 607 608 void 609 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view, 610 BView* drawView, bool selected, uint32 clipboardMode, BPoint offset, 611 bool direct) 612 { 613 textRect.OffsetBy(offset); 614 615 if (direct) { 616 // draw selection box if selected 617 if (selected) { 618 drawView->SetDrawingMode(B_OP_COPY); 619 // eraseRect.OffsetBy(offset); 620 // drawView->FillRect(eraseRect, B_SOLID_LOW); 621 drawView->FillRect(textRect, B_SOLID_LOW); 622 } else 623 drawView->SetDrawingMode(B_OP_OVER); 624 625 // set high color 626 rgb_color highColor; 627 // for active views, the selection is drawn as inverse text (background color for the text, 628 // solid black for the background). 629 // For inactive windows, the text is drawn normally, then the selection rect is 630 // alpha-blended on top of it. 631 // This all happens in BPose::Draw before and after calling this function, here we are 632 // only concerned with setting the correct color for the text. 633 if (selected && view->Window()->IsActive()) 634 highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR); 635 else 636 highColor = view->DeskTextColor(); 637 638 if (clipboardMode == kMoveSelectionTo && !selected) { 639 drawView->SetDrawingMode(B_OP_ALPHA); 640 drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 641 highColor.alpha = 64; 642 } 643 drawView->SetHighColor(highColor); 644 } 645 646 BPoint loc; 647 loc.y = textRect.bottom - view->FontInfo().descent; 648 loc.x = textRect.left + 1; 649 650 const char* fittingText = fText->FittingText(view); 651 652 // TODO: Comparing view and drawView here to avoid rendering 653 // the text outline when producing a drag bitmap. The check is 654 // not fully correct, since an offscreen view is also used in some 655 // other rare cases (something to do with columns). But for now, this 656 // fixes the broken drag bitmaps when dragging icons from the Desktop. 657 if (!selected && view == drawView && view->WidgetTextOutline()) { 658 // draw a halo around the text by using the "false bold" 659 // feature for text rendering. Either black or white is used for 660 // the glow (whatever acts as contrast) with a some alpha value, 661 drawView->SetDrawingMode(B_OP_ALPHA); 662 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 663 664 BFont font; 665 drawView->GetFont(&font); 666 667 rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR); 668 if (view->IsDesktopWindow()) 669 textColor = view->DeskTextColor(); 670 671 if (textColor.Brightness() < 100) { 672 // dark text on light outline 673 rgb_color glowColor = ui_color(B_SHINE_COLOR); 674 675 font.SetFalseBoldWidth(2.0); 676 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 677 glowColor.alpha = 30; 678 drawView->SetHighColor(glowColor); 679 680 drawView->DrawString(fittingText, loc); 681 682 font.SetFalseBoldWidth(1.0); 683 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 684 glowColor.alpha = 65; 685 drawView->SetHighColor(glowColor); 686 687 drawView->DrawString(fittingText, loc); 688 689 font.SetFalseBoldWidth(0.0); 690 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 691 } else { 692 // light text on dark outline 693 rgb_color outlineColor = kBlack; 694 695 font.SetFalseBoldWidth(1.0); 696 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 697 outlineColor.alpha = 30; 698 drawView->SetHighColor(outlineColor); 699 700 drawView->DrawString(fittingText, loc); 701 702 font.SetFalseBoldWidth(0.0); 703 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 704 705 outlineColor.alpha = 200; 706 drawView->SetHighColor(outlineColor); 707 708 drawView->DrawString(fittingText, loc + BPoint(1, 1)); 709 } 710 711 drawView->SetDrawingMode(B_OP_OVER); 712 drawView->SetHighColor(textColor); 713 } 714 715 drawView->DrawString(fittingText, loc); 716 717 if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) { 718 // TODO: 719 // this should be exported to the WidgetAttribute class, probably 720 // by having a per widget kind style 721 if (direct) { 722 rgb_color underlineColor = drawView->HighColor(); 723 underlineColor.alpha = 180; 724 drawView->SetHighColor(underlineColor); 725 drawView->SetDrawingMode(B_OP_ALPHA); 726 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 727 } 728 729 textRect.right = textRect.left + fText->Width(view); 730 // only underline text part 731 drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(), 732 B_MIXED_COLORS); 733 } 734 } 735