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 "TextWidget.h" 37 38 #include <string.h> 39 #include <stdlib.h> 40 41 #include <Alert.h> 42 #include <Catalog.h> 43 #include <Clipboard.h> 44 #include <Debug.h> 45 #include <Directory.h> 46 #include <MessageFilter.h> 47 #include <ScrollView.h> 48 #include <TextView.h> 49 #include <Volume.h> 50 #include <Window.h> 51 52 #include "Attributes.h" 53 #include "ContainerWindow.h" 54 #include "Commands.h" 55 #include "FSUtils.h" 56 #include "PoseView.h" 57 #include "Utilities.h" 58 59 60 #undef B_TRANSLATION_CONTEXT 61 #define B_TRANSLATION_CONTEXT "TextWidget" 62 63 64 const float kWidthMargin = 20; 65 66 67 // #pragma mark - BTextWidget 68 69 70 BTextWidget::BTextWidget(Model* model, BColumn* column, BPoseView* view) 71 : 72 fText(WidgetAttributeText::NewWidgetText(model, column, view)), 73 fAttrHash(column->AttrHash()), 74 fAlignment(column->Alignment()), 75 fEditable(column->Editable()), 76 fVisible(true), 77 fActive(false), 78 fSymLink(model->IsSymLink()), 79 fMaxWidth(0), 80 fLastClickedTime(0) 81 { 82 } 83 84 85 BTextWidget::~BTextWidget() 86 { 87 if (fLastClickedTime != 0) 88 fParams.poseView->SetTextWidgetToCheck(NULL, this); 89 90 delete fText; 91 } 92 93 94 int 95 BTextWidget::Compare(const BTextWidget& with, BPoseView* view) const 96 { 97 return fText->Compare(*with.fText, view); 98 } 99 100 101 const char* 102 BTextWidget::Text(const BPoseView* view) const 103 { 104 StringAttributeText* textAttribute 105 = dynamic_cast<StringAttributeText*>(fText); 106 if (textAttribute == NULL) 107 return NULL; 108 109 return textAttribute->ValueAsText(view); 110 } 111 112 113 float 114 BTextWidget::TextWidth(const BPoseView* pose) const 115 { 116 return fText->Width(pose); 117 } 118 119 120 float 121 BTextWidget::PreferredWidth(const BPoseView* pose) const 122 { 123 return fText->PreferredWidth(pose); 124 } 125 126 127 BRect 128 BTextWidget::ColumnRect(BPoint poseLoc, const BColumn* column, 129 const BPoseView* view) 130 { 131 if (view->ViewMode() != kListMode) { 132 // ColumnRect only makes sense in list view, return 133 // CalcRect otherwise 134 return CalcRect(poseLoc, column, view); 135 } 136 BRect result; 137 result.left = column->Offset() + poseLoc.x; 138 result.right = result.left + column->Width(); 139 result.bottom = poseLoc.y 140 + roundf((view->ListElemHeight() + ActualFontHeight(view)) / 2); 141 result.top = result.bottom - floorf(ActualFontHeight(view)); 142 return result; 143 } 144 145 146 BRect 147 BTextWidget::CalcRectCommon(BPoint poseLoc, const BColumn* column, 148 const BPoseView* view, float textWidth) 149 { 150 textWidth -= 1; 151 BRect result; 152 float viewWidth = textWidth; 153 154 if (view->ViewMode() == kListMode) { 155 viewWidth = std::min(column->Width(), textWidth); 156 157 poseLoc.x += column->Offset(); 158 159 switch (fAlignment) { 160 case B_ALIGN_LEFT: 161 result.left = poseLoc.x; 162 result.right = result.left + viewWidth; 163 break; 164 165 case B_ALIGN_CENTER: 166 result.left = poseLoc.x 167 + roundf((column->Width() - viewWidth) / 2); 168 if (result.left < 0) 169 result.left = 0; 170 171 result.right = result.left + viewWidth; 172 break; 173 174 case B_ALIGN_RIGHT: 175 result.right = poseLoc.x + column->Width(); 176 result.left = result.right - viewWidth; 177 if (result.left < 0) 178 result.left = 0; 179 break; 180 181 default: 182 TRESPASS(); 183 break; 184 } 185 186 result.bottom = poseLoc.y 187 + roundf((view->ListElemHeight() + ActualFontHeight(view)) / 2); 188 } else { 189 viewWidth = std::min(view->StringWidth("M") * 30, textWidth); 190 if (view->ViewMode() == kIconMode) { 191 // icon mode 192 result.left = poseLoc.x 193 + roundf((view->IconSizeInt() - viewWidth) / 2); 194 } else { 195 // mini icon mode 196 result.left = poseLoc.x + view->IconSizeInt() + kMiniIconSeparator; 197 } 198 result.bottom = poseLoc.y + view->IconPoseHeight(); 199 200 result.right = result.left + viewWidth; 201 } 202 203 result.top = result.bottom - floorf(ActualFontHeight(view)); 204 205 return result; 206 } 207 208 209 BRect 210 BTextWidget::CalcRect(BPoint poseLoc, const BColumn* column, 211 const BPoseView* view) 212 { 213 return CalcRectCommon(poseLoc, column, view, fText->Width(view)); 214 } 215 216 217 BRect 218 BTextWidget::CalcOldRect(BPoint poseLoc, const BColumn* column, 219 const BPoseView* view) 220 { 221 return CalcRectCommon(poseLoc, column, view, fText->CurrentWidth()); 222 } 223 224 225 BRect 226 BTextWidget::CalcClickRect(BPoint poseLoc, const BColumn* column, 227 const BPoseView* view) 228 { 229 BRect result = CalcRect(poseLoc, column, view); 230 if (result.Width() < kWidthMargin) { 231 // if resulting rect too narrow, make it a bit wider 232 // for comfortable clicking 233 if (column != NULL && column->Width() < kWidthMargin) 234 result.right = result.left + column->Width(); 235 else 236 result.right = result.left + kWidthMargin; 237 } 238 239 return result; 240 } 241 242 243 void 244 BTextWidget::CheckExpiration() 245 { 246 if (IsEditable() && fParams.pose->IsSelected() && fLastClickedTime) { 247 bigtime_t doubleClickSpeed; 248 get_click_speed(&doubleClickSpeed); 249 250 bigtime_t delta = system_time() - fLastClickedTime; 251 252 if (delta > doubleClickSpeed) { 253 // at least 'doubleClickSpeed' microseconds ellapsed and no click 254 // was registered since. 255 fLastClickedTime = 0; 256 StartEdit(fParams.bounds, fParams.poseView, fParams.pose); 257 } 258 } else { 259 fLastClickedTime = 0; 260 fParams.poseView->SetTextWidgetToCheck(NULL); 261 } 262 } 263 264 265 void 266 BTextWidget::CancelWait() 267 { 268 fLastClickedTime = 0; 269 fParams.poseView->SetTextWidgetToCheck(NULL); 270 } 271 272 273 void 274 BTextWidget::MouseUp(BRect bounds, BPoseView* view, BPose* pose, BPoint) 275 { 276 // Register the time of that click. The PoseView, through its Pulse() 277 // will allow us to StartEdit() if no other click have been registered since 278 // then. 279 280 // TODO: re-enable modifiers, one should be enough 281 view->SetTextWidgetToCheck(NULL); 282 if (IsEditable() && pose->IsSelected()) { 283 bigtime_t doubleClickSpeed; 284 get_click_speed(&doubleClickSpeed); 285 286 if (fLastClickedTime == 0) { 287 fLastClickedTime = system_time(); 288 if (fLastClickedTime - doubleClickSpeed < pose->SelectionTime()) 289 fLastClickedTime = 0; 290 } else 291 fLastClickedTime = 0; 292 293 if (fLastClickedTime == 0) 294 return; 295 296 view->SetTextWidgetToCheck(this); 297 298 fParams.pose = pose; 299 fParams.bounds = bounds; 300 fParams.poseView = view; 301 } else 302 fLastClickedTime = 0; 303 } 304 305 306 static filter_result 307 TextViewKeyDownFilter(BMessage* message, BHandler**, BMessageFilter* filter) 308 { 309 uchar key; 310 if (message->FindInt8("byte", (int8*)&key) != B_OK) 311 return B_DISPATCH_MESSAGE; 312 313 ThrowOnAssert(filter != NULL); 314 315 BContainerWindow* window = dynamic_cast<BContainerWindow*>( 316 filter->Looper()); 317 ThrowOnAssert(window != NULL); 318 319 BPoseView* view = window->PoseView(); 320 ThrowOnAssert(view != NULL); 321 322 if (key == B_RETURN || key == B_ESCAPE) { 323 view->CommitActivePose(key == B_RETURN); 324 return B_SKIP_MESSAGE; 325 } 326 327 if (key == B_TAB) { 328 if (view->ActivePose()) { 329 if (message->FindInt32("modifiers") & B_SHIFT_KEY) 330 view->ActivePose()->EditPreviousWidget(view); 331 else 332 view->ActivePose()->EditNextWidget(view); 333 } 334 335 return B_SKIP_MESSAGE; 336 } 337 338 // the BTextView doesn't respect window borders when resizing itself; 339 // we try to work-around this "bug" here. 340 341 // find the text editing view 342 BView* scrollView = view->FindView("BorderView"); 343 if (scrollView != NULL) { 344 BTextView* textView = dynamic_cast<BTextView*>( 345 scrollView->FindView("WidgetTextView")); 346 if (textView != NULL) { 347 ASSERT(view->ActiveTextWidget() != NULL); 348 float maxWidth = view->ActiveTextWidget()->MaxWidth(); 349 bool tooWide = textView->TextRect().Width() > maxWidth; 350 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 351 } 352 } 353 354 return B_DISPATCH_MESSAGE; 355 } 356 357 358 static filter_result 359 TextViewPasteFilter(BMessage* message, BHandler**, BMessageFilter* filter) 360 { 361 ThrowOnAssert(filter != NULL); 362 363 BContainerWindow* window = dynamic_cast<BContainerWindow*>( 364 filter->Looper()); 365 ThrowOnAssert(window != NULL); 366 367 BPoseView* view = window->PoseView(); 368 ThrowOnAssert(view != NULL); 369 370 // the BTextView doesn't respect window borders when resizing itself; 371 // we try to work-around this "bug" here. 372 373 // find the text editing view 374 BView* scrollView = view->FindView("BorderView"); 375 if (scrollView != NULL) { 376 BTextView* textView = dynamic_cast<BTextView*>( 377 scrollView->FindView("WidgetTextView")); 378 if (textView != NULL) { 379 float textWidth = textView->TextRect().Width(); 380 381 // subtract out selected text region width 382 int32 start, finish; 383 textView->GetSelection(&start, &finish); 384 if (start != finish) { 385 BRegion selectedRegion; 386 textView->GetTextRegion(start, finish, &selectedRegion); 387 textWidth -= selectedRegion.Frame().Width(); 388 } 389 390 // add pasted text width 391 if (be_clipboard->Lock()) { 392 BMessage* clip = be_clipboard->Data(); 393 if (clip != NULL) { 394 const char* text = NULL; 395 ssize_t length = 0; 396 397 if (clip->FindData("text/plain", B_MIME_TYPE, 398 (const void**)&text, &length) == B_OK) { 399 textWidth += textView->StringWidth(text); 400 } 401 } 402 403 be_clipboard->Unlock(); 404 } 405 406 // check if pasted text is too wide 407 ASSERT(view->ActiveTextWidget() != NULL); 408 float maxWidth = view->ActiveTextWidget()->MaxWidth(); 409 bool tooWide = textWidth > maxWidth; 410 411 if (tooWide) { 412 // resize text view to max width 413 414 // move scroll view if not left aligned 415 float oldWidth = textView->Bounds().Width(); 416 float newWidth = maxWidth; 417 float right = oldWidth - newWidth; 418 419 if (textView->Alignment() == B_ALIGN_CENTER) 420 scrollView->MoveBy(roundf(right / 2), 0); 421 else if (textView->Alignment() == B_ALIGN_RIGHT) 422 scrollView->MoveBy(right, 0); 423 424 // resize scroll view 425 float grow = newWidth - oldWidth; 426 scrollView->ResizeBy(grow, 0); 427 } 428 429 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 430 } 431 } 432 433 return B_DISPATCH_MESSAGE; 434 } 435 436 437 void 438 BTextWidget::StartEdit(BRect bounds, BPoseView* view, BPose* pose) 439 { 440 view->SetTextWidgetToCheck(NULL, this); 441 if (!IsEditable() || IsActive()) 442 return; 443 444 view->SetActiveTextWidget(this); 445 446 BRect rect(bounds); 447 rect.OffsetBy(view->ViewMode() == kListMode ? -2 : 0, -2); 448 BTextView* textView = new BTextView(rect, "WidgetTextView", rect, 449 be_plain_font, 0, B_FOLLOW_ALL, B_WILL_DRAW); 450 451 textView->SetWordWrap(false); 452 textView->SetInsets(2, 2, 2, 2); 453 DisallowMetaKeys(textView); 454 fText->SetupEditing(textView); 455 456 textView->AddFilter(new BMessageFilter(B_KEY_DOWN, TextViewKeyDownFilter)); 457 458 if (view->SelectedVolumeIsReadOnly()) { 459 textView->MakeEditable(false); 460 textView->MakeSelectable(true); 461 // tint text view background color to indicate not editable 462 textView->SetViewColor(tint_color(textView->ViewColor(), 463 ReadOnlyTint(textView->ViewColor()))); 464 } else 465 textView->AddFilter(new BMessageFilter(B_PASTE, TextViewPasteFilter)); 466 467 // get full text length 468 rect.right = rect.left + textView->LineWidth(); 469 rect.bottom = rect.top + textView->LineHeight() - 1 + 4; 470 471 if (view->ViewMode() == kListMode) { 472 // limit max width to column width in list mode 473 BColumn* column = view->ColumnFor(fAttrHash); 474 ASSERT(column != NULL); 475 fMaxWidth = column->Width(); 476 } else { 477 // limit max width to 30em in icon and mini icon mode 478 fMaxWidth = textView->StringWidth("M") * 30; 479 480 if (textView->LineWidth() > fMaxWidth 481 || view->ViewMode() == kMiniIconMode) { 482 // compensate for text going over right inset 483 rect.OffsetBy(-2, 0); 484 } 485 } 486 487 // resize textView 488 textView->MoveTo(rect.LeftTop()); 489 textView->ResizeTo(std::min(fMaxWidth, rect.Width()), rect.Height()); 490 textView->SetTextRect(rect); 491 492 // set alignment before adding textView so it doesn't redraw 493 switch (view->ViewMode()) { 494 case kIconMode: 495 textView->SetAlignment(B_ALIGN_CENTER); 496 break; 497 498 case kMiniIconMode: 499 textView->SetAlignment(B_ALIGN_LEFT); 500 break; 501 502 case kListMode: 503 textView->SetAlignment(fAlignment); 504 break; 505 } 506 507 BScrollView* scrollView = new BScrollView("BorderView", textView, 0, 0, 508 false, false, B_PLAIN_BORDER); 509 view->AddChild(scrollView); 510 511 bool tooWide = textView->TextRect().Width() > fMaxWidth; 512 textView->MakeResizable(!tooWide, tooWide ? NULL : scrollView); 513 514 view->SetActivePose(pose); 515 // tell view about pose 516 SetActive(true); 517 // for widget 518 519 textView->SelectAll(); 520 textView->ScrollToSelection(); 521 // scroll to beginning so that text is visible 522 textView->MakeFocus(); 523 524 // make this text widget invisible while we edit it 525 SetVisible(false); 526 527 ASSERT(view->Window() != NULL); 528 // how can I not have a Window here??? 529 530 if (view->Window()) { 531 // force immediate redraw so TextView appears instantly 532 view->Window()->UpdateIfNeeded(); 533 } 534 } 535 536 537 void 538 BTextWidget::StopEdit(bool saveChanges, BPoint poseLoc, BPoseView* view, 539 BPose* pose, int32 poseIndex) 540 { 541 view->SetActiveTextWidget(NULL); 542 543 // find the text editing view 544 BView* scrollView = view->FindView("BorderView"); 545 ASSERT(scrollView != NULL); 546 if (scrollView == NULL) 547 return; 548 549 BTextView* textView = dynamic_cast<BTextView*>( 550 scrollView->FindView("WidgetTextView")); 551 ASSERT(textView != NULL); 552 if (textView == NULL) 553 return; 554 555 BColumn* column = view->ColumnFor(fAttrHash); 556 ASSERT(column != NULL); 557 if (column == NULL) 558 return; 559 560 if (saveChanges && fText->CommitEditedText(textView)) { 561 // we have an actual change, re-sort 562 view->CheckPoseSortOrder(pose, poseIndex); 563 } 564 565 // make text widget visible again 566 SetVisible(true); 567 view->Invalidate(ColumnRect(poseLoc, column, view)); 568 569 // force immediate redraw so TEView disappears 570 scrollView->RemoveSelf(); 571 delete scrollView; 572 573 ASSERT(view->Window() != NULL); 574 view->Window()->UpdateIfNeeded(); 575 view->MakeFocus(); 576 577 SetActive(false); 578 } 579 580 581 void 582 BTextWidget::CheckAndUpdate(BPoint loc, const BColumn* column, 583 BPoseView* view, bool visible) 584 { 585 BRect oldRect; 586 if (view->ViewMode() != kListMode) 587 oldRect = CalcOldRect(loc, column, view); 588 589 if (fText->CheckAttributeChanged() && fText->CheckViewChanged(view) 590 && visible) { 591 BRect invalRect(ColumnRect(loc, column, view)); 592 if (view->ViewMode() != kListMode) 593 invalRect = invalRect | oldRect; 594 view->Invalidate(invalRect); 595 } 596 } 597 598 599 void 600 BTextWidget::SelectAll(BPoseView* view) 601 { 602 BTextView* text = dynamic_cast<BTextView*>( 603 view->FindView("WidgetTextView")); 604 if (text != NULL) 605 text->SelectAll(); 606 } 607 608 609 void 610 BTextWidget::Draw(BRect eraseRect, BRect textRect, float, BPoseView* view, 611 BView* drawView, bool selected, uint32 clipboardMode, BPoint offset, 612 bool direct) 613 { 614 textRect.OffsetBy(offset); 615 616 // We are only concerned with setting the correct text color. 617 618 // For active views the selection is drawn as inverse text 619 // (background color for the text, solid black for the background). 620 // For inactive windows the text is drawn normally, then the 621 // selection rect is alpha-blended on top. This all happens in 622 // BPose::Draw before and after calling this function. 623 624 if (direct) { 625 // draw selection box if selected 626 if (selected) { 627 drawView->SetDrawingMode(B_OP_COPY); 628 drawView->FillRect(textRect, B_SOLID_LOW); 629 } else 630 drawView->SetDrawingMode(B_OP_OVER); 631 632 // set high color 633 rgb_color highColor; 634 highColor = view->TextColor(selected && view->Window()->IsActive()); 635 636 if (clipboardMode == kMoveSelectionTo && !selected) { 637 drawView->SetDrawingMode(B_OP_ALPHA); 638 drawView->SetBlendingMode(B_PIXEL_ALPHA, B_ALPHA_OVERLAY); 639 highColor.alpha = 64; 640 } 641 drawView->SetHighColor(highColor); 642 } else if (selected && view->Window()->IsActive()) 643 drawView->SetHighColor(view->BackColor(true)); // inverse 644 else if (!selected) 645 drawView->SetHighColor(view->TextColor()); 646 647 BPoint location; 648 location.y = textRect.bottom - view->FontInfo().descent + 1; 649 location.x = textRect.left; 650 651 const char* fittingText = fText->FittingText(view); 652 653 // TODO: Comparing view and drawView here to avoid rendering 654 // the text outline when producing a drag bitmap. The check is 655 // not fully correct, since an offscreen view is also used in some 656 // other rare cases (something to do with columns). But for now, this 657 // fixes the broken drag bitmaps when dragging icons from the Desktop. 658 if (direct && !selected && view->WidgetTextOutline()) { 659 // draw a halo around the text by using the "false bold" 660 // feature for text rendering. Either black or white is used for 661 // the glow (whatever acts as contrast) with a some alpha value, 662 drawView->SetDrawingMode(B_OP_ALPHA); 663 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 664 665 BFont font; 666 drawView->GetFont(&font); 667 668 rgb_color textColor = view->TextColor(); 669 if (textColor.IsDark()) { 670 // dark text on light outline 671 rgb_color glowColor = ui_color(B_SHINE_COLOR); 672 673 font.SetFalseBoldWidth(2.0); 674 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 675 glowColor.alpha = 30; 676 drawView->SetHighColor(glowColor); 677 678 drawView->DrawString(fittingText, location); 679 680 font.SetFalseBoldWidth(1.0); 681 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 682 glowColor.alpha = 65; 683 drawView->SetHighColor(glowColor); 684 685 drawView->DrawString(fittingText, location); 686 687 font.SetFalseBoldWidth(0.0); 688 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 689 } else { 690 // light text on dark outline 691 rgb_color outlineColor = kBlack; 692 693 font.SetFalseBoldWidth(1.0); 694 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 695 outlineColor.alpha = 30; 696 drawView->SetHighColor(outlineColor); 697 698 drawView->DrawString(fittingText, location); 699 700 font.SetFalseBoldWidth(0.0); 701 drawView->SetFont(&font, B_FONT_FALSE_BOLD_WIDTH); 702 703 outlineColor.alpha = 200; 704 drawView->SetHighColor(outlineColor); 705 706 drawView->DrawString(fittingText, location + BPoint(1, 1)); 707 } 708 709 drawView->SetDrawingMode(B_OP_OVER); 710 drawView->SetHighColor(textColor); 711 } 712 713 drawView->DrawString(fittingText, location); 714 715 if (fSymLink && (fAttrHash == view->FirstColumn()->AttrHash())) { 716 // TODO: 717 // this should be exported to the WidgetAttribute class, probably 718 // by having a per widget kind style 719 if (direct) { 720 rgb_color underlineColor = drawView->HighColor(); 721 underlineColor.alpha = 180; 722 drawView->SetHighColor(underlineColor); 723 drawView->SetDrawingMode(B_OP_ALPHA); 724 drawView->SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY); 725 } 726 727 textRect.right = textRect.left + fText->Width(view); 728 // only underline text part 729 drawView->StrokeLine(textRect.LeftBottom(), textRect.RightBottom(), 730 B_MIXED_COLORS); 731 } 732 } 733