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