1/*
2 * Copyright 2010, Haiku, Inc. All rights reserved.
3 * Distributed under the terms of the MIT license.
4 *
5 * Authors:
6 *		Robert Polic
7 *		Stephan A��mus <superstippi@gmx.de>
8 *
9 * Copyright 1999, Be Incorporated.   All Rights Reserved.
10 * This file may be used under the terms of the Be Sample Code License.
11 */
12
13
14#include "PersonView.h"
15
16#include <stdio.h>
17#include <stdlib.h>
18#include <string.h>
19
20#include <BitmapStream.h>
21#include <Catalog.h>
22#include <fs_attr.h>
23#include <Box.h>
24#include <ControlLook.h>
25#include <GridLayout.h>
26#include <Locale.h>
27#include <MenuField.h>
28#include <MenuItem.h>
29#include <PopUpMenu.h>
30#include <Query.h>
31#include <TranslationUtils.h>
32#include <Translator.h>
33#include <VolumeRoster.h>
34#include <Window.h>
35
36#include "AttributeTextControl.h"
37#include "PictureView.h"
38
39
40#undef B_TRANSLATION_CONTEXT
41#define B_TRANSLATION_CONTEXT "People"
42
43
44PersonView::PersonView(const char* name, const char* categoryAttribute,
45		const entry_ref *ref)
46	:
47	BGridView(),
48	fLastModificationTime(0),
49	fGroups(NULL),
50	fControls(20, false),
51	fCategoryAttribute(categoryAttribute),
52	fPictureView(NULL),
53	fSaving(false)
54{
55	SetName(name);
56	SetFlags(Flags() | B_WILL_DRAW);
57
58	fRef = ref;
59	BFile* file = NULL;
60	if (fRef != NULL)
61		file = new BFile(fRef, B_READ_ONLY);
62
63	// Add picture "field", using ID photo 35mm x 45mm ratio
64	fPictureView = new PictureView(70, 90, ref);
65
66	BGridLayout* layout = GridLayout();
67
68	float spacing = be_control_look->DefaultItemSpacing();
69	layout->SetInsets(spacing, spacing, spacing, spacing);
70
71	layout->AddView(fPictureView, 0, 0, 1, 5);
72	layout->ItemAt(0, 0)->SetExplicitAlignment(
73		BAlignment(B_ALIGN_CENTER, B_ALIGN_TOP));
74
75	if (file != NULL)
76		file->GetModificationTime(&fLastModificationTime);
77	delete file;
78}
79
80
81PersonView::~PersonView()
82{
83}
84
85
86void
87PersonView::AddAttribute(const char* label, const char* attribute)
88{
89	// Check if this attribute has already been added.
90	AttributeTextControl* control = NULL;
91	for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
92		if (fControls.ItemAt(i)->Attribute() == attribute) {
93			return;
94		}
95	}
96
97	control = new AttributeTextControl(label, attribute);
98	fControls.AddItem(control);
99
100	BGridLayout* layout = GridLayout();
101	int32 row = fControls.CountItems();
102
103	if (fCategoryAttribute == attribute) {
104		// Special case the category attribute. The Group popup field will
105		// be added as the label instead.
106		fGroups = new BPopUpMenu(label);
107		fGroups->SetRadioMode(false);
108		BuildGroupMenu();
109
110		BMenuField* field = new BMenuField("", "", fGroups);
111		field->SetEnabled(true);
112		layout->AddView(field, 1, row);
113
114		control->SetLabel("");
115		layout->AddView(control, 2, row);
116	} else {
117		layout->AddItem(control->CreateLabelLayoutItem(), 1, row);
118		layout->AddItem(control->CreateTextViewLayoutItem(), 2, row);
119	}
120
121	SetAttribute(attribute, true);
122}
123
124
125void
126PersonView::MakeFocus(bool focus)
127{
128	if (focus && fControls.CountItems() > 0)
129		fControls.ItemAt(0)->MakeFocus();
130	else
131		BView::MakeFocus(focus);
132}
133
134
135void
136PersonView::MessageReceived(BMessage* msg)
137{
138	switch (msg->what) {
139		case M_SAVE:
140			Save();
141			break;
142
143		case M_REVERT:
144			if (fPictureView)
145				fPictureView->Revert();
146
147			for (int32 i = fControls.CountItems() - 1; i >= 0; i--)
148				fControls.ItemAt(i)->Revert();
149			break;
150
151		case M_SELECT:
152			for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
153				BTextView* text = fControls.ItemAt(i)->TextView();
154				if (text->IsFocus()) {
155					text->Select(0, text->TextLength());
156					break;
157				}
158			}
159			break;
160
161		case M_GROUP_MENU:
162		{
163			const char* name = NULL;
164			if (msg->FindString("group", &name) == B_OK)
165				SetAttribute(fCategoryAttribute, name, false);
166			break;
167		}
168
169	}
170}
171
172
173void
174PersonView::Draw(BRect updateRect)
175{
176	if (!fPictureView)
177		return;
178
179	// Draw a alert/get info-like strip
180	BRect stripeRect = Bounds();
181	stripeRect.right = GridLayout()->HorizontalSpacing()
182		+ fPictureView->Bounds().Width() / 2;
183	SetHighColor(tint_color(ViewColor(), B_DARKEN_1_TINT));
184	FillRect(stripeRect);
185}
186
187
188void
189PersonView::BuildGroupMenu()
190{
191	if (fGroups == NULL)
192		return;
193
194	BMenuItem* item;
195	while ((item = fGroups->ItemAt(0)) != NULL) {
196		fGroups->RemoveItem(item);
197		delete item;
198	}
199
200	int32 count = 0;
201
202	BVolumeRoster volumeRoster;
203	BVolume volume;
204	while (volumeRoster.GetNextVolume(&volume) == B_OK) {
205		BQuery query;
206		query.SetVolume(&volume);
207
208		char buffer[256];
209		snprintf(buffer, sizeof(buffer), "%s=*", fCategoryAttribute.String());
210		query.SetPredicate(buffer);
211		query.Fetch();
212
213		BEntry entry;
214		while (query.GetNextEntry(&entry) == B_OK) {
215			BFile file(&entry, B_READ_ONLY);
216			attr_info info;
217
218			if (file.InitCheck() == B_OK
219				&& file.GetAttrInfo(fCategoryAttribute, &info) == B_OK
220				&& info.size > 1) {
221				if (info.size > (off_t)sizeof(buffer))
222					info.size = sizeof(buffer);
223
224				if (file.ReadAttr(fCategoryAttribute.String(), B_STRING_TYPE,
225						0, buffer, info.size) < 0) {
226					continue;
227				}
228
229				const char *text = buffer;
230				while (true) {
231					char* offset = strstr(text, ",");
232					if (offset != NULL)
233						offset[0] = '\0';
234
235					if (!fGroups->FindItem(text)) {
236						int32 index = 0;
237						while ((item = fGroups->ItemAt(index)) != NULL) {
238							if (strcmp(text, item->Label()) < 0)
239								break;
240							index++;
241						}
242						BMessage* message = new BMessage(M_GROUP_MENU);
243						message->AddString("group", text);
244						fGroups->AddItem(new BMenuItem(text, message), index);
245						count++;
246					}
247					if (offset) {
248						text = offset + 1;
249						while (*text == ' ')
250							text++;
251					}
252					else
253						break;
254				}
255			}
256		}
257	}
258
259	if (count == 0) {
260		fGroups->AddItem(item = new BMenuItem(
261			B_TRANSLATE_CONTEXT("none", "Groups list"),
262			new BMessage(M_GROUP_MENU)));
263		item->SetEnabled(false);
264	}
265
266	fGroups->SetTargetForItems(this);
267}
268
269
270void
271PersonView::CreateFile(const entry_ref* ref)
272{
273	fRef = ref;
274	Save();
275}
276
277
278bool
279PersonView::IsSaved() const
280{
281	if (fPictureView && fPictureView->HasChanged())
282		return false;
283
284	for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
285		if (fControls.ItemAt(i)->HasChanged())
286			return false;
287	}
288
289	return true;
290}
291
292
293void
294PersonView::Save()
295{
296	BFile file(fRef, B_READ_WRITE);
297	if (file.InitCheck() != B_NO_ERROR)
298		return;
299
300	fSaving = true;
301
302	int32 count = fControls.CountItems();
303	for (int32 i = 0; i < count; i++) {
304		AttributeTextControl* control = fControls.ItemAt(i);
305		const char* value = control->Text();
306		file.WriteAttr(control->Attribute().String(), B_STRING_TYPE, 0,
307			value, strlen(value) + 1);
308		control->Update();
309	}
310
311	// Write the picture, if any, in the person file content
312	if (fPictureView) {
313		// Trim any previous content
314		file.Seek(0, SEEK_SET);
315		file.SetSize(0);
316
317		BBitmap* picture = fPictureView->Bitmap();
318		if (picture) {
319			BBitmapStream stream(picture);
320			// Detach *our* bitmap from stream to avoid its deletion
321			// at stream object destruction
322			stream.DetachBitmap(&picture);
323
324			BTranslatorRoster* roster = BTranslatorRoster::Default();
325			roster->Translate(&stream, NULL, NULL, &file,
326				fPictureView->SuggestedType(), B_TRANSLATOR_BITMAP,
327				fPictureView->SuggestedMIMEType());
328
329		}
330
331		fPictureView->Update();
332	}
333
334	file.GetModificationTime(&fLastModificationTime);
335
336	fSaving = false;
337}
338
339
340const char*
341PersonView::AttributeValue(const char* attribute) const
342{
343	for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
344		if (fControls.ItemAt(i)->Attribute() == attribute)
345			return fControls.ItemAt(i)->Text();
346	}
347
348	return "";
349}
350
351
352void
353PersonView::SetAttribute(const char* attribute, bool update)
354{
355	char* value = NULL;
356	attr_info info;
357	BFile* file = NULL;
358
359	if (fRef != NULL)
360		file = new(std::nothrow) BFile(fRef, B_READ_ONLY);
361
362	if (file != NULL && file->GetAttrInfo(attribute, &info) == B_OK) {
363		value = (char*)calloc(info.size, 1);
364		file->ReadAttr(attribute, B_STRING_TYPE, 0, value, info.size);
365	}
366
367	SetAttribute(attribute, value, update);
368
369	free(value);
370	delete file;
371}
372
373
374void
375PersonView::SetAttribute(const char* attribute, const char* value,
376	bool update)
377{
378	if (!LockLooper())
379		return;
380
381	AttributeTextControl* control = NULL;
382	for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
383		if (fControls.ItemAt(i)->Attribute() == attribute) {
384			control = fControls.ItemAt(i);
385			break;
386		}
387	}
388
389	if (control == NULL) {
390		UnlockLooper();
391		return;
392	}
393
394	if (update) {
395		control->SetText(value);
396		control->Update();
397	} else {
398		BTextView* text = control->TextView();
399
400		int32 start, end;
401		text->GetSelection(&start, &end);
402		if (start != end) {
403			text->Delete();
404			text->Insert(value);
405		} else if ((end = text->TextLength())) {
406			text->Select(end, end);
407			text->Insert(",");
408			text->Insert(value);
409			text->Select(text->TextLength(), text->TextLength());
410		} else
411			control->SetText(value);
412	}
413
414	UnlockLooper();
415}
416
417
418void
419PersonView::UpdatePicture(const entry_ref* ref)
420{
421	if (fPictureView == NULL)
422		return;
423
424	if (fSaving)
425		return;
426
427	time_t modificationTime = 0;
428	BEntry entry(ref);
429	entry.GetModificationTime(&modificationTime);
430
431	if (entry.InitCheck() == B_OK
432		&& modificationTime <= fLastModificationTime) {
433		return;
434	}
435
436	fPictureView->Update(ref);
437}
438
439
440bool
441PersonView::IsTextSelected() const
442{
443	for (int32 i = fControls.CountItems() - 1; i >= 0; i--) {
444		BTextView* text = fControls.ItemAt(i)->TextView();
445
446		int32 start, end;
447		text->GetSelection(&start, &end);
448		if (start != end)
449			return true;
450	}
451	return false;
452}
453