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