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 <Debug.h> 44 #include <Directory.h> 45 #include <MessageFilter.h> 46 #include <ScrollView.h> 47 #include <TextView.h> 48 #include <Volume.h> 49 #include <Window.h> 50 51 #include "Attributes.h" 52 #include "ContainerWindow.h" 53 #include "Commands.h" 54 #include "FSUtils.h" 55 #include "PoseView.h" 56 #include "Utilities.h" 57 58 59 #undef B_TRANSLATION_CONTEXT 60 #define B_TRANSLATION_CONTEXT "TextWidget" 61 62 63 const float kWidthMargin = 20; 64 65 66 // #pragma mark - BTextWidget 67 68 69 BTextWidget::BTextWidget(Model* model, BColumn* column, BPoseView* view) 70 : 71 fText(WidgetAttributeText::NewWidgetText(model, column, view)), 72 fAttrHash(column->AttrHash()), 73 fAlignment(column->Alignment()), 74 fEditable(column->Editable()), 75 fVisible(true), 76 fActive(false), 77 fSymLink(model->IsSymLink()), 78 fLastClickedTime(0) 79 { 80 } 81 82 83 BTextWidget::~BTextWidget() 84 { 85 if (fLastClickedTime != 0) 86 fParams.poseView->SetTextWidgetToCheck(NULL, this); 87 88 delete fText; 89 } 90 91 92 int 93 BTextWidget::Compare(const BTextWidget& with, BPoseView* view) const 94 { 95 return fText->Compare(*with.fText, view); 96 } 97 98 99 const char* 100 BTextWidget::Text(const BPoseView* view) const 101 { 102 StringAttributeText* textAttribute 103 = dynamic_cast<StringAttributeText*>(fText); 104 if (textAttribute == NULL) 105 return NULL; 106 107 return textAttribute->ValueAsText(view); 108 } 109 110 111 float 112 BTextWidget::TextWidth(const BPoseView* pose) const 113 { 114 return fText->Width(pose); 115 } 116 117 118 float 119 BTextWidget::PreferredWidth(const BPoseView* pose) const 120 { 121 return fText->PreferredWidth(pose) + 1; 122 } 123 124 125 BRect 126 BTextWidget::ColumnRect(BPoint poseLoc, const BColumn* column, 127 const BPoseView* view) 128 { 129 if (view->ViewMode() != kListMode) { 130 // ColumnRect only makes sense in list view, return 131 // CalcRect otherwise 132 return CalcRect(poseLoc, column, view); 133 } 134 BRect result; 135 result.left = column->Offset() + poseLoc.x; 136 result.right = result.left + column->Width(); 137 result.bottom = poseLoc.y + view->ListElemHeight() - 1; 138 result.top = result.bottom - view->FontHeight(); 139 return result; 140 } 141 142 143 BRect 144 BTextWidget::CalcRectCommon(BPoint poseLoc, const BColumn* column, 145 const BPoseView* view, float textWidth) 146 { 147 BRect result; 148 if (view->ViewMode() == kListMode) { 149 poseLoc.x += column->Offset(); 150 151 switch (fAlignment) { 152 case B_ALIGN_LEFT: 153 result.left = poseLoc.x; 154 result.right = result.left + textWidth + 1; 155 break; 156 157 case B_ALIGN_CENTER: 158 result.left = poseLoc.x + (column->Width() / 2) 159 - (textWidth / 2); 160 if (result.left < 0) 161 result.left = 0; 162 163 result.right = result.left + textWidth + 1; 164 break; 165 166 case B_ALIGN_RIGHT: 167 result.right = poseLoc.x + column->Width(); 168 result.left = result.right - textWidth - 1; 169 if (result.left < 0) 170 result.left = 0; 171 break; 172 173 default: 174 TRESPASS(); 175 break; 176 } 177 178 result.bottom = poseLoc.y 179 + roundf((view->ListElemHeight() + view->FontHeight()) / 2); 180 } else { 181 if (view->ViewMode() == kIconMode) { 182 // large/scaled icon mode 183 result.left = poseLoc.x + (view->IconSizeInt() - textWidth) / 2; 184 } else { 185 // mini icon mode 186 result.left = poseLoc.x + B_MINI_ICON + kMiniIconSeparator; 187 } 188 189 result.right = result.left + textWidth; 190 result.bottom = poseLoc.y + view->IconPoseHeight(); 191 } 192 result.top = result.bottom - view->FontHeight(); 193 194 return result; 195 } 196 197 198 BRect 199 BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column, 200 const BPoseView* view) 201 { 202 return CalcRectCommon(poseLoc, column, view, fText->Width(view)); 203 } 204 205 206 BRect 207 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column, 208 const BPoseView* view) 209 { 210 return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth()); 211 } 212 213 214 BRect 215 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column, 216 const BPoseView* view) 217 { 218 BRect result = CalcRect(poseLoc, column, view); 219 if (result.Width() < kWidthMargin) { 220 // if resulting rect too narrow, make it a bit wider 221 // for comfortable clicking 222 if (column != NULL && column->Width() < kWidthMargin) 223 result.right = result.left + column->Width(); 224 else 225 result.right = result.left + kWidthMargin; 226 } 227 228 return result; 229 } 230 231 232 void 233 BTextWidget::CheckExpiration() 234 { 235 if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) { 236 bigtime_t doubleClickSpeed; 237 get_click_speed(&doubleClickSpeed); 238 239 bigtime_t delta = system_time() - fLastClickedTime; 240 241 if (delta > doubleClickSpeed) { 242 // at least 'doubleClickSpeed' microseconds ellapsed and no click 243 // was registered since. 244 fLastClickedTime = 0; 245 StartEdit(fParams.bounds, fParams.poseView, fParams.pose); 246 } 247 } else { 248 fLastClickedTime = 0; 249 fParams.poseView->SetTextWidgetToCheck(NULL); 250 } 251 } 252 253 254 void 255 BTextWidget::CancelWait() 256 { 257 fLastClickedTime = 0; 258 fParams.poseView->SetTextWidgetToCheck(NULL); 259 } 260 261 262 void 263 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint) 264 { 265 // Register the time of that click. The PoseView, through its Pulse() 266 // will allow us to StartEdit() if no other click have been registered since 267 // then. 268 269 // TODO: re-enable modifiers, one should be enough 270 view->SetTextWidgetToCheck(NULL); 271 if (IsEditable() && pose->IsSelected()) { 272 bigtime_t doubleClickSpeed; 273 get_click_speed(&doubleClickSpeed); 274 275 if (fLastClickedTime == 0) { 276 fLastClickedTime = system_time(); 277 if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime()) 278 fLastClickedTime = 0; 279 } else 280 fLastClickedTime = 0; 281 282 if (fLastClickedTime == 0) 283 return; 284 285 view->SetTextWidgetToCheck(this); 286 287 fParams.pose = pose; 288 fParams.bounds = bounds; 289 fParams.poseView = view; 290 } else 291 fLastClickedTime = 0; 292 } 293 294 295 static filter_result 296 TextViewFilter(BMessage* message, BHandler**, BMessageFilter* filter) 297 { 298 uchar key; 299 if (message->FindInt8("byte", (int8*)&key) != B_OK) 300 return B_DISPATCH_MESSAGE; 301 302 ThrowOnAssert(filter != NULL); 303 304 BContainerWindow* window = dynamic_cast<BContainerWindow*>( 305 filter->Looper()); 306 ThrowOnAssert(window != NULL); 307 308 BPoseView* poseView = window->PoseView(); 309 ThrowOnAssert(poseView != NULL); 310 311 if (key == B_RETURN || key == B_ESCAPE) { 312 poseView->CommitActivePose(key == B_RETURN); 313 return B_SKIP_MESSAGE; 314 } 315 316 if (key == B_TAB) { 317 if (poseView->ActivePose()) { 318 if (message->FindInt32("modifiers") & B_SHIFT_KEY) 319 poseView->ActivePose()->EditPreviousWidget(poseView); 320 else 321 poseView->ActivePose()->EditNextWidget(poseView); 322 } 323 324 return B_SKIP_MESSAGE; 325 } 326 327 // the BTextView doesn't respect window borders when resizing itself; 328 // we try to work-around this "bug" here. 329 330 // find the text editing view 331 BView* scrollView = poseView->FindView("BorderView"); 332 if (scrollView != NULL) { 333 BTextView* textView = dynamic_cast<BTextView*>( 334 scrollView->FindView("WidgetTextView")); 335 if (textView != NULL) { 336 BRect rect = scrollView->Frame(); 337 338 if (rect.right + 3 > poseView->Bounds().right 339 || rect.left - 3 < 0) 340 textView->MakeResizable(true, NULL); 341 } 342 } 343 344 return B_DISPATCH_MESSAGE; 345 } 346 347 348 void 349 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose) 350 { 351 view->SetTextWidgetToCheck(NULL, this); 352 if (!IsEditable() || IsActive()) 353 return; 354 355 BEntry entry(pose->TargetModel()->EntryRef()); 356 if (entry.InitCheck() == B_OK 357 && !ConfirmChangeIfWellKnownDirectory(&entry, kRename)) { 358 return; 359 } 360 361 // get bounds with full text length 362 BRect rect(bounds); 363 BRect textRect(bounds); 364 rect.OffsetBy(-2, -1); 365 rect.right += 1; 366 367 BFont font; 368 view->GetFont(&font); 369 BTextView* textView = new BTextView(rect, "WidgetTextView", textRect, 370 &font, 0, B_FOLLOW_ALL, B_WILL_DRAW); 371 372 textView->SetWordWrap(false); 373 DisallowMetaKeys(textView); 374 fText->SetUpEditing(textView); 375 376 textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewFilter)); 377 378 rect.right = rect.left + textView->LineWidth() + 3; 379 // center new width, if necessary 380 if (view->ViewMode() == kIconMode 381 || (view->ViewMode() == kListMode && fAlignment == B_ALIGN_CENTER)) { 382 rect.OffsetBy(bounds.Width() / 2 - rect.Width() / 2, 0); 383 } 384 385 rect.bottom = rect.top + textView->LineHeight() + 1; 386 textRect = rect.OffsetToCopy(2, 1); 387 textRect.right -= 3; 388 textRect.bottom--; 389 textView->SetTextRect(textRect); 390 391 BPoint origin = view->LeftTop(); 392 textRect = view->Bounds(); 393 394 bool hitBorder = false; 395 if (rect.left <= origin.x) 396 rect.left = origin.x + 1, hitBorder = true; 397 if (rect.right >= textRect.right) 398 rect.right = textRect.right - 1, hitBorder = true; 399 400 textView->MoveTo(rect.LeftTop()); 401 textView->ResizeTo(rect.Width(), rect.Height()); 402 403 BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0, 404 false, false, B_PLAIN_BORDER); 405 view->AddChild(scrollView); 406 407 // configure text view 408 switch (view->ViewMode()) { 409 case kIconMode: 410 textView->SetAlignment(B_ALIGN_CENTER); 411 break; 412 413 case kMiniIconMode: 414 textView->SetAlignment(B_ALIGN_LEFT); 415 break; 416 417 case kListMode: 418 textView->SetAlignment(fAlignment); 419 break; 420 } 421 textView->MakeResizable(true, hitBorder ? NULL : scrollView); 422 423 view->SetActivePose(pose); 424 // tell view about pose 425 SetActive(true); 426 // for widget 427 428 textView->SelectAll(); 429 textView->MakeFocus(); 430 431 // make this text widget invisible while we edit it 432 SetVisible(false); 433 434 ASSERT(view->Window() != NULL); 435 // how can I not have a Window here??? 436 437 if (view->Window()) { 438 // force immediate redraw so TextView appears instantly 439 view->Window()->UpdateIfNeeded(); 440 } 441 } 442 443 444 void 445 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view, 446 BPose* pose, int32 poseIndex) 447 { 448 // find the text editing view 449 BView* scrollView = view->FindView("BorderView"); 450 ASSERT(scrollView != NULL); 451 if (scrollView == NULL) 452 return; 453 454 BTextView* textView = dynamic_cast<BTextView*>( 455 scrollView->FindView("WidgetTextView")); 456 ASSERT(textView != NULL); 457 if (textView == NULL) 458 return; 459 460 BColumn* column = view->ColumnFor(fAttrHash); 461 ASSERT(column != NULL); 462 if (column == NULL) 463 return; 464 465 if (saveChanges && fText->CommitEditedText(textView)) { 466 // we have an actual change, re-sort 467 view->CheckPoseSortOrder(pose, poseIndex); 468 } 469 470 // make text widget visible again 471 SetVisible(true); 472 view->Invalidate(ColumnRect(poseLoc, column, view)); 473 474 // force immediate redraw so TEView disappears 475 scrollView->RemoveSelf(); 476 delete scrollView; 477 478 ASSERT(view->Window() != NULL); 479 view->Window()->UpdateIfNeeded(); 480 view->MakeFocus(); 481 482 SetActive(false); 483 } 484 485 486 void 487 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column, 488 BPoseView* view, bool visible) 489 { 490 BRect oldRect; 491 if (view->ViewMode() != kListMode) 492 oldRect = CalcOldRect(loc, column, view); 493 494 if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view) 495 && visible) { 496 BRect invalRect(ColumnRect(loc, column, view)); 497 if (view->ViewMode() != kListMode) 498 invalRect = invalRect | oldRect; 499 view->Invalidate(invalRect); 500 } 501 } 502 503 504 void 505 BTextWidget::SelectAll(BPoseView* view) 506 { 507 BTextView* text = dynamic_cast<BTextView*>( 508 view->FindView("WidgetTextView")); 509 if (text != NULL) 510 text->SelectAll(); 511 } 512 513 514 void 515 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view, 516 BView* drawView, bool selected, uint32 clipboardMode, BPoint offset, 517 bool direct) 518 { 519 textRect.OffsetBy(offset); 520 521 if (direct) { 522 // draw selection box if selected 523 if (selected) { 524 drawView->SetDrawingMode(B_OP_COPY); 525 // eraseRect.OffsetBy(offset); 526 // drawView->FillRect(eraseRect, B_SOLID_LOW); 527 drawView->FillRect(textRect, B_SOLID_LOW); 528 } else 529 drawView->SetDrawingMode(B_OP_OVER); 530 531 // set high color 532 rgb_color highColor; 533 if (view->IsDesktopWindow()) { 534 if (selected) 535 highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR); 536 else 537 highColor = view->DeskTextColor(); 538 } else if (selected && view->Window()->IsActive()) { 539 highColor = ui_color(B_DOCUMENT_BACKGROUND_COLOR); 540 } else 541 highColor = kBlack; 542 543 if (clipboardMode == kMoveSelectionTo && !selected) { 544 drawView->SetDrawingMode(B_OP_ALPHA); 545 drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 546 highColor.alpha = 64; 547 } 548 drawView->SetHighColor(highColor); 549 } 550 551 BPoint loc; 552 loc.y = textRect.bottom - view->FontInfo().descent; 553 loc.x = textRect.left + 1; 554 555 const char* fittingText = fText->FittingText(view); 556 557 // TODO: Comparing view and drawView here to avoid rendering 558 // the text outline when producing a drag bitmap. The check is 559 // not fully correct, since an offscreen view is also used in some 560 // other rare cases (something to do with columns). But for now, this 561 // fixes the broken drag bitmaps when dragging icons from the Desktop. 562 if (!selected && view == drawView && view->WidgetTextOutline()) { 563 // draw a halo around the text by using the "false bold" 564 // feature for text rendering. Either black or white is used for 565 // the glow (whatever acts as contrast) with a some alpha value, 566 drawView->SetDrawingMode(B_OP_ALPHA); 567 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 568 569 BFont font; 570 drawView->GetFont(&font); 571 572 rgb_color textColor = ui_color(B_PANEL_TEXT_COLOR); 573 if (view->IsDesktopWindow()) 574 textColor = view->DeskTextColor(); 575 576 if (textColor.Brightness() < 100) { 577 // dark text on light outline 578 rgb_color glowColor = ui_color(B_SHINE_COLOR); 579 580 font.SetFalseBoldWidth(2.0); 581 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 582 glowColor.alpha = 30; 583 drawView->SetHighColor(glowColor); 584 585 drawView->DrawString(fittingText, loc); 586 587 font.SetFalseBoldWidth(1.0); 588 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 589 glowColor.alpha = 65; 590 drawView->SetHighColor(glowColor); 591 592 drawView->DrawString(fittingText, loc); 593 594 font.SetFalseBoldWidth(0.0); 595 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 596 } else { 597 // light text on dark outline 598 rgb_color outlineColor = kBlack; 599 600 font.SetFalseBoldWidth(1.0); 601 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 602 outlineColor.alpha = 30; 603 drawView->SetHighColor(outlineColor); 604 605 drawView->DrawString(fittingText, loc); 606 607 font.SetFalseBoldWidth(0.0); 608 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 609 610 outlineColor.alpha = 200; 611 drawView->SetHighColor(outlineColor); 612 613 drawView->DrawString(fittingText, loc + BPoint(1, 1)); 614 } 615 616 drawView->SetDrawingMode(B_OP_OVER); 617 drawView->SetHighColor(textColor); 618 } 619 620 drawView->DrawString(fittingText, loc); 621 622 if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) { 623 // TODO: 624 // this should be exported to the WidgetAttribute class, probably 625 // by having a per widget kind style 626 if (direct) { 627 rgb_color underlineColor = drawView->HighColor(); 628 underlineColor.alpha = 180; 629 drawView->SetHighColor(underlineColor); 630 drawView->SetDrawingMode(B_OP_ALPHA); 631 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 632 } 633 634 textRect.right = textRect.left + fText->Width(view); 635 // only underline text part 636 drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(), 637 B_MIXED_COLORS); 638 } 639 } 640