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