xref: /haiku/src/apps/icon-o-matic/generic/property/view/PropertyListView.cpp (revision ed6250c95736c0b55da79d6e9dd01369532260c0)
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