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
39enum {
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
52class 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
87PropertyListView::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
111PropertyListView::~PropertyListView()
112{
113	delete fClipboard;
114
115	delete fPropertyObject;
116	delete fSavedProperties;
117
118	delete fMouseWheelFilter;
119	delete fTabFilter;
120}
121
122// AttachedToWindow
123void
124PropertyListView::AttachedToWindow()
125{
126	Window()->AddCommonFilter(fMouseWheelFilter);
127	Window()->AddCommonFilter(fTabFilter);
128}
129
130// DetachedFromWindow
131void
132PropertyListView::DetachedFromWindow()
133{
134	Window()->RemoveCommonFilter(fTabFilter);
135	Window()->RemoveCommonFilter(fMouseWheelFilter);
136}
137
138// FrameResized
139void
140PropertyListView::FrameResized(float width, float height)
141{
142	SetVisibleSize(width, height);
143	Invalidate();
144}
145
146// Draw
147void
148PropertyListView::Draw(BRect updateRect)
149{
150	if (!fSuspendUpdates)
151		FillRect(updateRect, B_SOLID_LOW);
152}
153
154// MakeFocus
155void
156PropertyListView::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
167void
168PropertyListView::MouseDown(BPoint where)
169{
170	if (!(modifiers() & B_SHIFT_KEY)) {
171		DeselectAll();
172	}
173	MakeFocus(true);
174}
175
176// MessageReceived
177void
178PropertyListView::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
264BSize
265PropertyListView::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
273BSize
274PropertyListView::MaxSize()
275{
276	return BView::MaxSize();
277}
278
279
280BSize
281PropertyListView::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
293bool
294PropertyListView::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
318void
319PropertyListView::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
355void
356PropertyListView::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*
377PropertyListView::ScrollView() const
378{
379	return dynamic_cast< ::ScrollView*>(ScrollSource());
380}
381
382// #pragma mark -
383
384// SetTo
385void
386PropertyListView::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
472void
473PropertyListView::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
481void
482PropertyListView::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
498bool
499PropertyListView::IsEditingMultipleObjects()
500{
501	return false;
502}
503
504// #pragma mark -
505
506// UpdateObject
507void
508PropertyListView::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
526void
527PropertyListView::ScrollOffsetChanged(BPoint oldOffset, BPoint newOffset)
528{
529	ScrollBy(newOffset.x - oldOffset.x,
530			 newOffset.y - oldOffset.y);
531}
532
533// Select
534void
535PropertyListView::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
569void
570PropertyListView::DeselectAll()
571{
572	for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
573		item->SetSelected(false);
574	}
575	_CheckMenuStatus();
576}
577
578// Clicked
579void
580PropertyListView::Clicked(PropertyItemView* item)
581{
582	fLastClickedItem = item;
583}
584
585// DoubleClicked
586void
587PropertyListView::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
598void
599PropertyListView::_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
614bool
615PropertyListView::_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
627PropertyItemView*
628PropertyListView::_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
640PropertyItemView*
641PropertyListView::_ItemAt(int32 index) const
642{
643	return (PropertyItemView*)BList::ItemAt(index);
644}
645
646// _CountItems
647int32
648PropertyListView::_CountItems() const
649{
650	return BList::CountItems();
651}
652
653// _MakeEmpty
654void
655PropertyListView::_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
669BRect
670PropertyListView::_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
683void
684PropertyListView::_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
710void
711PropertyListView::_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