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 355 // ScrollView 356 ::ScrollView* 357 PropertyListView::ScrollView() const 358 { 359 return dynamic_cast< ::ScrollView*>(ScrollSource()); 360 } 361 362 // #pragma mark - 363 364 // SetTo 365 void 366 PropertyListView::SetTo(PropertyObject* object) 367 { 368 // try to do without rebuilding the list 369 // it should in fact be pretty unlikely that this does not 370 // work, but we keep being defensive 371 if (fPropertyObject && object && 372 fPropertyObject->ContainsSameProperties(*object)) { 373 // iterate over view items and update their value views 374 bool error = false; 375 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 376 Property* property = object->PropertyAt(i); 377 if (!item->AdoptProperty(property)) { 378 // the reason for this can be that the property is 379 // unkown to the PropertyEditorFactory and therefor 380 // there is no editor view at this item 381 fprintf(stderr, "PropertyListView::_SetTo() - " 382 "property mismatch at %" B_PRId32 "\n", i); 383 error = true; 384 break; 385 } 386 if (property) 387 item->SetEnabled(property->IsEditable()); 388 } 389 // we didn't need to make empty, but transfer ownership 390 // of the object 391 if (!error) { 392 // if the "adopt" process went only halfway, 393 // some properties of the original object 394 // are still referenced, so we can only 395 // delete the original object if the process 396 // was successful and leak Properties otherwise, 397 // but this case is only theoretical anyways... 398 delete fPropertyObject; 399 } 400 fPropertyObject = object; 401 } else { 402 // remember scroll pos, selection and focused item 403 BPoint scrollOffset = ScrollOffset(); 404 BList selection(20); 405 int32 focused = -1; 406 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 407 if (item->IsSelected()) 408 selection.AddItem((void*)(long)i); 409 if (item->IsFocused()) 410 focused = i; 411 } 412 if (Window()) 413 Window()->BeginViewTransaction(); 414 fSuspendUpdates = true; 415 416 // rebuild list 417 _MakeEmpty(); 418 fPropertyObject = object; 419 420 if (fPropertyObject) { 421 // fill with content 422 for (int32 i = 0; Property* property = fPropertyObject->PropertyAt(i); i++) { 423 PropertyItemView* item = new PropertyItemView(property); 424 item->SetEnabled(property->IsEditable()); 425 _AddItem(item); 426 } 427 _LayoutItems(); 428 429 // restore scroll pos, selection and focus 430 SetScrollOffset(scrollOffset); 431 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 432 if (selection.HasItem((void*)(long)i)) 433 item->SetSelected(true); 434 if (i == focused) 435 item->MakeFocus(true); 436 } 437 } 438 439 if (Window()) 440 Window()->EndViewTransaction(); 441 fSuspendUpdates = false; 442 443 SetDataRect(_ItemsRect()); 444 } 445 446 _UpdateSavedProperties(); 447 _CheckMenuStatus(); 448 Invalidate(); 449 } 450 451 // PropertyChanged 452 void 453 PropertyListView::PropertyChanged(const Property* previous, 454 const Property* current) 455 { 456 printf("PropertyListView::PropertyChanged(%s)\n", 457 name_for_id(current->Identifier())); 458 } 459 460 // PasteProperties 461 void 462 PropertyListView::PasteProperties(const PropertyObject* object) 463 { 464 if (!fPropertyObject) 465 return; 466 467 // default implementation is to adopt the pasted properties 468 int32 count = object->CountProperties(); 469 for (int32 i = 0; i < count; i++) { 470 Property* p = object->PropertyAtFast(i); 471 Property* local = fPropertyObject->FindProperty(p->Identifier()); 472 if (local) 473 local->SetValue(p); 474 } 475 } 476 477 // IsEditingMultipleObjects 478 bool 479 PropertyListView::IsEditingMultipleObjects() 480 { 481 return false; 482 } 483 484 // #pragma mark - 485 486 // UpdateObject 487 void 488 PropertyListView::UpdateObject(uint32 propertyID) 489 { 490 Property* previous = fSavedProperties->FindProperty(propertyID); 491 Property* current = fPropertyObject->FindProperty(propertyID); 492 if (previous && current) { 493 // call hook function 494 PropertyChanged(previous, current); 495 // update saved property if it is still contained 496 // in the saved properties (if not, the notification 497 // mechanism has caused to update the properties 498 // and "previous" and "current" are toast) 499 if (fSavedProperties->HasProperty(previous) 500 && fPropertyObject->HasProperty(current)) 501 previous->SetValue(current); 502 } 503 } 504 505 // ScrollOffsetChanged 506 void 507 PropertyListView::ScrollOffsetChanged(BPoint oldOffset, BPoint newOffset) 508 { 509 ScrollBy(newOffset.x - oldOffset.x, 510 newOffset.y - oldOffset.y); 511 } 512 513 // Select 514 void 515 PropertyListView::Select(PropertyItemView* item) 516 { 517 if (item) { 518 if (modifiers() & B_SHIFT_KEY) { 519 item->SetSelected(!item->IsSelected()); 520 } else if (modifiers() & B_OPTION_KEY) { 521 item->SetSelected(true); 522 int32 firstSelected = _CountItems(); 523 int32 lastSelected = -1; 524 for (int32 i = 0; PropertyItemView* otherItem = _ItemAt(i); i++) { 525 if (otherItem->IsSelected()) { 526 if (i < firstSelected) 527 firstSelected = i; 528 if (i > lastSelected) 529 lastSelected = i; 530 } 531 } 532 if (lastSelected > firstSelected) { 533 for (int32 i = firstSelected; PropertyItemView* otherItem = _ItemAt(i); i++) { 534 if (i > lastSelected) 535 break; 536 otherItem->SetSelected(true); 537 } 538 } 539 } else { 540 for (int32 i = 0; PropertyItemView* otherItem = _ItemAt(i); i++) { 541 otherItem->SetSelected(otherItem == item); 542 } 543 } 544 } 545 _CheckMenuStatus(); 546 } 547 548 // DeselectAll 549 void 550 PropertyListView::DeselectAll() 551 { 552 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 553 item->SetSelected(false); 554 } 555 _CheckMenuStatus(); 556 } 557 558 // Clicked 559 void 560 PropertyListView::Clicked(PropertyItemView* item) 561 { 562 fLastClickedItem = item; 563 } 564 565 // DoubleClicked 566 void 567 PropertyListView::DoubleClicked(PropertyItemView* item) 568 { 569 if (fLastClickedItem == item) { 570 printf("implement PropertyListView::DoubleClicked()\n"); 571 } 572 fLastClickedItem = NULL; 573 } 574 575 // #pragma mark - 576 577 // _UpdateSavedProperties 578 void 579 PropertyListView::_UpdateSavedProperties() 580 { 581 fSavedProperties->DeleteProperties(); 582 583 if (!fPropertyObject) 584 return; 585 586 int32 count = fPropertyObject->CountProperties(); 587 for (int32 i = 0; i < count; i++) { 588 const Property* p = fPropertyObject->PropertyAtFast(i); 589 fSavedProperties->AddProperty(p->Clone()); 590 } 591 } 592 593 // _AddItem 594 bool 595 PropertyListView::_AddItem(PropertyItemView* item) 596 { 597 if (item && BList::AddItem((void*)item)) { 598 // AddChild(item); 599 // NOTE: for now added in _LayoutItems() 600 item->SetListView(this); 601 return true; 602 } 603 return false; 604 } 605 606 // _RemoveItem 607 PropertyItemView* 608 PropertyListView::_RemoveItem(int32 index) 609 { 610 PropertyItemView* item = (PropertyItemView*)BList::RemoveItem(index); 611 if (item) { 612 item->SetListView(NULL); 613 if (!RemoveChild(item)) 614 fprintf(stderr, "failed to remove view in PropertyListView::_RemoveItem()\n"); 615 } 616 return item; 617 } 618 619 // _ItemAt 620 PropertyItemView* 621 PropertyListView::_ItemAt(int32 index) const 622 { 623 return (PropertyItemView*)BList::ItemAt(index); 624 } 625 626 // _CountItems 627 int32 628 PropertyListView::_CountItems() const 629 { 630 return BList::CountItems(); 631 } 632 633 // _MakeEmpty 634 void 635 PropertyListView::_MakeEmpty() 636 { 637 int32 count = _CountItems(); 638 while (PropertyItemView* item = _RemoveItem(count - 1)) { 639 delete item; 640 count--; 641 } 642 delete fPropertyObject; 643 fPropertyObject = NULL; 644 645 SetScrollOffset(BPoint(0.0, 0.0)); 646 } 647 648 // _ItemsRect 649 BRect 650 PropertyListView::_ItemsRect() const 651 { 652 float width = Bounds().Width(); 653 float height = -1.0; 654 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 655 height += item->PreferredHeight() + 1.0; 656 } 657 if (height < 0.0) 658 height = 0.0; 659 return BRect(0.0, 0.0, width, height); 660 } 661 662 // _LayoutItems 663 void 664 PropertyListView::_LayoutItems() 665 { 666 // figure out maximum label width 667 float labelWidth = 0.0; 668 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 669 if (item->PreferredLabelWidth() > labelWidth) 670 labelWidth = item->PreferredLabelWidth(); 671 } 672 labelWidth = ceilf(labelWidth); 673 // layout items 674 float top = 0.0; 675 float width = Bounds().Width(); 676 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 677 item->MoveTo(BPoint(0.0, top)); 678 float height = item->PreferredHeight(); 679 item->SetLabelWidth(labelWidth); 680 item->ResizeTo(width, height); 681 item->FrameResized(item->Bounds().Width(), 682 item->Bounds().Height()); 683 top += height + 1.0; 684 685 AddChild(item); 686 } 687 } 688 689 // _CheckMenuStatus 690 void 691 PropertyListView::_CheckMenuStatus() 692 { 693 if (!fPropertyM || fSuspendUpdates) 694 return; 695 696 if (!fPropertyObject) { 697 fPropertyM->SetEnabled(false); 698 return; 699 } else 700 fPropertyM->SetEnabled(false); 701 702 bool gotSelection = false; 703 for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) { 704 if (item->IsSelected()) { 705 gotSelection = true; 706 break; 707 } 708 } 709 fCopyMI->SetEnabled(gotSelection); 710 711 bool clipboardHasData = false; 712 if (fClipboard->Lock()) { 713 if (BMessage* data = fClipboard->Data()) { 714 clipboardHasData = data->HasMessage("property"); 715 } 716 fClipboard->Unlock(); 717 } 718 719 fPasteMI->SetEnabled(clipboardHasData); 720 if (IsEditingMultipleObjects()) 721 fPasteMI->SetLabel(B_TRANSLATE("Multi-paste")); 722 else 723 fPasteMI->SetLabel(B_TRANSLATE("Paste")); 724 725 bool enableMenu = fPropertyObject; 726 if (fPropertyM->IsEnabled() != enableMenu) 727 fPropertyM->SetEnabled(enableMenu); 728 729 bool gotItems = _CountItems() > 0; 730 fSelectM->SetEnabled(gotItems); 731 } 732 733 734