1 /* 2 * Copyright 2006-2007, 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 */ 8 9 #include "PropertyListView.h" 10 11 #include <stdio.h> 12 #include <string.h> 13 14 #include <ClassInfo.h> 15 #include <Clipboard.h> 16 #ifdef __HAIKU__ 17 # include <LayoutUtils.h> 18 #endif 19 #include <Menu.h> 20 #include <MenuItem.h> 21 #include <Message.h> 22 #include <Window.h> 23 24 #include "CommonPropertyIDs.h" 25 //#include "LanguageManager.h" 26 #include "Property.h" 27 #include "PropertyItemView.h" 28 #include "PropertyObject.h" 29 #include "Scrollable.h" 30 #include "Scroller.h" 31 #include "ScrollView.h" 32 33 enum { 34 MSG_COPY_PROPERTIES = 'cppr', 35 MSG_PASTE_PROPERTIES = 'pspr', 36 37 MSG_ADD_KEYFRAME = 'adkf', 38 39 MSG_SELECT_ALL = B_SELECT_ALL, 40 MSG_SELECT_NONE = 'slnn', 41 MSG_INVERT_SELECTION = 'invs', 42 }; 43 44 // TabFilter class 45 46 class TabFilter : public BMessageFilter { 47 public: 48 TabFilter(PropertyListView* target) 49 : BMessageFilter(B_ANY_DELIVERY, B_ANY_SOURCE), 50 fTarget(target) 51 { 52 } 53 virtual ~TabFilter() 54 { 55 } 56 virtual filter_result Filter(BMessage* message, BHandler** target) 57 { 58 filter_result result = B_DISPATCH_MESSAGE; 59 switch (message->what) { 60 case B_UNMAPPED_KEY_DOWN: 61 case B_KEY_DOWN: { 62 uint32 key; 63 uint32 modifiers; 64 if (message->FindInt32("raw_char", (int32*)&key) >= B_OK 65 && message->FindInt32("modifiers", (int32*)&modifiers) >= B_OK) 66 if (key == B_TAB && fTarget->TabFocus(modifiers & B_SHIFT_KEY)) 67 result = B_SKIP_MESSAGE; 68 break; 69 } 70 default: 71 break; 72 } 73 return result; 74 } 75 private: 76 PropertyListView* fTarget; 77 }; 78 79 80 // constructor 81 PropertyListView::PropertyListView() 82 : BView(BRect(0.0, 0.0, 100.0, 100.0), NULL, B_FOLLOW_NONE, 83 B_WILL_DRAW | B_FRAME_EVENTS | B_NAVIGABLE), 84 Scrollable(), 85 BList(20), 86 fClipboard(new BClipboard("icon-o-matic properties")), 87 88 fPropertyM(NULL), 89 90 fPropertyObject(NULL), 91 fSavedProperties(new PropertyObject()), 92 93 fLastClickedItem(NULL), 94 fSuspendUpdates(false), 95 96 fMouseWheelFilter(new MouseWheelFilter(this)), 97 fTabFilter(new TabFilter(this)) 98 { 99 SetLowColor(255, 255, 255, 255); 100 SetHighColor(0, 0, 0, 255); 101 SetViewColor(B_TRANSPARENT_32_BIT); 102 } 103 104 // destructor 105 PropertyListView::~PropertyListView() 106 { 107 delete fClipboard; 108 109 delete fPropertyObject; 110 delete fSavedProperties; 111 112 delete fMouseWheelFilter; 113 delete fTabFilter; 114 } 115 116 // AttachedToWindow 117 void 118 PropertyListView::AttachedToWindow() 119 { 120 Window()->AddCommonFilter(fMouseWheelFilter); 121 Window()->AddCommonFilter(fTabFilter); 122 } 123 124 // DetachedFromWindow 125 void 126 PropertyListView::DetachedFromWindow() 127 { 128 Window()->RemoveCommonFilter(fTabFilter); 129 Window()->RemoveCommonFilter(fMouseWheelFilter); 130 } 131 132 // FrameResized 133 void 134 PropertyListView::FrameResized(float width, float height) 135 { 136 SetVisibleSize(width, height); 137 Invalidate(); 138 } 139 140 // Draw 141 void 142 PropertyListView::Draw(BRect updateRect) 143 { 144 if (!fSuspendUpdates) 145 FillRect(updateRect, B_SOLID_LOW); 146 } 147 148 // MakeFocus 149 void 150 PropertyListView::MakeFocus(bool focus) 151 { 152 if (focus == IsFocus()) 153 return; 154 155 BView::MakeFocus(focus); 156 if (::ScrollView* scrollView = dynamic_cast< ::ScrollView*>(Parent())) 157 scrollView->ChildFocusChanged(focus); 158 } 159 160 // MouseDown 161 void 162 PropertyListView::MouseDown(BPoint where) 163 { 164 if (!(modifiers() & B_SHIFT_KEY)) { 165 DeselectAll(); 166 } 167 MakeFocus(true); 168 } 169 170 // MessageReceived 171 void 172 PropertyListView::MessageReceived(BMessage* message) 173 { 174 switch (message->what) { 175 case MSG_PASTE_PROPERTIES: { 176 if (!fPropertyObject || !fClipboard->Lock()) 177 break; 178 179 BMessage* data = fClipboard->Data(); 180 if (!data) { 181 fClipboard->Unlock(); 182 break; 183 } 184 185 PropertyObject propertyObject; 186 BMessage archive; 187 for (int32 i = 0; 188 data->FindMessage("property", i, &archive) >= B_OK; 189 i++) { 190 BArchivable* archivable = instantiate_object(&archive); 191 if (!archivable) 192 continue; 193 // see if this is actually a property 194 Property* property = cast_as(archivable, Property); 195 if (!property || !propertyObject.AddProperty(property)) 196 delete archivable; 197 } 198 if (propertyObject.CountProperties() > 0) 199 PasteProperties(&propertyObject); 200 fClipboard->Unlock(); 201 break; 202 } 203 case MSG_COPY_PROPERTIES: { 204 if (!fPropertyObject || !fClipboard->Lock()) 205 break; 206 207 BMessage* data = fClipboard->Data(); 208 if (!data) { 209 fClipboard->Unlock(); 210 break; 211 } 212 213 fClipboard->Clear(); 214 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 215 if (!item->IsSelected()) 216 continue; 217 const Property* property = item->GetProperty(); 218 if (property) { 219 BMessage archive; 220 if (property->Archive(&archive) >= B_OK) { 221 data->AddMessage("property", &archive); 222 } 223 } 224 } 225 fClipboard->Commit(); 226 fClipboard->Unlock(); 227 _CheckMenuStatus(); 228 break; 229 } 230 231 // property selection 232 case MSG_SELECT_ALL: 233 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 234 item->SetSelected(true); 235 } 236 _CheckMenuStatus(); 237 break; 238 case MSG_SELECT_NONE: 239 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 240 item->SetSelected(false); 241 } 242 _CheckMenuStatus(); 243 break; 244 case MSG_INVERT_SELECTION: 245 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 246 item->SetSelected(!item->IsSelected()); 247 } 248 _CheckMenuStatus(); 249 break; 250 251 default: 252 BView::MessageReceived(message); 253 } 254 } 255 256 #ifdef __HAIKU__ 257 258 BSize 259 PropertyListView::MinSize() 260 { 261 // We need a stable min size: the BView implementation uses 262 // GetPreferredSize(), which by default just returns the current size. 263 return BLayoutUtils::ComposeSize(ExplicitMinSize(), BSize(10, 10)); 264 } 265 266 267 BSize 268 PropertyListView::MaxSize() 269 { 270 return BView::MaxSize(); 271 } 272 273 274 BSize 275 PropertyListView::PreferredSize() 276 { 277 // We need a stable preferred size: the BView implementation uses 278 // GetPreferredSize(), which by default just returns the current size. 279 return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), BSize(100, 50)); 280 } 281 282 #endif // __HAIKU__ 283 284 // #pragma mark - 285 286 // TabFocus 287 bool 288 PropertyListView::TabFocus(bool shift) 289 { 290 bool result = false; 291 PropertyItemView* item = NULL; 292 if (IsFocus() && !shift) { 293 item = _ItemAt(0); 294 } else { 295 int32 focussedIndex = -1; 296 for (int32 i = 0; PropertyItemView* oldItem = _ItemAt(i); i++) { 297 if (oldItem->IsFocused()) { 298 focussedIndex = shift ? i - 1 : i + 1; 299 break; 300 } 301 } 302 item = _ItemAt(focussedIndex); 303 } 304 if (item) { 305 item->MakeFocus(true); 306 result = true; 307 } 308 return result; 309 } 310 311 // SetMenu 312 void 313 PropertyListView::SetMenu(BMenu* menu) 314 { 315 fPropertyM = menu; 316 if (!fPropertyM) 317 return; 318 319 fSelectM = new BMenu("Select"); 320 fSelectAllMI = new BMenuItem("All", new BMessage(MSG_SELECT_ALL)); 321 fSelectM->AddItem(fSelectAllMI); 322 fSelectNoneMI = new BMenuItem("None", new BMessage(MSG_SELECT_NONE)); 323 fSelectM->AddItem(fSelectNoneMI); 324 fInvertSelectionMI = new BMenuItem("Invert Selection", new BMessage(MSG_INVERT_SELECTION)); 325 fSelectM->AddItem(fInvertSelectionMI); 326 fSelectM->SetTargetForItems(this); 327 328 fPropertyM->AddItem(fSelectM); 329 330 fPropertyM->AddSeparatorItem(); 331 332 fCopyMI = new BMenuItem("Copy", new BMessage(MSG_COPY_PROPERTIES)); 333 fPropertyM->AddItem(fCopyMI); 334 fPasteMI = new BMenuItem("Paste", new BMessage(MSG_PASTE_PROPERTIES)); 335 fPropertyM->AddItem(fPasteMI); 336 337 fPropertyM->SetTargetForItems(this); 338 339 // disable menus 340 _CheckMenuStatus(); 341 } 342 343 // UpdateStrings 344 void 345 PropertyListView::UpdateStrings() 346 { 347 // if (fSelectM) { 348 // LanguageManager* m = LanguageManager::Default(); 349 // 350 // fSelectM->Superitem()->SetLabel(m->GetString(PROPERTY_SELECTION, "Select")); 351 // fSelectAllMI->SetLabel(m->GetString(SELECT_ALL_PROPERTIES, "All")); 352 // fSelectNoneMI->SetLabel(m->GetString(SELECT_NO_PROPERTIES, "None")); 353 // fInvertSelectionMI->SetLabel(m->GetString(INVERT_SELECTION, "Invert Selection")); 354 // 355 // fPropertyM->Superitem()->SetLabel(m->GetString(PROPERTY, "Property")); 356 // fCopyMI->SetLabel(m->GetString(COPY, "Copy")); 357 // if (IsEditingMultipleObjects()) 358 // fPasteMI->SetLabel(m->GetString(MULTI_PASTE, "Multi Paste")); 359 // else 360 // fPasteMI->SetLabel(m->GetString(PASTE, "Paste")); 361 // } 362 } 363 364 // ScrollView 365 ::ScrollView* 366 PropertyListView::ScrollView() const 367 { 368 return dynamic_cast< ::ScrollView*>(ScrollSource()); 369 } 370 371 // #pragma mark - 372 373 // SetTo 374 void 375 PropertyListView::SetTo(PropertyObject* object) 376 { 377 // try to do without rebuilding the list 378 // it should in fact be pretty unlikely that this does not 379 // work, but we keep being defensive 380 if (fPropertyObject && object && 381 fPropertyObject->ContainsSameProperties(*object)) { 382 // iterate over view items and update their value views 383 bool error = false; 384 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 385 Property* property = object->PropertyAt(i); 386 if (!item->AdoptProperty(property)) { 387 // the reason for this can be that the property is 388 // unkown to the PropertyEditorFactory and therefor 389 // there is no editor view at this item 390 fprintf(stderr, "PropertyListView::_SetTo() - " 391 "property mismatch at %ld\n", i); 392 error = true; 393 break; 394 } 395 if (property) 396 item->SetEnabled(property->IsEditable()); 397 } 398 // we didn't need to make empty, but transfer ownership 399 // of the object 400 if (!error) { 401 // if the "adopt" process went only halfway, 402 // some properties of the original object 403 // are still referenced, so we can only 404 // delete the original object if the process 405 // was successful and leak Properties otherwise, 406 // but this case is only theoretical anyways... 407 delete fPropertyObject; 408 } 409 fPropertyObject = object; 410 } else { 411 // remember scroll pos, selection and focused item 412 BPoint scrollOffset = ScrollOffset(); 413 BList selection(20); 414 int32 focused = -1; 415 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 416 if (item->IsSelected()) 417 selection.AddItem((void*)i); 418 if (item->IsFocused()) 419 focused = i; 420 } 421 if (Window()) 422 Window()->BeginViewTransaction(); 423 fSuspendUpdates = true; 424 425 // rebuild list 426 _MakeEmpty(); 427 fPropertyObject = object; 428 429 if (fPropertyObject) { 430 // fill with content 431 for (int32 i = 0; Property* property = fPropertyObject->PropertyAt(i); i++) { 432 PropertyItemView* item = new PropertyItemView(property); 433 item->SetEnabled(property->IsEditable()); 434 _AddItem(item); 435 } 436 _LayoutItems(); 437 438 // restore scroll pos, selection and focus 439 SetScrollOffset(scrollOffset); 440 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 441 if (selection.HasItem((void*)i)) 442 item->SetSelected(true); 443 if (i == focused) 444 item->MakeFocus(true); 445 } 446 } 447 448 if (Window()) 449 Window()->EndViewTransaction(); 450 fSuspendUpdates = false; 451 452 SetDataRect(_ItemsRect()); 453 } 454 455 _UpdateSavedProperties(); 456 _CheckMenuStatus(); 457 Invalidate(); 458 } 459 460 // PropertyChanged 461 void 462 PropertyListView::PropertyChanged(const Property* previous, 463 const Property* current) 464 { 465 printf("PropertyListView::PropertyChanged(%s)\n", 466 name_for_id(current->Identifier())); 467 } 468 469 // PasteProperties 470 void 471 PropertyListView::PasteProperties(const PropertyObject* object) 472 { 473 if (!fPropertyObject) 474 return; 475 476 // default implementation is to adopt the pasted properties 477 int32 count = object->CountProperties(); 478 for (int32 i = 0; i < count; i++) { 479 Property* p = object->PropertyAtFast(i); 480 Property* local = fPropertyObject->FindProperty(p->Identifier()); 481 if (local) 482 local->SetValue(p); 483 } 484 } 485 486 // IsEditingMultipleObjects 487 bool 488 PropertyListView::IsEditingMultipleObjects() 489 { 490 return false; 491 } 492 493 // #pragma mark - 494 495 // UpdateObject 496 void 497 PropertyListView::UpdateObject(uint32 propertyID) 498 { 499 Property* previous = fSavedProperties->FindProperty(propertyID); 500 Property* current = fPropertyObject->FindProperty(propertyID); 501 if (previous && current) { 502 // call hook function 503 PropertyChanged(previous, current); 504 // update saved property if it is still contained 505 // in the saved properties (if not, the notification 506 // mechanism has caused to update the properties 507 // and "previous" and "current" are toast) 508 if (fSavedProperties->HasProperty(previous) 509 && fPropertyObject->HasProperty(current)) 510 previous->SetValue(current); 511 } 512 } 513 514 // ScrollOffsetChanged 515 void 516 PropertyListView::ScrollOffsetChanged(BPoint oldOffset, BPoint newOffset) 517 { 518 ScrollBy(newOffset.x - oldOffset.x, 519 newOffset.y - oldOffset.y); 520 } 521 522 // Select 523 void 524 PropertyListView::Select(PropertyItemView* item) 525 { 526 if (item) { 527 if (modifiers() & B_SHIFT_KEY) { 528 item->SetSelected(!item->IsSelected()); 529 } else if (modifiers() & B_OPTION_KEY) { 530 item->SetSelected(true); 531 int32 firstSelected = _CountItems(); 532 int32 lastSelected = -1; 533 for (int32 i = 0; PropertyItemView* otherItem = _ItemAt(i); i++) { 534 if (otherItem->IsSelected()) { 535 if (i < firstSelected) 536 firstSelected = i; 537 if (i > lastSelected) 538 lastSelected = i; 539 } 540 } 541 if (lastSelected > firstSelected) { 542 for (int32 i = firstSelected; PropertyItemView* otherItem = _ItemAt(i); i++) { 543 if (i > lastSelected) 544 break; 545 otherItem->SetSelected(true); 546 } 547 } 548 } else { 549 for (int32 i = 0; PropertyItemView* otherItem = _ItemAt(i); i++) { 550 otherItem->SetSelected(otherItem == item); 551 } 552 } 553 } 554 _CheckMenuStatus(); 555 } 556 557 // DeselectAll 558 void 559 PropertyListView::DeselectAll() 560 { 561 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 562 item->SetSelected(false); 563 } 564 _CheckMenuStatus(); 565 } 566 567 // Clicked 568 void 569 PropertyListView::Clicked(PropertyItemView* item) 570 { 571 fLastClickedItem = item; 572 } 573 574 // DoubleClicked 575 void 576 PropertyListView::DoubleClicked(PropertyItemView* item) 577 { 578 if (fLastClickedItem == item) { 579 printf("implement PropertyListView::DoubleClicked()\n"); 580 } 581 fLastClickedItem = NULL; 582 } 583 584 // #pragma mark - 585 586 // _UpdateSavedProperties 587 void 588 PropertyListView::_UpdateSavedProperties() 589 { 590 fSavedProperties->DeleteProperties(); 591 592 if (!fPropertyObject) 593 return; 594 595 int32 count = fPropertyObject->CountProperties(); 596 for (int32 i = 0; i < count; i++) { 597 const Property* p = fPropertyObject->PropertyAtFast(i); 598 fSavedProperties->AddProperty(p->Clone()); 599 } 600 } 601 602 // _AddItem 603 bool 604 PropertyListView::_AddItem(PropertyItemView* item) 605 { 606 if (item && BList::AddItem((void*)item)) { 607 // AddChild(item); 608 // NOTE: for now added in _LayoutItems() 609 item->SetListView(this); 610 return true; 611 } 612 return false; 613 } 614 615 // _RemoveItem 616 PropertyItemView* 617 PropertyListView::_RemoveItem(int32 index) 618 { 619 PropertyItemView* item = (PropertyItemView*)BList::RemoveItem(index); 620 if (item) { 621 item->SetListView(NULL); 622 if (!RemoveChild(item)) 623 fprintf(stderr, "failed to remove view in PropertyListView::_RemoveItem()\n"); 624 } 625 return item; 626 } 627 628 // _ItemAt 629 PropertyItemView* 630 PropertyListView::_ItemAt(int32 index) const 631 { 632 return (PropertyItemView*)BList::ItemAt(index); 633 } 634 635 // _CountItems 636 int32 637 PropertyListView::_CountItems() const 638 { 639 return BList::CountItems(); 640 } 641 642 // _MakeEmpty 643 void 644 PropertyListView::_MakeEmpty() 645 { 646 int32 count = _CountItems(); 647 while (PropertyItemView* item = _RemoveItem(count - 1)) { 648 delete item; 649 count--; 650 } 651 delete fPropertyObject; 652 fPropertyObject = NULL; 653 654 SetScrollOffset(BPoint(0.0, 0.0)); 655 } 656 657 // _ItemsRect 658 BRect 659 PropertyListView::_ItemsRect() const 660 { 661 float width = Bounds().Width(); 662 float height = -1.0; 663 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 664 height += item->PreferredHeight() + 1.0; 665 } 666 if (height < 0.0) 667 height = 0.0; 668 return BRect(0.0, 0.0, width, height); 669 } 670 671 // _LayoutItems 672 void 673 PropertyListView::_LayoutItems() 674 { 675 // figure out maximum label width 676 float labelWidth = 0.0; 677 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 678 if (item->PreferredLabelWidth() > labelWidth) 679 labelWidth = item->PreferredLabelWidth(); 680 } 681 labelWidth = ceilf(labelWidth); 682 // layout items 683 float top = 0.0; 684 float width = Bounds().Width(); 685 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 686 item->MoveTo(BPoint(0.0, top)); 687 float height = item->PreferredHeight(); 688 item->SetLabelWidth(labelWidth); 689 item->ResizeTo(width, height); 690 item->FrameResized(item->Bounds().Width(), 691 item->Bounds().Height()); 692 top += height + 1.0; 693 694 AddChild(item); 695 } 696 } 697 698 // _CheckMenuStatus 699 void 700 PropertyListView::_CheckMenuStatus() 701 { 702 if (!fPropertyM || fSuspendUpdates) 703 return; 704 705 if (!fPropertyObject) { 706 fPropertyM->SetEnabled(false); 707 return; 708 } else 709 fPropertyM->SetEnabled(false); 710 711 bool gotSelection = false; 712 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 713 if (item->IsSelected()) { 714 gotSelection = true; 715 break; 716 } 717 } 718 fCopyMI->SetEnabled(gotSelection); 719 720 bool clipboardHasData = false; 721 if (fClipboard->Lock()) { 722 if (BMessage* data = fClipboard->Data()) { 723 clipboardHasData = data->HasMessage("property"); 724 } 725 fClipboard->Unlock(); 726 } 727 728 fPasteMI->SetEnabled(clipboardHasData); 729 // LanguageManager* m = LanguageManager::Default(); 730 if (IsEditingMultipleObjects()) 731 // fPasteMI->SetLabel(m->GetString(MULTI_PASTE, "Multi Paste")); 732 fPasteMI->SetLabel("Multi Paste"); 733 else 734 // fPasteMI->SetLabel(m->GetString(PASTE, "Paste")); 735 fPasteMI->SetLabel("Paste"); 736 737 bool enableMenu = fPropertyObject; 738 if (fPropertyM->IsEnabled() != enableMenu) 739 fPropertyM->SetEnabled(enableMenu); 740 741 bool gotItems = _CountItems() > 0; 742 fSelectM->SetEnabled(gotItems); 743 } 744 745 746