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