1/*
2 * Copyright 2011, Haiku.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Philippe Houdoin
7 */
8
9
10#include "PictureView.h"
11
12#include <math.h>
13#include <new>
14#include <stdio.h>
15
16#include <Alert.h>
17#include <Bitmap.h>
18#include <BitmapStream.h>
19#include <Catalog.h>
20#include <Clipboard.h>
21#include <Directory.h>
22#include <File.h>
23#include <FilePanel.h>
24#include <IconUtils.h>
25#include <LayoutUtils.h>
26#include <PopUpMenu.h>
27#include <DataIO.h>
28#include <MenuItem.h>
29#include <Messenger.h>
30#include <MimeType.h>
31#include <NodeInfo.h>
32#include <String.h>
33#include <TranslatorRoster.h>
34#include <TranslationUtils.h>
35#include <Window.h>
36
37#include "PeopleApp.h"	// for B_PERSON_MIMETYPE
38
39
40#undef B_TRANSLATION_CONTEXT
41#define B_TRANSLATION_CONTEXT "People"
42
43
44const uint32 kMsgPopUpMenuClosed = 'pmcl';
45
46class PopUpMenu : public BPopUpMenu {
47public:
48							PopUpMenu(const char* name, BMessenger target);
49	virtual 				~PopUpMenu();
50
51private:
52		BMessenger 			fTarget;
53};
54
55
56PopUpMenu::PopUpMenu(const char* name, BMessenger target)
57	:
58	BPopUpMenu(name, false, false),	fTarget(target)
59{
60	SetAsyncAutoDestruct(true);
61}
62
63
64PopUpMenu::~PopUpMenu()
65{
66	fTarget.SendMessage(kMsgPopUpMenuClosed);
67}
68
69
70// #pragma mark -
71
72using std::nothrow;
73
74
75const float kPictureMargin = 6.0;
76
77PictureView::PictureView(float width, float height, const entry_ref* ref)
78	:
79	BView("pictureview", B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_NAVIGABLE),
80	fPicture(NULL),
81	fOriginalPicture(NULL),
82	fDefaultPicture(NULL),
83	fShowingPopUpMenu(false),
84	fPictureType(0),
85	fFocusChanging(false),
86	fOpenPanel(new BFilePanel(B_OPEN_PANEL))
87{
88	SetViewColor(255, 255, 255);
89
90	SetToolTip(B_TRANSLATE(
91		"Drop an image here,\n"
92		"or use the contextual menu."));
93
94	BSize size(width + 2 * kPictureMargin, height + 2 * kPictureMargin);
95	SetExplicitMinSize(size);
96	SetExplicitMaxSize(size);
97
98	BMimeType mime(B_PERSON_MIMETYPE);
99	uint8* iconData;
100	size_t iconDataSize;
101	if (mime.GetIcon(&iconData, &iconDataSize) == B_OK) {
102		float size = width < height ? width : height;
103		fDefaultPicture = new BBitmap(BRect(0, 0, size, size),
104			B_RGB32);
105		if (fDefaultPicture->InitCheck() != B_OK
106			|| BIconUtils::GetVectorIcon(iconData, iconDataSize,
107				fDefaultPicture) != B_OK) {
108			delete fDefaultPicture;
109			fDefaultPicture = NULL;
110		}
111	}
112
113	Update(ref);
114}
115
116
117PictureView::~PictureView()
118{
119	delete fDefaultPicture;
120	delete fPicture;
121	if (fOriginalPicture != fPicture)
122		delete fOriginalPicture;
123
124	delete fOpenPanel;
125}
126
127
128bool
129PictureView::HasChanged()
130{
131	return fPicture != fOriginalPicture;
132}
133
134
135void
136PictureView::Revert()
137{
138	if (!HasChanged())
139		return;
140
141	_SetPicture(fOriginalPicture);
142}
143
144
145void
146PictureView::Update()
147{
148	if (fOriginalPicture != fPicture) {
149		delete fOriginalPicture;
150		fOriginalPicture = fPicture;
151	}
152}
153
154
155void
156PictureView::Update(const entry_ref* ref)
157{
158	// Don't update when user has modified the picture
159	if (HasChanged())
160		return;
161
162	if (_LoadPicture(ref) == B_OK) {
163		delete fOriginalPicture;
164		fOriginalPicture = fPicture;
165	}
166}
167
168
169BBitmap*
170PictureView::Bitmap()
171{
172	return fPicture;
173}
174
175
176uint32
177PictureView::SuggestedType()
178{
179	return fPictureType;
180}
181
182
183const char*
184PictureView::SuggestedMIMEType()
185{
186	if (fPictureMIMEType == "")
187		return NULL;
188
189	return fPictureMIMEType.String();
190}
191
192
193void
194PictureView::MessageReceived(BMessage* message)
195{
196	switch (message->what) {
197		case B_REFS_RECEIVED:
198		case B_SIMPLE_DATA:
199		{
200			entry_ref ref;
201			if (message->FindRef("refs", &ref) == B_OK
202				&& _LoadPicture(&ref) == B_OK)
203				MakeFocus(true);
204			else
205				_HandleDrop(message);
206			break;
207		}
208
209		case B_MIME_DATA:
210			// TODO
211			break;
212
213		case B_COPY_TARGET:
214			_HandleDrop(message);
215			break;
216
217		case B_PASTE:
218		{
219			if (be_clipboard->Lock() != B_OK)
220				break;
221
222			BMessage* data = be_clipboard->Data();
223			BMessage archivedBitmap;
224			if (data->FindMessage("image/bitmap", &archivedBitmap) == B_OK) {
225				BBitmap* picture = new(std::nothrow) BBitmap(&archivedBitmap);
226				_SetPicture(picture);
227			}
228
229			be_clipboard->Unlock();
230			break;
231		}
232
233		case B_DELETE:
234		case B_TRASH_TARGET:
235			_SetPicture(NULL);
236			break;
237
238		case kMsgLoadImage:
239			fOpenPanel->SetTarget(BMessenger(this));
240			fOpenPanel->Show();
241			break;
242
243		case kMsgPopUpMenuClosed:
244			fShowingPopUpMenu = false;
245			break;
246
247		default:
248			BView::MessageReceived(message);
249			break;
250	}
251}
252
253
254void
255PictureView::Draw(BRect updateRect)
256{
257	BRect rect = Bounds();
258
259	// Draw the outer frame
260	rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR);
261	if (IsFocus() && Window() && Window()->IsActive())
262		SetHighColor(ui_color(B_KEYBOARD_NAVIGATION_COLOR));
263	else
264		SetHighColor(tint_color(base, B_DARKEN_3_TINT));
265	StrokeRect(rect);
266
267	if (fFocusChanging) {
268		// focus frame is already redraw, stop here
269		return;
270	}
271
272	BBitmap* picture = fPicture ? fPicture : fDefaultPicture;
273	if (picture != NULL) {
274		// scale to fit and center picture in frame
275		BRect frame = rect.InsetByCopy(kPictureMargin, kPictureMargin);
276		BRect srcRect = picture->Bounds();
277		BSize size = frame.Size();
278		float pictureAspect = srcRect.Height() / srcRect.Width();
279		float frameAspect = size.height / size.width;
280
281		if (pictureAspect > frameAspect)
282			size.width = srcRect.Width() * size.height / srcRect.Height();
283		else if (pictureAspect < frameAspect)
284			size.height = srcRect.Height() * size.width / srcRect.Width();
285
286		fPictureRect = BLayoutUtils::AlignInFrame(frame, size,
287			BAlignment(B_ALIGN_HORIZONTAL_CENTER, B_ALIGN_VERTICAL_CENTER));
288
289		SetDrawingMode(B_OP_ALPHA);
290		if (picture == fDefaultPicture) {
291			SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_OVERLAY);
292			SetHighColor(0, 0, 0, 24);
293		}
294
295 		DrawBitmapAsync(picture, srcRect, fPictureRect,
296 			B_FILTER_BITMAP_BILINEAR);
297
298		SetDrawingMode(B_OP_OVER);
299	}
300}
301
302
303void
304PictureView::WindowActivated(bool active)
305{
306	BView::WindowActivated(active);
307
308	if (IsFocus())
309		Invalidate();
310}
311
312
313void
314PictureView::MakeFocus(bool focused)
315{
316	if (focused == IsFocus())
317		return;
318
319	BView::MakeFocus(focused);
320
321	if (Window()) {
322		fFocusChanging = true;
323		Invalidate();
324		Flush();
325		fFocusChanging = false;
326	}
327}
328
329
330void
331PictureView::MouseDown(BPoint position)
332{
333	MakeFocus(true);
334
335	uint32 buttons = 0;
336	if (Window() != NULL && Window()->CurrentMessage() != NULL)
337		buttons = Window()->CurrentMessage()->FindInt32("buttons");
338
339	if (fPicture != NULL && fPictureRect.Contains(position)
340		&& (buttons
341			& (B_PRIMARY_MOUSE_BUTTON | B_SECONDARY_MOUSE_BUTTON)) != 0) {
342
343		_BeginDrag(position);
344
345	} else if (buttons == B_SECONDARY_MOUSE_BUTTON)
346		_ShowPopUpMenu(ConvertToScreen(position));
347}
348
349
350void
351PictureView::KeyDown(const char* bytes, int32 numBytes)
352{
353	if (numBytes != 1) {
354		BView::KeyDown(bytes, numBytes);
355		return;
356	}
357
358	switch (*bytes) {
359		case B_DELETE:
360			_SetPicture(NULL);
361			break;
362
363		default:
364			BView::KeyDown(bytes, numBytes);
365			break;
366	}
367}
368
369
370// #pragma mark -
371
372
373void
374PictureView::_ShowPopUpMenu(BPoint screen)
375{
376	if (fShowingPopUpMenu)
377		return;
378
379	PopUpMenu* menu = new PopUpMenu("PopUpMenu", this);
380
381	BMenuItem* item = new BMenuItem(B_TRANSLATE("Load image" B_UTF8_ELLIPSIS),
382		new BMessage(kMsgLoadImage));
383	menu->AddItem(item);
384
385	item = new BMenuItem(B_TRANSLATE("Remove image"), new BMessage(B_DELETE));
386	item->SetEnabled(fPicture != NULL);
387	menu->AddItem(item);
388
389	menu->SetTargetForItems(this);
390	menu->Go(screen, true, true, true);
391	fShowingPopUpMenu = true;
392}
393
394
395BBitmap*
396PictureView::_CopyPicture(uint8 alpha)
397{
398	bool hasAlpha = alpha != 255;
399
400	if (!fPicture)
401		return NULL;
402
403	BRect rect = fPictureRect.OffsetToCopy(B_ORIGIN);
404	BView view(rect, NULL, B_FOLLOW_NONE, B_WILL_DRAW);
405	BBitmap* bitmap = new(nothrow) BBitmap(rect, hasAlpha ? B_RGBA32
406		: fPicture->ColorSpace(), true);
407	if (bitmap == NULL || !bitmap->IsValid()) {
408		delete bitmap;
409		return NULL;
410	}
411
412	if (bitmap->Lock()) {
413		bitmap->AddChild(&view);
414		if (hasAlpha) {
415			view.SetHighColor(0, 0, 0, 0);
416			view.FillRect(rect);
417			view.SetDrawingMode(B_OP_ALPHA);
418			view.SetBlendingMode(B_CONSTANT_ALPHA, B_ALPHA_COMPOSITE);
419			view.SetHighColor(0, 0, 0, alpha);
420		}
421		view.DrawBitmap(fPicture, fPicture->Bounds().OffsetToCopy(B_ORIGIN),
422			rect, B_FILTER_BITMAP_BILINEAR);
423		view.Sync();
424		bitmap->RemoveChild(&view);
425		bitmap->Unlock();
426	}
427
428	return bitmap;
429}
430
431
432void
433PictureView::_BeginDrag(BPoint sourcePoint)
434{
435	BBitmap* bitmap = _CopyPicture(128);
436	if (bitmap == NULL)
437		return;
438
439	// fill the drag message
440	BMessage drag(B_SIMPLE_DATA);
441	drag.AddInt32("be:actions", B_COPY_TARGET);
442	drag.AddInt32("be:actions", B_TRASH_TARGET);
443
444	// name the clip after person name, if any
445	BString name = B_TRANSLATE("%name% picture");
446	name.ReplaceFirst("%name%", Window() ? Window()->Title() :
447		B_TRANSLATE("Unnamed person"));
448	drag.AddString("be:clip_name", name.String());
449
450	BTranslatorRoster* roster = BTranslatorRoster::Default();
451	if (roster == NULL) {
452		delete bitmap;
453		return;
454	}
455
456	int32 infoCount;
457	translator_info* info;
458	BBitmapStream stream(bitmap);
459	if (roster->GetTranslators(&stream, NULL, &info, &infoCount) == B_OK) {
460		for (int32 i = 0; i < infoCount; i++) {
461			const translation_format* formats;
462			int32 count;
463			roster->GetOutputFormats(info[i].translator, &formats, &count);
464			for (int32 j = 0; j < count; j++) {
465				if (strcmp(formats[j].MIME, "image/x-be-bitmap") != 0) {
466					// needed to send data in message
467					drag.AddString("be:types", formats[j].MIME);
468					// needed to pass data via file
469					drag.AddString("be:filetypes", formats[j].MIME);
470					drag.AddString("be:type_descriptions", formats[j].name);
471				}
472			}
473		}
474	}
475	stream.DetachBitmap(&bitmap);
476
477	// we also support "Passing Data via File" protocol
478	drag.AddString("be:types", B_FILE_MIME_TYPE);
479
480	sourcePoint -= fPictureRect.LeftTop();
481
482	SetMouseEventMask(B_POINTER_EVENTS);
483
484	DragMessage(&drag, bitmap, B_OP_ALPHA, sourcePoint);
485	bitmap = NULL;
486}
487
488
489void
490PictureView::_HandleDrop(BMessage* msg)
491{
492	entry_ref dirRef;
493	BString name, type;
494	bool saveToFile = msg->FindString("be:filetypes", &type) == B_OK
495		&& msg->FindRef("directory", &dirRef) == B_OK
496		&& msg->FindString("name", &name) == B_OK;
497
498	bool sendInMessage = !saveToFile
499		&& msg->FindString("be:types", &type) == B_OK;
500
501	if (!sendInMessage && !saveToFile)
502		return;
503
504	BBitmap* bitmap = fPicture;
505	if (bitmap == NULL)
506		return;
507
508	BTranslatorRoster* roster = BTranslatorRoster::Default();
509	if (roster == NULL)
510		return;
511
512	BBitmapStream stream(bitmap);
513
514	// find translation format we're asked for
515	translator_info* outInfo;
516	int32 outNumInfo;
517	bool found = false;
518	translation_format format;
519
520	if (roster->GetTranslators(&stream, NULL, &outInfo, &outNumInfo) == B_OK) {
521		for (int32 i = 0; i < outNumInfo; i++) {
522			const translation_format* formats;
523			int32 formatCount;
524			roster->GetOutputFormats(outInfo[i].translator, &formats,
525					&formatCount);
526			for (int32 j = 0; j < formatCount; j++) {
527				if (strcmp(formats[j].MIME, type.String()) == 0) {
528					format = formats[j];
529					found = true;
530					break;
531				}
532			}
533		}
534	}
535
536	if (!found) {
537		stream.DetachBitmap(&bitmap);
538		return;
539	}
540
541	if (sendInMessage) {
542
543		BMessage reply(B_MIME_DATA);
544		BMallocIO memStream;
545		if (roster->Translate(&stream, NULL, NULL, &memStream,
546			format.type) == B_OK) {
547			reply.AddData(format.MIME, B_MIME_TYPE, memStream.Buffer(),
548				memStream.BufferLength());
549			msg->SendReply(&reply);
550		}
551
552	} else {
553
554		BDirectory dir(&dirRef);
555		BFile file(&dir, name.String(), B_WRITE_ONLY | B_CREATE_FILE
556			| B_ERASE_FILE);
557
558		if (file.InitCheck() == B_OK
559			&& roster->Translate(&stream, NULL, NULL, &file,
560				format.type) == B_OK) {
561			BNodeInfo nodeInfo(&file);
562			if (nodeInfo.InitCheck() == B_OK)
563				nodeInfo.SetType(type.String());
564		} else {
565			BString text = B_TRANSLATE("The file '%name%' could not "
566				"be written.");
567			text.ReplaceFirst("%name%", name);
568			BAlert* alert = new BAlert(B_TRANSLATE("Error"), text.String(),
569				B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_AS_USUAL, B_STOP_ALERT);
570			alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
571			alert->Go();
572		}
573	}
574
575	// Detach, as we don't want our fPicture to be deleted
576	stream.DetachBitmap(&bitmap);
577}
578
579
580status_t
581PictureView::_LoadPicture(const entry_ref* ref)
582{
583	BFile file;
584	status_t status = file.SetTo(ref, B_READ_ONLY);
585	if (status != B_OK)
586		return status;
587
588	off_t fileSize;
589	status = file.GetSize(&fileSize);
590	if (status != B_OK)
591		return status;
592
593	// Check that we've at least some data to translate...
594	if (fileSize < 1)
595		return B_OK;
596
597	translator_info info;
598	memset(&info, 0, sizeof(translator_info));
599	BMessage ioExtension;
600
601	BTranslatorRoster* roster = BTranslatorRoster::Default();
602	if (roster == NULL)
603		return B_ERROR;
604
605	status = roster->Identify(&file, &ioExtension, &info, 0, NULL,
606		B_TRANSLATOR_BITMAP);
607
608	BBitmapStream stream;
609
610	if (status == B_OK) {
611		status = roster->Translate(&file, &info, &ioExtension, &stream,
612			B_TRANSLATOR_BITMAP);
613	}
614	if (status != B_OK)
615		return status;
616
617	BBitmap* picture = NULL;
618	if (stream.DetachBitmap(&picture) != B_OK
619		|| picture == NULL)
620		return B_ERROR;
621
622	// Remember image format so we could store using the same
623	fPictureMIMEType = info.MIME;
624	fPictureType = info.type;
625
626	_SetPicture(picture);
627	return B_OK;
628}
629
630
631void
632PictureView::_SetPicture(BBitmap* picture)
633{
634	if (fPicture != fOriginalPicture)
635		delete fPicture;
636
637	fPicture = picture;
638
639	if (picture == NULL) {
640		fPictureType = 0;
641		fPictureMIMEType = "";
642	}
643
644	Invalidate();
645}
646
647
648