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 ThrowOnAssert(filter != NULL); 301 302 BContainerWindow* window = dynamic_cast<BContainerWindow*>( 303 filter->Looper()); 304 ThrowOnAssert(window != NULL); 305 306 BPoseView* poseView = window->PoseView(); 307 ThrowOnAssert(poseView != NULL); 308 309 if (key == B_RETURN || key == B_ESCAPE) { 310 poseView->CommitActivePose(key == B_RETURN); 311 return B_SKIP_MESSAGE; 312 } 313 314 if (key == B_TAB) { 315 if (poseView->ActivePose()) { 316 if (message->FindInt32("modifiers") & B_SHIFT_KEY) 317 poseView->ActivePose()->EditPreviousWidget(poseView); 318 else 319 poseView->ActivePose()->EditNextWidget(poseView); 320 } 321 322 return B_SKIP_MESSAGE; 323 } 324 325 // the BTextView doesn't respect window borders when resizing itself; 326 // we try to work-around this "bug" here. 327 328 // find the text editing view 329 BView* scrollView = poseView->FindView("BorderView"); 330 if (scrollView != NULL) { 331 BTextView* textView = dynamic_cast<BTextView*>( 332 scrollView->FindView("WidgetTextView")); 333 if (textView != NULL) { 334 BRect rect = scrollView->Frame(); 335 336 if (rect.right + 3 > poseView->Bounds().right 337 || rect.left - 3 < 0) 338 textView->MakeResizable(true, NULL); 339 } 340 } 341 342 return B_DISPATCH_MESSAGE; 343 } 344 345 346 void 347 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose) 348 { 349 view->SetTextWidgetToCheck(NULL, this); 350 if (!IsEditable() || IsActive()) 351 return; 352 353 BEntry entry(pose->TargetModel()->EntryRef()); 354 if (entry.InitCheck() == B_OK 355 && !ConfirmChangeIfWellKnownDirectory(&entry, 356 B_TRANSLATE_COMMENT("rename", 357 "As in 'if you rename this folder...' (en) " 358 "'Wird dieser Ordner umbenannt...' (de)"), 359 B_TRANSLATE_COMMENT("rename", 360 "As in 'to rename this folder...' (en) " 361 "'Um diesen Ordner umzubenennen...' (de)"), 362 B_TRANSLATE_COMMENT("Rename", 363 "Button label, 'Rename' (en), 'Umbenennen' (de)"))) 364 return; 365 366 // get bounds with full text length 367 BRect rect(bounds); 368 BRect textRect(bounds); 369 rect.OffsetBy(-2, -1); 370 rect.right += 1; 371 372 BFont font; 373 view->GetFont(&font); 374 BTextView* textView = new BTextView(rect, "WidgetTextView", textRect, 375 &font, 0, B_FOLLOW_ALL, B_WILL_DRAW); 376 377 textView->SetWordWrap(false); 378 DisallowMetaKeys(textView); 379 fText->SetUpEditing(textView); 380 381 textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewFilter)); 382 383 rect.right = rect.left + textView->LineWidth() + 3; 384 // center new width, if necessary 385 if (view->ViewMode() == kIconMode 386 || (view->ViewMode() == kListMode && fAlignment == B_ALIGN_CENTER)) { 387 rect.OffsetBy(bounds.Width() / 2 - rect.Width() / 2, 0); 388 } 389 390 rect.bottom = rect.top + textView->LineHeight() + 1; 391 textRect = rect.OffsetToCopy(2, 1); 392 textRect.right -= 3; 393 textRect.bottom--; 394 textView->SetTextRect(textRect); 395 396 BPoint origin = view->LeftTop(); 397 textRect = view->Bounds(); 398 399 bool hitBorder = false; 400 if (rect.left <= origin.x) 401 rect.left = origin.x + 1, hitBorder = true; 402 if (rect.right >= textRect.right) 403 rect.right = textRect.right - 1, hitBorder = true; 404 405 textView->MoveTo(rect.LeftTop()); 406 textView->ResizeTo(rect.Width(), rect.Height()); 407 408 BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0, 409 false, false, B_PLAIN_BORDER); 410 view->AddChild(scrollView); 411 412 // configure text view 413 switch (view->ViewMode()) { 414 case kIconMode: 415 textView->SetAlignment(B_ALIGN_CENTER); 416 break; 417 418 case kMiniIconMode: 419 textView->SetAlignment(B_ALIGN_LEFT); 420 break; 421 422 case kListMode: 423 textView->SetAlignment(fAlignment); 424 break; 425 } 426 textView->MakeResizable(true, hitBorder ? NULL : scrollView); 427 428 view->SetActivePose(pose); 429 // tell view about pose 430 SetActive(true); 431 // for widget 432 433 textView->SelectAll(); 434 textView->MakeFocus(); 435 436 // make this text widget invisible while we edit it 437 SetVisible(false); 438 439 ASSERT(view->Window()); 440 // how can I not have a Window here??? 441 442 if (view->Window()) { 443 // force immediate redraw so TextView appears instantly 444 view->Window()->UpdateIfNeeded(); 445 } 446 } 447 448 449 void 450 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view, 451 BPose* pose, int32 poseIndex) 452 { 453 // find the text editing view 454 BView* scrollView = view->FindView("BorderView"); 455 ASSERT(scrollView); 456 if (scrollView == NULL) 457 return; 458 459 BTextView* textView = dynamic_cast<BTextView*>( 460 scrollView->FindView("WidgetTextView")); 461 ASSERT(textView != NULL); 462 if (textView == NULL) 463 return; 464 465 BColumn* column = view->ColumnFor(fAttrHash); 466 ASSERT(column != NULL); 467 if (column == NULL) 468 return; 469 470 if (saveChanges && fText->CommitEditedText(textView)) { 471 // we have an actual change, re-sort 472 view->CheckPoseSortOrder(pose, poseIndex); 473 } 474 475 // make text widget visible again 476 SetVisible(true); 477 view->Invalidate(ColumnRect(poseLoc, column, view)); 478 479 // force immediate redraw so TEView disappears 480 scrollView->RemoveSelf(); 481 delete scrollView; 482 483 ASSERT(view->Window()); 484 view->Window()->UpdateIfNeeded(); 485 view->MakeFocus(); 486 487 SetActive(false); 488 } 489 490 491 void 492 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column, 493 BPoseView* view, bool visible) 494 { 495 BRect oldRect; 496 if (view->ViewMode() != kListMode) 497 oldRect = CalcOldRect(loc, column, view); 498 499 if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view) 500 && visible) { 501 BRect invalRect(ColumnRect(loc, column, view)); 502 if (view->ViewMode() != kListMode) 503 invalRect = invalRect | oldRect; 504 view->Invalidate(invalRect); 505 } 506 } 507 508 509 void 510 BTextWidget::SelectAll(BPoseView* view) 511 { 512 BTextView* text = dynamic_cast<BTextView*>( 513 view->FindView("WidgetTextView")); 514 if (text != NULL) 515 text->SelectAll(); 516 } 517 518 519 void 520 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view, 521 BView* drawView, bool selected, uint32 clipboardMode, BPoint offset, 522 bool direct) 523 { 524 textRect.OffsetBy(offset); 525 526 if (direct) { 527 // draw selection box if selected 528 if (selected) { 529 drawView->SetDrawingMode(B_OP_COPY); 530 // eraseRect.OffsetBy(offset); 531 // drawView->FillRect(eraseRect, B_SOLID_LOW); 532 drawView->FillRect(textRect, B_SOLID_LOW); 533 } else 534 drawView->SetDrawingMode(B_OP_OVER); 535 536 // set high color 537 rgb_color highColor; 538 if (view->IsDesktopWindow()) { 539 if (selected) 540 highColor = kWhite; 541 else 542 highColor = view->DeskTextColor(); 543 } else if (selected && view->Window()->IsActive()) { 544 highColor = kWhite; 545 } else 546 highColor = kBlack; 547 548 if (clipboardMode == kMoveSelectionTo && !selected) { 549 drawView->SetDrawingMode(B_OP_ALPHA); 550 drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 551 highColor.alpha = 64; 552 } 553 drawView->SetHighColor(highColor); 554 } 555 556 BPoint loc; 557 loc.y = textRect.bottom - view->FontInfo().descent; 558 loc.x = textRect.left + 1; 559 560 const char* fittingText = fText->FittingText(view); 561 562 // TODO: Comparing view and drawView here to avoid rendering 563 // the text outline when producing a drag bitmap. The check is 564 // not fully correct, since an offscreen view is also used in some 565 // other rare cases (something to do with columns). But for now, this 566 // fixes the broken drag bitmaps when dragging icons from the Desktop. 567 if (!selected && view == drawView && view->WidgetTextOutline()) { 568 // draw a halo around the text by using the "false bold" 569 // feature for text rendering. Either black or white is used for 570 // the glow (whatever acts as contrast) with a some alpha value, 571 drawView->SetDrawingMode(B_OP_ALPHA); 572 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 573 574 BFont font; 575 drawView->GetFont(&font); 576 577 rgb_color textColor = drawView->HighColor(); 578 if (textColor.red + textColor.green + textColor.blue < 128 * 3) { 579 // dark text on light outline 580 rgb_color glowColor = kWhite; 581 582 font.SetFalseBoldWidth(2.0); 583 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 584 glowColor.alpha = 30; 585 drawView->SetHighColor(glowColor); 586 587 drawView->DrawString(fittingText, loc); 588 589 font.SetFalseBoldWidth(1.0); 590 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 591 glowColor.alpha = 65; 592 drawView->SetHighColor(glowColor); 593 594 drawView->DrawString(fittingText, loc); 595 596 font.SetFalseBoldWidth(0.0); 597 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 598 } else { 599 // light text on dark outline 600 rgb_color outlineColor = kBlack; 601 602 font.SetFalseBoldWidth(1.0); 603 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 604 outlineColor.alpha = 30; 605 drawView->SetHighColor(outlineColor); 606 607 drawView->DrawString(fittingText, loc); 608 609 font.SetFalseBoldWidth(0.0); 610 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 611 612 outlineColor.alpha = 200; 613 drawView->SetHighColor(outlineColor); 614 615 drawView->DrawString(fittingText, loc + BPoint(1, 1)); 616 } 617 618 drawView->SetDrawingMode(B_OP_OVER); 619 drawView->SetHighColor(textColor); 620 } 621 622 drawView->DrawString(fittingText, loc); 623 624 if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) { 625 // TODO: 626 // this should be exported to the WidgetAttribute class, probably 627 // by having a per widget kind style 628 if (direct) { 629 rgb_color underlineColor = drawView->HighColor(); 630 underlineColor.alpha = 180; 631 drawView->SetHighColor(underlineColor); 632 drawView->SetDrawingMode(B_OP_ALPHA); 633 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 634 } 635 636 textRect.right = textRect.left + fText->Width(view); 637 // only underline text part 638 drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(), 639 B_MIXED_COLORS); 640 } 641 } 642