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 view->SetActiveTextWidget(this); 444 445 // TODO fix text rect being off by a pixel on some files 446 447 BRect rect(bounds); 448 rect.OffsetBy(view->ViewMode() == kListMode ? -1 : 1, -4); 449 BTextView* textView = new BTextView(rect, "WidgetTextView", rect, 450 be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW); 451 452 textView->SetWordWrap(false); 453 textView->SetInsets(2, 2, 2, 2); 454 DisallowMetaKeys(textView); 455 fText->SetUpEditing(textView); 456 457 textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter)); 458 459 if (view->TargetVolumeIsReadOnly()) { 460 textView->MakeEditable(false); 461 textView->MakeSelectable(true); 462 } else 463 textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter)); 464 465 // get full text length 466 rect.right = rect.left + textView->LineWidth(); 467 rect.bottom = rect.top + textView->LineHeight() - 1 + 4; 468 469 if (view->ViewMode() == kListMode) { 470 // limit max width to column width in list mode 471 BColumn* column = view->ColumnFor(fAttrHash); 472 ASSERT(column != NULL); 473 fMaxWidth = column->Width(); 474 } else { 475 // limit max width to 30em in icon and mini icon mode 476 fMaxWidth = textView->StringWidth("M") * 30; 477 478 if (textView->LineWidth() > fMaxWidth 479 || view->ViewMode() == kMiniIconMode) { 480 // compensate for text going over right inset 481 rect.OffsetBy(-2, 0); 482 } 483 } 484 485 // resize textView 486 textView->MoveTo(rect.LeftTop()); 487 textView->ResizeTo(std::min(fMaxWidth, rect.Width()), rect.Height()); 488 textView->SetTextRect(rect); 489 490 // set alignment before adding textView so it doesn't redraw 491 switch (view->ViewMode()) { 492 case kIconMode: 493 textView->SetAlignment(B_ALIGN_CENTER); 494 break; 495 496 case kMiniIconMode: 497 textView->SetAlignment(B_ALIGN_LEFT); 498 break; 499 500 case kListMode: 501 textView->SetAlignment(fAlignment); 502 break; 503 } 504 505 BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0, 506 false, false, B_PLAIN_BORDER); 507 view->AddChild(scrollView); 508 509 bool tooWide = textView->TextRect().Width() > fMaxWidth; 510 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 511 512 view->SetActivePose(pose); 513 // tell view about pose 514 SetActive(true); 515 // for widget 516 517 textView->SelectAll(); 518 textView->ScrollToSelection(); 519 // scroll to beginning so that text is visible 520 textView->MakeFocus(); 521 522 // make this text widget invisible while we edit it 523 SetVisible(false); 524 525 ASSERT(view->Window() != NULL); 526 // how can I not have a Window here??? 527 528 if (view->Window()) { 529 // force immediate redraw so TextView appears instantly 530 view->Window()->UpdateIfNeeded(); 531 } 532 } 533 534 535 void 536 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view, 537 BPose* pose, int32 poseIndex) 538 { 539 view->SetActiveTextWidget(NULL); 540 541 // find the text editing view 542 BView* scrollView = view->FindView("BorderView"); 543 ASSERT(scrollView != NULL); 544 if (scrollView == NULL) 545 return; 546 547 BTextView* textView = dynamic_cast<BTextView*>( 548 scrollView->FindView("WidgetTextView")); 549 ASSERT(textView != NULL); 550 if (textView == NULL) 551 return; 552 553 BColumn* column = view->ColumnFor(fAttrHash); 554 ASSERT(column != NULL); 555 if (column == NULL) 556 return; 557 558 if (saveChanges && fText->CommitEditedText(textView)) { 559 // we have an actual change, re-sort 560 view->CheckPoseSortOrder(pose, poseIndex); 561 } 562 563 // make text widget visible again 564 SetVisible(true); 565 view->Invalidate(ColumnRect(poseLoc, column, view)); 566 567 // force immediate redraw so TEView disappears 568 scrollView->RemoveSelf(); 569 delete scrollView; 570 571 ASSERT(view->Window() != NULL); 572 view->Window()->UpdateIfNeeded(); 573 view->MakeFocus(); 574 575 SetActive(false); 576 } 577 578 579 void 580 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column, 581 BPoseView* view, bool visible) 582 { 583 BRect oldRect; 584 if (view->ViewMode() != kListMode) 585 oldRect = CalcOldRect(loc, column, view); 586 587 if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view) 588 && visible) { 589 BRect invalRect(ColumnRect(loc, column, view)); 590 if (view->ViewMode() != kListMode) 591 invalRect = invalRect | oldRect; 592 view->Invalidate(invalRect); 593 } 594 } 595 596 597 void 598 BTextWidget::SelectAll(BPoseView* view) 599 { 600 BTextView* text = dynamic_cast<BTextView*>( 601 view->FindView("WidgetTextView")); 602 if (text != NULL) 603 text->SelectAll(); 604 } 605 606 607 void 608 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view, 609 BView* drawView, bool selected, uint32 clipboardMode, BPoint offset, 610 bool direct) 611 { 612 textRect.OffsetBy(offset); 613 614 // We are only concerned with setting the correct text color. 615 616 // For active views the selection is drawn as inverse text 617 // (background color for the text, solid black for the background). 618 // For inactive windows the text is drawn normally, then the 619 // selection rect is alpha-blended on top. This all happens in 620 // BPose::Draw before and after calling this function. 621 622 if (direct) { 623 // draw selection box if selected 624 if (selected) { 625 drawView->SetDrawingMode(B_OP_COPY); 626 drawView->FillRect(textRect, B_SOLID_LOW); 627 } else 628 drawView->SetDrawingMode(B_OP_OVER); 629 630 // set high color 631 rgb_color highColor; 632 highColor = view->TextColor(selected && view->Window()->IsActive()); 633 634 if (clipboardMode == kMoveSelectionTo && !selected) { 635 drawView->SetDrawingMode(B_OP_ALPHA); 636 drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 637 highColor.alpha = 64; 638 } 639 drawView->SetHighColor(highColor); 640 } else if (selected && view->Window()->IsActive()) 641 drawView->SetHighColor(view->BackColor(true)); // inverse 642 else if (!selected) 643 drawView->SetHighColor(view->TextColor()); 644 645 BPoint location; 646 location.y = textRect.bottom - view->FontInfo().descent; 647 location.x = textRect.left + 1; 648 649 const char* fittingText = fText->FittingText(view); 650 651 // TODO: Comparing view and drawView here to avoid rendering 652 // the text outline when producing a drag bitmap. The check is 653 // not fully correct, since an offscreen view is also used in some 654 // other rare cases (something to do with columns). But for now, this 655 // fixes the broken drag bitmaps when dragging icons from the Desktop. 656 if (direct && !selected && view->WidgetTextOutline()) { 657 // draw a halo around the text by using the "false bold" 658 // feature for text rendering. Either black or white is used for 659 // the glow (whatever acts as contrast) with a some alpha value, 660 drawView->SetDrawingMode(B_OP_ALPHA); 661 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 662 663 BFont font; 664 drawView->GetFont(&font); 665 666 rgb_color textColor = view->TextColor(); 667 if (textColor.Brightness() < 100) { 668 // dark text on light outline 669 rgb_color glowColor = ui_color(B_SHINE_COLOR); 670 671 font.SetFalseBoldWidth(2.0); 672 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 673 glowColor.alpha = 30; 674 drawView->SetHighColor(glowColor); 675 676 drawView->DrawString(fittingText, location); 677 678 font.SetFalseBoldWidth(1.0); 679 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 680 glowColor.alpha = 65; 681 drawView->SetHighColor(glowColor); 682 683 drawView->DrawString(fittingText, location); 684 685 font.SetFalseBoldWidth(0.0); 686 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 687 } else { 688 // light text on dark outline 689 rgb_color outlineColor = kBlack; 690 691 font.SetFalseBoldWidth(1.0); 692 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 693 outlineColor.alpha = 30; 694 drawView->SetHighColor(outlineColor); 695 696 drawView->DrawString(fittingText, location); 697 698 font.SetFalseBoldWidth(0.0); 699 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 700 701 outlineColor.alpha = 200; 702 drawView->SetHighColor(outlineColor); 703 704 drawView->DrawString(fittingText, location + BPoint(1, 1)); 705 } 706 707 drawView->SetDrawingMode(B_OP_OVER); 708 drawView->SetHighColor(textColor); 709 } 710 711 drawView->DrawString(fittingText, location); 712 713 if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) { 714 // TODO: 715 // this should be exported to the WidgetAttribute class, probably 716 // by having a per widget kind style 717 if (direct) { 718 rgb_color underlineColor = drawView->HighColor(); 719 underlineColor.alpha = 180; 720 drawView->SetHighColor(underlineColor); 721 drawView->SetDrawingMode(B_OP_ALPHA); 722 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 723 } 724 725 textRect.right = textRect.left + fText->Width(view); 726 // only underline text part 727 drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(), 728 B_MIXED_COLORS); 729 } 730 } 731