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 StartEdit(fParams.bounds, fParams.poseView, fParams.pose); 238 } 239 } else { 240 fLastClickedTime = 0; 241 fParams.poseView->SetTextWidgetToCheck(NULL); 242 } 243 } 244 245 246 void 247 BTextWidget::CancelWait() 248 { 249 fLastClickedTime = 0; 250 fParams.poseView->SetTextWidgetToCheck(NULL); 251 } 252 253 254 void 255 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint) 256 { 257 // Register the time of that click. The PoseView, through its Pulse() 258 // will allow us to StartEdit() if no other click have been registered since 259 // then. 260 261 // TODO: re-enable modifiers, one should be enough 262 view->SetTextWidgetToCheck(NULL); 263 if (IsEditable() && pose->IsSelected()) { 264 bigtime_t doubleClickSpeed; 265 get_click_speed(&doubleClickSpeed); 266 267 if (fLastClickedTime == 0) { 268 fLastClickedTime = system_time(); 269 if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime()) 270 fLastClickedTime = 0; 271 } else 272 fLastClickedTime = 0; 273 274 if (fLastClickedTime == 0) 275 return; 276 277 view->SetTextWidgetToCheck(this); 278 279 fParams.pose = pose; 280 fParams.bounds = bounds; 281 fParams.poseView = view; 282 } else 283 fLastClickedTime = 0; 284 } 285 286 287 static filter_result 288 TextViewFilter(BMessage* message, BHandler**, BMessageFilter* filter) 289 { 290 uchar key; 291 if (message->FindInt8("byte", (int8*)&key) != B_OK) 292 return B_DISPATCH_MESSAGE; 293 294 BPoseView* poseView = dynamic_cast<BContainerWindow*>( 295 filter->Looper())->PoseView(); 296 297 if (key == B_RETURN || key == B_ESCAPE) { 298 poseView->CommitActivePose(key == B_RETURN); 299 return B_SKIP_MESSAGE; 300 } 301 302 if (key == B_TAB) { 303 if (poseView->ActivePose()) { 304 if (message->FindInt32("modifiers") & B_SHIFT_KEY) 305 poseView->ActivePose()->EditPreviousWidget(poseView); 306 else 307 poseView->ActivePose()->EditNextWidget(poseView); 308 } 309 310 return B_SKIP_MESSAGE; 311 } 312 313 // the BTextView doesn't respect window borders when resizing itself; 314 // we try to work-around this "bug" here. 315 316 // find the text editing view 317 BView* scrollView = poseView->FindView("BorderView"); 318 if (scrollView != NULL) { 319 BTextView* textView = dynamic_cast<BTextView*>( 320 scrollView->FindView("WidgetTextView")); 321 if (textView != NULL) { 322 BRect rect = scrollView->Frame(); 323 324 if (rect.right + 3 > poseView->Bounds().right 325 || rect.left - 3 < 0) 326 textView->MakeResizable(true, NULL); 327 } 328 } 329 330 return B_DISPATCH_MESSAGE; 331 } 332 333 334 void 335 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose) 336 { 337 view->SetTextWidgetToCheck(NULL, this); 338 if (!IsEditable() || IsActive()) 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 BPoint origin = view->LeftTop(); 385 textRect = view->Bounds(); 386 387 bool hitBorder = false; 388 if (rect.left <= origin.x) 389 rect.left = origin.x + 1, hitBorder = true; 390 if (rect.right >= textRect.right) 391 rect.right = textRect.right - 1, hitBorder = true; 392 393 textView->MoveTo(rect.LeftTop()); 394 textView->ResizeTo(rect.Width(), rect.Height()); 395 396 BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0, 397 false, false, B_PLAIN_BORDER); 398 view->AddChild(scrollView); 399 400 // configure text view 401 switch (view->ViewMode()) { 402 case kIconMode: 403 textView->SetAlignment(B_ALIGN_CENTER); 404 break; 405 406 case kMiniIconMode: 407 textView->SetAlignment(B_ALIGN_LEFT); 408 break; 409 410 case kListMode: 411 textView->SetAlignment(fAlignment); 412 break; 413 } 414 textView->MakeResizable(true, hitBorder ? NULL : scrollView); 415 416 view->SetActivePose(pose); // tell view about pose 417 SetActive(true); // for widget 418 419 textView->SelectAll(); 420 textView->MakeFocus(); 421 422 // make this text widget invisible while we edit it 423 SetVisible(false); 424 425 ASSERT(view->Window()); // how can I not have a Window here??? 426 427 if (view->Window()) { 428 // force immediate redraw so TextView appears instantly 429 view->Window()->UpdateIfNeeded(); 430 } 431 } 432 433 434 void 435 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view, 436 BPose* pose, int32 poseIndex) 437 { 438 // find the text editing view 439 BView* scrollView = view->FindView("BorderView"); 440 ASSERT(scrollView); 441 if (!scrollView) 442 return; 443 444 BTextView* textView = dynamic_cast<BTextView*>( 445 scrollView->FindView("WidgetTextView")); 446 ASSERT(textView); 447 if (!textView) 448 return; 449 450 BColumn* column = view->ColumnFor(fAttrHash); 451 ASSERT(column); 452 if (!column) 453 return; 454 455 if (saveChanges && fText->CommitEditedText(textView)) { 456 // we have an actual change, re-sort 457 view->CheckPoseSortOrder(pose, poseIndex); 458 } 459 460 // make text widget visible again 461 SetVisible(true); 462 view->Invalidate(ColumnRect(poseLoc, column, view)); 463 464 // force immediate redraw so TEView disappears 465 scrollView->RemoveSelf(); 466 delete scrollView; 467 468 ASSERT(view->Window()); 469 view->Window()->UpdateIfNeeded(); 470 view->MakeFocus(); 471 472 SetActive(false); 473 } 474 475 476 void 477 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column, 478 BPoseView* view, bool visible) 479 { 480 BRect oldRect; 481 if (view->ViewMode() != kListMode) 482 oldRect = CalcOldRect(loc, column, view); 483 484 if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view) 485 && visible) { 486 BRect invalRect(ColumnRect(loc, column, view)); 487 if (view->ViewMode() != kListMode) 488 invalRect = invalRect | oldRect; 489 view->Invalidate(invalRect); 490 } 491 } 492 493 494 void 495 BTextWidget::SelectAll(BPoseView* view) 496 { 497 BTextView* text = dynamic_cast<BTextView*>( 498 view->FindView("WidgetTextView")); 499 if (text) 500 text->SelectAll(); 501 } 502 503 504 void 505 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view, 506 BView* drawView, bool selected, uint32 clipboardMode, BPoint offset, 507 bool direct) 508 { 509 textRect.OffsetBy(offset); 510 511 if (direct) { 512 // draw selection box if selected 513 if (selected) { 514 drawView->SetDrawingMode(B_OP_COPY); 515 // eraseRect.OffsetBy(offset); 516 // drawView->FillRect(eraseRect, B_SOLID_LOW); 517 drawView->FillRect(textRect, B_SOLID_LOW); 518 } else 519 drawView->SetDrawingMode(B_OP_OVER); 520 521 // set high color 522 rgb_color highColor; 523 if (view->IsDesktopWindow()) { 524 if (selected) 525 highColor = kWhite; 526 else 527 highColor = view->DeskTextColor(); 528 } else if (selected && view->Window()->IsActive()) { 529 highColor = kWhite; 530 } else 531 highColor = kBlack; 532 533 if (clipboardMode == kMoveSelectionTo && !selected) { 534 drawView->SetDrawingMode(B_OP_ALPHA); 535 drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 536 highColor.alpha = 64; 537 } 538 drawView->SetHighColor(highColor); 539 } 540 541 BPoint loc; 542 loc.y = textRect.bottom - view->FontInfo().descent; 543 loc.x = textRect.left + 1; 544 545 const char* fittingText = fText->FittingText(view); 546 547 // TODO: Comparing view and drawView here to avoid rendering 548 // the text outline when producing a drag bitmap. The check is 549 // not fully correct, since an offscreen view is also used in some 550 // other rare cases (something to do with columns). But for now, this 551 // fixes the broken drag bitmaps when dragging icons from the Desktop. 552 if (!selected && view == drawView && view->WidgetTextOutline()) { 553 // draw a halo around the text by using the "false bold" 554 // feature for text rendering. Either black or white is used for 555 // the glow (whatever acts as contrast) with a some alpha value, 556 drawView->SetDrawingMode(B_OP_ALPHA); 557 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 558 559 BFont font; 560 drawView->GetFont(&font); 561 562 rgb_color textColor = drawView->HighColor(); 563 if (textColor.red + textColor.green + textColor.blue < 128 * 3) { 564 // dark text on light outline 565 rgb_color glowColor = kWhite; 566 567 font.SetFalseBoldWidth(2.0); 568 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 569 glowColor.alpha = 30; 570 drawView->SetHighColor(glowColor); 571 572 drawView->DrawString(fittingText, loc); 573 574 font.SetFalseBoldWidth(1.0); 575 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 576 glowColor.alpha = 65; 577 drawView->SetHighColor(glowColor); 578 579 drawView->DrawString(fittingText, loc); 580 581 font.SetFalseBoldWidth(0.0); 582 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 583 } else { 584 // light text on dark outline 585 rgb_color outlineColor = kBlack; 586 587 font.SetFalseBoldWidth(1.0); 588 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 589 outlineColor.alpha = 30; 590 drawView->SetHighColor(outlineColor); 591 592 drawView->DrawString(fittingText, loc); 593 594 font.SetFalseBoldWidth(0.0); 595 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 596 597 outlineColor.alpha = 200; 598 drawView->SetHighColor(outlineColor); 599 600 drawView->DrawString(fittingText, loc + BPoint(1, 1)); 601 } 602 603 drawView->SetDrawingMode(B_OP_OVER); 604 drawView->SetHighColor(textColor); 605 } 606 607 drawView->DrawString(fittingText, loc); 608 609 if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) { 610 // TODO: 611 // this should be exported to the WidgetAttribute class, probably 612 // by having a per widget kind style 613 if (direct) { 614 rgb_color underlineColor = drawView->HighColor(); 615 underlineColor.alpha = 180; 616 drawView->SetHighColor(underlineColor); 617 drawView->SetDrawingMode(B_OP_ALPHA); 618 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 619 } 620 621 textRect.right = textRect.left + fText->Width(view); 622 // only underline text part 623 drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(), 624 B_MIXED_COLORS); 625 } 626 } 627