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