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