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
355// ScrollView
356::ScrollView*
357PropertyListView::ScrollView() const
358{
359	return dynamic_cast< ::ScrollView*>(ScrollSource());
360}
361
362// #pragma mark -
363
364// SetTo
365void
366PropertyListView::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
452void
453PropertyListView::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
461void
462PropertyListView::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
478bool
479PropertyListView::IsEditingMultipleObjects()
480{
481	return false;
482}
483
484// #pragma mark -
485
486// UpdateObject
487void
488PropertyListView::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
506void
507PropertyListView::ScrollOffsetChanged(BPoint oldOffset, BPoint newOffset)
508{
509	ScrollBy(newOffset.x - oldOffset.x,
510			 newOffset.y - oldOffset.y);
511}
512
513// Select
514void
515PropertyListView::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
549void
550PropertyListView::DeselectAll()
551{
552	for (int32 i = 0; PropertyItemView* item = _ItemAt(i); i++) {
553		item->SetSelected(false);
554	}
555	_CheckMenuStatus();
556}
557
558// Clicked
559void
560PropertyListView::Clicked(PropertyItemView* item)
561{
562	fLastClickedItem = item;
563}
564
565// DoubleClicked
566void
567PropertyListView::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
578void
579PropertyListView::_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
594bool
595PropertyListView::_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
607PropertyItemView*
608PropertyListView::_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
620PropertyItemView*
621PropertyListView::_ItemAt(int32 index) const
622{
623	return (PropertyItemView*)BList::ItemAt(index);
624}
625
626// _CountItems
627int32
628PropertyListView::_CountItems() const
629{
630	return BList::CountItems();
631}
632
633// _MakeEmpty
634void
635PropertyListView::_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
649BRect
650PropertyListView::_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
663void
664PropertyListView::_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
690void
691PropertyListView::_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