1 /* 2 * Copyright 2006-2012, 2023, Haiku, Inc. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Stephan Aßmus <superstippi@gmx.de> 7 * Zardshard 8 */ 9 10 #include "ShapeListView.h" 11 12 #include <new> 13 #include <stdio.h> 14 15 #include <Application.h> 16 #include <Catalog.h> 17 #include <ListItem.h> 18 #include <Locale.h> 19 #include <Menu.h> 20 #include <MenuItem.h> 21 #include <Message.h> 22 #include <Mime.h> 23 #include <Window.h> 24 25 #include "AddPathsCommand.h" 26 #include "AddShapesCommand.h" 27 #include "AddStylesCommand.h" 28 #include "CommandStack.h" 29 #include "CompoundCommand.h" 30 #include "Container.h" 31 #include "FreezeTransformationCommand.h" 32 #include "MainWindow.h" 33 #include "MoveShapesCommand.h" 34 #include "Observer.h" 35 #include "PathSourceShape.h" 36 #include "ReferenceImage.h" 37 #include "RemoveShapesCommand.h" 38 #include "ResetTransformationCommand.h" 39 #include "Selection.h" 40 #include "Shape.h" 41 #include "Style.h" 42 #include "Util.h" 43 #include "VectorPath.h" 44 45 #undef B_TRANSLATION_CONTEXT 46 #define B_TRANSLATION_CONTEXT "Icon-O-Matic-ShapesList" 47 48 49 using std::nothrow; 50 51 class ShapeListItem : public SimpleItem, public Observer { 52 public: 53 ShapeListItem(Shape* s, ShapeListView* listView) 54 : 55 SimpleItem(""), 56 shape(NULL), 57 fListView(listView) 58 { 59 SetShape(s); 60 } 61 62 63 virtual ~ShapeListItem() 64 { 65 SetShape(NULL); 66 } 67 68 69 virtual void ObjectChanged(const Observable* object) 70 { 71 UpdateText(); 72 } 73 74 void SetShape(Shape* s) 75 { 76 if (s == shape) 77 return; 78 79 if (shape) { 80 shape->RemoveObserver(this); 81 shape->ReleaseReference(); 82 } 83 84 shape = s; 85 86 if (shape) { 87 shape->AcquireReference(); 88 shape->AddObserver(this); 89 UpdateText(); 90 } 91 } 92 93 void UpdateText() 94 { 95 SetText(shape->Name()); 96 if (fListView->LockLooper()) { 97 fListView->InvalidateItem(fListView->IndexOf(this)); 98 fListView->UnlockLooper(); 99 } 100 } 101 102 public: 103 Shape* shape; 104 105 private: 106 ShapeListView* fListView; 107 }; 108 109 110 // #pragma mark - 111 112 113 enum { 114 MSG_REMOVE = 'rmsh', 115 MSG_DUPLICATE = 'dpsh', 116 MSG_RESET_TRANSFORMATION = 'rstr', 117 MSG_FREEZE_TRANSFORMATION = 'frzt', 118 119 MSG_DRAG_SHAPE = 'drgs', 120 }; 121 122 123 ShapeListView::ShapeListView(BRect frame, const char* name, BMessage* message, 124 BHandler* target) 125 : 126 SimpleListView(frame, name, NULL, B_MULTIPLE_SELECTION_LIST), 127 fMessage(message), 128 fShapeContainer(NULL), 129 fStyleContainer(NULL), 130 fPathContainer(NULL), 131 fCommandStack(NULL) 132 { 133 SetDragCommand(MSG_DRAG_SHAPE); 134 SetTarget(target); 135 } 136 137 138 ShapeListView::~ShapeListView() 139 { 140 _MakeEmpty(); 141 delete fMessage; 142 143 if (fShapeContainer != NULL) 144 fShapeContainer->RemoveListener(this); 145 } 146 147 148 void 149 ShapeListView::SelectionChanged() 150 { 151 SimpleListView::SelectionChanged(); 152 153 if (!fSyncingToSelection) { 154 ShapeListItem* item 155 = dynamic_cast<ShapeListItem*>(ItemAt(CurrentSelection(0))); 156 if (fMessage) { 157 BMessage message(*fMessage); 158 message.AddPointer("shape", item ? (void*)item->shape : NULL); 159 Invoke(&message); 160 } 161 } 162 163 _UpdateMenu(); 164 } 165 166 167 void 168 ShapeListView::MessageReceived(BMessage* message) 169 { 170 switch (message->what) { 171 case MSG_REMOVE: 172 RemoveSelected(); 173 break; 174 175 case MSG_DUPLICATE: 176 { 177 int32 count = CountSelectedItems(); 178 int32 index = 0; 179 BList items; 180 for (int32 i = 0; i < count; i++) { 181 index = CurrentSelection(i); 182 BListItem* item = ItemAt(index); 183 if (item) 184 items.AddItem((void*)item); 185 } 186 CopyItems(items, index + 1); 187 break; 188 } 189 190 case MSG_RESET_TRANSFORMATION: 191 { 192 BList shapes; 193 _GetSelectedShapes(shapes); 194 int32 count = shapes.CountItems(); 195 if (count < 0) 196 break; 197 198 Transformable* transformables[count]; 199 for (int32 i = 0; i < count; i++) { 200 Shape* shape = (Shape*)shapes.ItemAtFast(i); 201 transformables[i] = shape; 202 } 203 204 ResetTransformationCommand* command = 205 new ResetTransformationCommand(transformables, count); 206 207 fCommandStack->Perform(command); 208 break; 209 } 210 211 case MSG_FREEZE_TRANSFORMATION: 212 { 213 BList shapes; 214 _GetSelectedShapes(shapes); 215 int32 count = shapes.CountItems(); 216 if (count < 0) 217 break; 218 219 BList pathSourceShapes; 220 221 for (int i = 0; i < count; i++) { 222 Shape* shape = (Shape*) shapes.ItemAtFast(i); 223 if (dynamic_cast<PathSourceShape*>(shape) != NULL) 224 pathSourceShapes.AddItem(shape); 225 } 226 227 count = pathSourceShapes.CountItems(); 228 229 FreezeTransformationCommand* command 230 = new FreezeTransformationCommand( 231 (PathSourceShape**)pathSourceShapes.Items(), 232 count); 233 234 fCommandStack->Perform(command); 235 break; 236 } 237 238 default: 239 SimpleListView::MessageReceived(message); 240 break; 241 } 242 } 243 244 245 void 246 ShapeListView::MakeDragMessage(BMessage* message) const 247 { 248 SimpleListView::MakeDragMessage(message); 249 message->AddPointer("container", fShapeContainer); 250 int32 count = CountSelectedItems(); 251 for (int32 i = 0; i < count; i++) { 252 ShapeListItem* item = dynamic_cast<ShapeListItem*>( 253 ItemAt(CurrentSelection(i))); 254 if (item != NULL && item->shape != NULL) { 255 PathSourceShape* pathSourceShape = dynamic_cast<PathSourceShape*>(item->shape); 256 if (pathSourceShape != NULL) { 257 message->AddInt32("type", PathSourceShape::archive_code); 258 259 PathSourceShape* shape = pathSourceShape; 260 message->AddPointer("shape", (void*)shape); 261 262 // Add archives of everything this Shape uses 263 BMessage archive; 264 265 BMessage styleArchive; 266 shape->Style()->Archive(&styleArchive, true); 267 archive.AddMessage("style", &styleArchive); 268 269 Container<VectorPath>* paths = shape->Paths(); 270 for (int32 j = 0; j < paths->CountItems(); j++) { 271 BMessage pathArchive; 272 paths->ItemAt(j)->Archive(&pathArchive, true); 273 archive.AddMessage("path", &pathArchive); 274 } 275 276 BMessage shapeArchive; 277 shape->Archive(&shapeArchive, true); 278 archive.AddMessage("shape", &shapeArchive); 279 280 message->AddMessage("shape archive", &archive); 281 continue; 282 } 283 284 ReferenceImage* referenceImage = dynamic_cast<ReferenceImage*>(item->shape); 285 if (referenceImage != NULL) { 286 message->AddInt32("type", ReferenceImage::archive_code); 287 288 message->AddPointer("shape", (void*)referenceImage); 289 290 // Add archives of everything this Shape uses 291 BMessage archive; 292 293 BMessage shapeArchive; 294 referenceImage->Archive(&shapeArchive, true); 295 archive.AddMessage("shape", &shapeArchive); 296 297 message->AddMessage("shape archive", &archive); 298 continue; 299 } 300 } else 301 break; 302 } 303 } 304 305 306 bool 307 ShapeListView::AcceptDragMessage(const BMessage* message) const 308 { 309 return SimpleListView::AcceptDragMessage(message); 310 } 311 312 313 void 314 ShapeListView::SetDropTargetRect(const BMessage* message, BPoint where) 315 { 316 SimpleListView::SetDropTargetRect(message, where); 317 } 318 319 320 bool 321 ShapeListView::HandleDropMessage(const BMessage* message, int32 dropIndex) 322 { 323 // Let SimpleListView handle drag-sorting (when drag came from ourself) 324 if (SimpleListView::HandleDropMessage(message, dropIndex)) 325 return true; 326 327 if (fCommandStack == NULL || fShapeContainer == NULL 328 || fStyleContainer == NULL || fPathContainer == NULL) { 329 return false; 330 } 331 332 // Drag may have come from another instance, like in another window. 333 // Reconstruct the Shapes from the archive and add them at the drop 334 // index. 335 int index = 0; 336 BList styles; 337 BList paths; 338 BList shapes; 339 while (true) { 340 int32 type; 341 if (message->FindInt32("type", index, &type) != B_OK) 342 break; 343 344 if (type == PathSourceShape::archive_code) { 345 BMessage archive; 346 if (message->FindMessage("shape archive", index, &archive) != B_OK) 347 break; 348 349 // Extract the shape archive 350 BMessage shapeArchive; 351 if (archive.FindMessage("shape", &shapeArchive) != B_OK) 352 break; 353 354 // Extract the style 355 BMessage styleArchive; 356 if (archive.FindMessage("style", &styleArchive) != B_OK) 357 break; 358 359 Style* style = new Style(&styleArchive); 360 if (style == NULL) 361 break; 362 363 Style* styleToAssign = style; 364 // Try to find an existing style that is the same as the extracted 365 // style and use that one instead. 366 for (int32 i = 0; i < fStyleContainer->CountItems(); i++) { 367 Style* other = fStyleContainer->ItemAtFast(i); 368 if (*other == *style) { 369 styleToAssign = other; 370 delete style; 371 style = NULL; 372 break; 373 } 374 } 375 376 if (style != NULL && !styles.AddItem(style)) { 377 delete style; 378 break; 379 } 380 381 // Create the shape using the given style 382 PathSourceShape* shape = new(std::nothrow) PathSourceShape(styleToAssign); 383 if (shape == NULL) 384 break; 385 386 if (shape->Unarchive(&shapeArchive) != B_OK 387 || !shapes.AddItem(shape)) { 388 delete shape; 389 if (style != NULL) { 390 styles.RemoveItem(style); 391 delete style; 392 } 393 break; 394 } 395 396 // Extract the paths 397 int pathIndex = 0; 398 while (true) { 399 BMessage pathArchive; 400 if (archive.FindMessage("path", pathIndex, &pathArchive) != B_OK) 401 break; 402 403 VectorPath* path = new(nothrow) VectorPath(&pathArchive); 404 if (path == NULL) 405 break; 406 407 VectorPath* pathToInclude = path; 408 for (int32 i = 0; i < fPathContainer->CountItems(); i++) { 409 VectorPath* other = fPathContainer->ItemAtFast(i); 410 if (*other == *path) { 411 pathToInclude = other; 412 delete path; 413 path = NULL; 414 break; 415 } 416 } 417 418 if (path != NULL && !paths.AddItem(path)) { 419 delete path; 420 break; 421 } 422 423 shape->Paths()->AddItem(pathToInclude); 424 425 pathIndex++; 426 } 427 } else if (type == ReferenceImage::archive_code) { 428 BMessage archive; 429 if (message->FindMessage("shape archive", index, &archive) != B_OK) 430 break; 431 432 BMessage shapeArchive; 433 if (archive.FindMessage("shape", &shapeArchive) != B_OK) 434 break; 435 436 ReferenceImage* shape = new (std::nothrow) ReferenceImage(&shapeArchive); 437 if (shape == NULL) 438 break; 439 440 if (shapes.AddItem(shape) != B_OK) 441 break; 442 } 443 444 index++; 445 } 446 447 int32 shapeCount = shapes.CountItems(); 448 if (shapeCount == 0) 449 return false; 450 451 // TODO: Add allocation checks beyond this point. 452 453 AddStylesCommand* stylesCommand = new(std::nothrow) AddStylesCommand( 454 fStyleContainer, (Style**)styles.Items(), styles.CountItems(), 455 fStyleContainer->CountItems()); 456 457 AddPathsCommand* pathsCommand = new(std::nothrow) AddPathsCommand( 458 fPathContainer, (VectorPath**)paths.Items(), paths.CountItems(), 459 true, fPathContainer->CountItems()); 460 461 AddShapesCommand* shapesCommand = new(std::nothrow) AddShapesCommand( 462 fShapeContainer, (Shape**)shapes.Items(), shapeCount, dropIndex); 463 464 ::Command** commands = new(std::nothrow) ::Command*[3]; 465 466 commands[0] = stylesCommand; 467 commands[1] = pathsCommand; 468 commands[2] = shapesCommand; 469 470 CompoundCommand* command = new CompoundCommand(commands, 3, 471 B_TRANSLATE("Drop shapes"), -1); 472 473 fCommandStack->Perform(command); 474 475 return true; 476 } 477 478 479 // #pragma mark - 480 481 482 void 483 ShapeListView::MoveItems(BList& items, int32 toIndex) 484 { 485 if (fCommandStack == NULL || fShapeContainer == NULL) 486 return; 487 488 int32 count = items.CountItems(); 489 Shape** shapes = new(nothrow) Shape*[count]; 490 if (shapes == NULL) 491 return; 492 493 for (int32 i = 0; i < count; i++) { 494 ShapeListItem* item 495 = dynamic_cast<ShapeListItem*>((BListItem*)items.ItemAtFast(i)); 496 shapes[i] = item ? item->shape : NULL; 497 } 498 499 MoveShapesCommand* command = new (nothrow) MoveShapesCommand( 500 fShapeContainer, shapes, count, toIndex); 501 if (command == NULL) { 502 delete[] shapes; 503 return; 504 } 505 506 fCommandStack->Perform(command); 507 } 508 509 // CopyItems 510 void 511 ShapeListView::CopyItems(BList& items, int32 toIndex) 512 { 513 if (fCommandStack == NULL || fShapeContainer == NULL) 514 return; 515 516 int32 count = items.CountItems(); 517 Shape* shapes[count]; 518 519 for (int32 i = 0; i < count; i++) { 520 ShapeListItem* item 521 = dynamic_cast<ShapeListItem*>((BListItem*)items.ItemAtFast(i)); 522 shapes[i] = item ? item->shape->Clone() : NULL; 523 } 524 525 AddShapesCommand* command = new(nothrow) AddShapesCommand(fShapeContainer, 526 shapes, count, toIndex); 527 if (command == NULL) { 528 for (int32 i = 0; i < count; i++) 529 delete shapes[i]; 530 return; 531 } 532 533 fCommandStack->Perform(command); 534 } 535 536 537 void 538 ShapeListView::RemoveItemList(BList& items) 539 { 540 if (fCommandStack == NULL || fShapeContainer == NULL) 541 return; 542 543 int32 count = items.CountItems(); 544 int32 indices[count]; 545 for (int32 i = 0; i < count; i++) 546 indices[i] = IndexOf((BListItem*)items.ItemAtFast(i)); 547 548 RemoveShapesCommand* command = new(nothrow) RemoveShapesCommand( 549 fShapeContainer, indices, count); 550 551 fCommandStack->Perform(command); 552 } 553 554 555 BListItem* 556 ShapeListView::CloneItem(int32 index) const 557 { 558 ShapeListItem* item = dynamic_cast<ShapeListItem*>(ItemAt(index)); 559 if (item != NULL) { 560 return new ShapeListItem(item->shape, 561 const_cast<ShapeListView*>(this)); 562 } 563 return NULL; 564 } 565 566 567 int32 568 ShapeListView::IndexOfSelectable(Selectable* selectable) const 569 { 570 Shape* shape = dynamic_cast<Shape*>(selectable); 571 if (shape == NULL) { 572 Transformer* transformer = dynamic_cast<Transformer*>(selectable); 573 if (transformer == NULL) 574 return -1; 575 int32 count = CountItems(); 576 for (int32 i = 0; i < count; i++) { 577 ShapeListItem* item = dynamic_cast<ShapeListItem*>(ItemAt(i)); 578 if (item != NULL && item->shape->Transformers()->HasItem(transformer)) 579 return i; 580 } 581 } else { 582 int32 count = CountItems(); 583 for (int32 i = 0; i < count; i++) { 584 ShapeListItem* item = dynamic_cast<ShapeListItem*>(ItemAt(i)); 585 if (item != NULL && item->shape == shape) 586 return i; 587 } 588 } 589 590 return -1; 591 } 592 593 594 Selectable* 595 ShapeListView::SelectableFor(BListItem* item) const 596 { 597 ShapeListItem* shapeItem = dynamic_cast<ShapeListItem*>(item); 598 if (shapeItem != NULL) 599 return shapeItem->shape; 600 return NULL; 601 } 602 603 604 // #pragma mark - 605 606 607 void 608 ShapeListView::ItemAdded(Shape* shape, int32 index) 609 { 610 // NOTE: we are in the thread that messed with the 611 // ShapeContainer, so no need to lock the 612 // container, when this is changed to asynchronous 613 // notifications, then it would need to be read-locked! 614 if (!LockLooper()) 615 return; 616 617 if (_AddShape(shape, index)) 618 Select(index); 619 620 UnlockLooper(); 621 } 622 623 624 void 625 ShapeListView::ItemRemoved(Shape* shape) 626 { 627 // NOTE: we are in the thread that messed with the 628 // ShapeContainer, so no need to lock the 629 // container, when this is changed to asynchronous 630 // notifications, then it would need to be read-locked! 631 if (!LockLooper()) 632 return; 633 634 // NOTE: we're only interested in Shape objects 635 _RemoveShape(shape); 636 637 UnlockLooper(); 638 } 639 640 641 // #pragma mark - 642 643 644 void 645 ShapeListView::SetMenu(BMenu* menu) 646 { 647 if (fMenu == menu) 648 return; 649 650 fMenu = menu; 651 652 if (fMenu == NULL) 653 return; 654 655 BMessage* message = new BMessage(MSG_ADD_SHAPE); 656 fAddEmptyMI = new BMenuItem(B_TRANSLATE("Add empty"), message); 657 658 message = new BMessage(MSG_ADD_SHAPE); 659 message->AddBool("path", true); 660 fAddWidthPathMI = new BMenuItem(B_TRANSLATE("Add with path"), message); 661 662 message = new BMessage(MSG_ADD_SHAPE); 663 message->AddBool("style", true); 664 fAddWidthStyleMI = new BMenuItem(B_TRANSLATE("Add with style"), message); 665 666 message = new BMessage(MSG_ADD_SHAPE); 667 message->AddBool("path", true); 668 message->AddBool("style", true); 669 fAddWidthPathAndStyleMI = new BMenuItem( 670 B_TRANSLATE("Add with path & style"), message); 671 672 message = new BMessage(MSG_OPEN); 673 message->AddBool("reference image", true); 674 fAddReferenceImageMI = new BMenuItem(B_TRANSLATE("Add reference image"), message); 675 676 fDuplicateMI = new BMenuItem(B_TRANSLATE("Duplicate"), 677 new BMessage(MSG_DUPLICATE)); 678 fResetTransformationMI = new BMenuItem(B_TRANSLATE("Reset transformation"), 679 new BMessage(MSG_RESET_TRANSFORMATION)); 680 fFreezeTransformationMI = new BMenuItem( 681 B_TRANSLATE("Freeze transformation"), 682 new BMessage(MSG_FREEZE_TRANSFORMATION)); 683 684 fRemoveMI = new BMenuItem(B_TRANSLATE("Remove"), new BMessage(MSG_REMOVE)); 685 686 687 fMenu->AddItem(fAddEmptyMI); 688 fMenu->AddItem(fAddWidthPathMI); 689 fMenu->AddItem(fAddWidthStyleMI); 690 fMenu->AddItem(fAddWidthPathAndStyleMI); 691 692 fMenu->AddSeparatorItem(); 693 694 fMenu->AddItem(fAddReferenceImageMI); 695 696 fMenu->AddSeparatorItem(); 697 698 fMenu->AddItem(fDuplicateMI); 699 fMenu->AddItem(fResetTransformationMI); 700 fMenu->AddItem(fFreezeTransformationMI); 701 fMenu->AddSeparatorItem(); 702 703 fMenu->AddItem(fRemoveMI); 704 705 fDuplicateMI->SetTarget(this); 706 fResetTransformationMI->SetTarget(this); 707 fFreezeTransformationMI->SetTarget(this); 708 fRemoveMI->SetTarget(this); 709 710 _UpdateMenu(); 711 } 712 713 714 void 715 ShapeListView::SetShapeContainer(Container<Shape>* container) 716 { 717 if (fShapeContainer == container) 718 return; 719 720 // detach from old container 721 if (fShapeContainer != NULL) 722 fShapeContainer->RemoveListener(this); 723 724 _MakeEmpty(); 725 726 fShapeContainer = container; 727 728 if (fShapeContainer == NULL) 729 return; 730 731 fShapeContainer->AddListener(this); 732 733 // sync 734 int32 count = fShapeContainer->CountItems(); 735 for (int32 i = 0; i < count; i++) 736 _AddShape(fShapeContainer->ItemAtFast(i), i); 737 } 738 739 740 void 741 ShapeListView::SetStyleContainer(Container<Style>* container) 742 { 743 fStyleContainer = container; 744 } 745 746 747 void 748 ShapeListView::SetPathContainer(Container<VectorPath>* container) 749 { 750 fPathContainer = container; 751 } 752 753 754 void 755 ShapeListView::SetCommandStack(CommandStack* stack) 756 { 757 fCommandStack = stack; 758 } 759 760 761 // #pragma mark - 762 763 764 bool 765 ShapeListView::_AddShape(Shape* shape, int32 index) 766 { 767 if (shape == NULL) 768 return false; 769 770 ShapeListItem* item = new(std::nothrow) ShapeListItem(shape, this); 771 if (item == NULL) 772 return false; 773 774 if (!AddItem(item, index)) { 775 delete item; 776 return false; 777 } 778 779 return true; 780 } 781 782 783 bool 784 ShapeListView::_RemoveShape(Shape* shape) 785 { 786 ShapeListItem* item = _ItemForShape(shape); 787 if (item != NULL && RemoveItem(item)) { 788 delete item; 789 return true; 790 } 791 return false; 792 } 793 794 795 ShapeListItem* 796 ShapeListView::_ItemForShape(Shape* shape) const 797 { 798 int32 count = CountItems(); 799 for (int32 i = 0; i < count; i++) { 800 ShapeListItem* item = dynamic_cast<ShapeListItem*>(ItemAt(i)); 801 if (item != NULL && item->shape == shape) 802 return item; 803 } 804 return NULL; 805 } 806 807 808 void 809 ShapeListView::_UpdateMenu() 810 { 811 if (fMenu == NULL) 812 return; 813 814 bool gotSelection = CurrentSelection(0) >= 0; 815 816 fDuplicateMI->SetEnabled(gotSelection); 817 fResetTransformationMI->SetEnabled(gotSelection); 818 fFreezeTransformationMI->SetEnabled(gotSelection); 819 fRemoveMI->SetEnabled(gotSelection); 820 821 if (gotSelection) { 822 bool hasPathSourceShape = false; 823 824 int32 count = CountSelectedItems(); 825 for (int32 i = 0; i < count; i++) { 826 ShapeListItem* item 827 = dynamic_cast<ShapeListItem*>(ItemAt(CurrentSelection(i))); 828 bool isPathSourceShape 829 = item ? dynamic_cast<PathSourceShape*>(item->shape) != NULL : false; 830 hasPathSourceShape |= isPathSourceShape; 831 } 832 833 fFreezeTransformationMI->SetEnabled(hasPathSourceShape); 834 } 835 } 836 837 838 void 839 ShapeListView::_GetSelectedShapes(BList& shapes) const 840 { 841 int32 count = CountSelectedItems(); 842 for (int32 i = 0; i < count; i++) { 843 ShapeListItem* item = dynamic_cast<ShapeListItem*>( 844 ItemAt(CurrentSelection(i))); 845 if (item != NULL && item->shape != NULL) { 846 if (!shapes.AddItem((void*)item->shape)) 847 break; 848 } 849 } 850 } 851