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