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