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