xref: /haiku/src/apps/screenshot/ScreenshotWindow.cpp (revision bef5fffeae877e7a2b825cdb81676f6df0d1fd60)
1 /*
2  * Copyright Karsten Heimrich, host.haiku@gmx.de. All rights reserved.
3  * Distributed under the terms of the MIT License.
4  */
5 
6 #include "ScreenshotWindow.h"
7 
8 #include "PNGDump.h"
9 
10 
11 #include <Application.h>
12 #include <Bitmap.h>
13 #include <Box.h>
14 #include <BitmapStream.h>
15 #include <Button.h>
16 #include <CardLayout.h>
17 #include <CheckBox.h>
18 #include <Directory.h>
19 #include <Entry.h>
20 #include <File.h>
21 #include <FindDirectory.h>
22 #include <FilePanel.h>
23 #include <GridLayoutBuilder.h>
24 #include <GroupLayoutBuilder.h>
25 #include <LayoutItem.h>
26 #include <Menu.h>
27 #include <MenuField.h>
28 #include <MenuItem.h>
29 #include <NodeInfo.h>
30 #include <Path.h>
31 #include <RadioButton.h>
32 #include <Screen.h>
33 #include <String.h>
34 #include <StringView.h>
35 #include <SpaceLayoutItem.h>
36 #include <TextControl.h>
37 #include <TranslatorFormats.h>
38 #include <TranslationUtils.h>
39 #include <TranslatorRoster.h>
40 #include <View.h>
41 #include <WindowInfo.h>
42 
43 
44 #include <stdio.h>
45 #include <stdlib.h>
46 
47 
48 enum {
49 	kScreenshotType,
50 	kIncludeBorder,
51 	kShowCursor,
52 	kBackToSave,
53 	kTakeScreenshot,
54 	kImageOutputFormat,
55 	kLocationChanged,
56 	kChooseLocation,
57 	kFinishScreenshot,
58 	kShowOptions
59 };
60 
61 
62 // #pragma mark - DirectoryRefFilter
63 
64 
65 class DirectoryRefFilter : public BRefFilter {
66 public:
67 	virtual			~DirectoryRefFilter() {}
68 			bool	Filter(const entry_ref* ref, BNode* node, struct stat* stat,
69 						const char* filetype)
70 					{
71 						return node->IsDirectory();
72 					}
73 };
74 
75 
76 // #pragma mark - ScreenshotWindow
77 
78 
79 ScreenshotWindow::ScreenshotWindow(bigtime_t delay, bool includeBorder,
80 	bool includeCursor, bool grabActiveWindow, bool showConfigWindow,
81 	bool saveScreenshotSilent)
82 	: BWindow(BRect(0, 0, 200.0, 100.0), "Take Screenshot", B_TITLED_WINDOW,
83 		B_NOT_ZOOMABLE | B_NOT_RESIZABLE | B_QUIT_ON_WINDOW_CLOSE |
84 		B_AVOID_FRONT | B_AUTO_UPDATE_SIZE_LIMITS | B_CLOSE_ON_ESCAPE),
85 	fScreenshot(NULL),
86 	fOutputPathPanel(NULL),
87 	fLastSelectedPath(NULL),
88 	fDelay(delay),
89 	fIncludeBorder(includeBorder),
90 	fIncludeCursor(includeCursor),
91 	fGrabActiveWindow(grabActiveWindow),
92 	fShowConfigWindow(showConfigWindow),
93 	fSaveScreenshotSilent(saveScreenshotSilent)
94 {
95 	_InitWindow();
96 	_CenterAndShow();
97 }
98 
99 
100 ScreenshotWindow::~ScreenshotWindow()
101 {
102 	if (fOutputPathPanel)
103 		delete fOutputPathPanel->RefFilter();
104 
105 	delete fScreenshot;
106 	delete fOutputPathPanel;
107 }
108 
109 
110 void
111 ScreenshotWindow::MessageReceived(BMessage* message)
112 {
113 	switch (message->what) {
114 		case kScreenshotType: {
115 			fGrabActiveWindow = false;
116 			if (fActiveWindow->Value() == B_CONTROL_ON)
117 				fGrabActiveWindow = true;
118 			fWindowBorder->SetEnabled(fGrabActiveWindow);
119 		}	break;
120 
121 		case kIncludeBorder: {
122 				fIncludeBorder = (fWindowBorder->Value() == B_CONTROL_ON);
123 		}	break;
124 
125 		case kShowCursor: {
126 			fIncludeCursor = (fShowCursor->Value() == B_CONTROL_ON);
127 		}	break;
128 
129 		case kBackToSave: {
130 			BCardLayout* layout = dynamic_cast<BCardLayout*> (GetLayout());
131 			if (layout)
132 				layout->SetVisibleItem(1L);
133 			SetTitle("Save Screenshot");
134 		}	break;
135 
136 		case kTakeScreenshot: {
137 			Hide();
138 			_TakeScreenshot();
139 			Show();
140 		}	break;
141 
142 		case kImageOutputFormat: {
143 			message->FindInt32("be:type", &fImageFileType);
144 			message->FindInt32("be:translator", &fTranslator);
145 		}	break;
146 
147 		case kLocationChanged: {
148 			void* source = NULL;
149 			if (message->FindPointer("source", &source) == B_OK)
150 				fLastSelectedPath = static_cast<BMenuItem*> (source);
151 
152 			const char* text = fNameControl->Text();
153 			fNameControl->SetText(_FindValidFileName(text).String());
154 		}	break;
155 
156 		case kChooseLocation: {
157 			if (!fOutputPathPanel) {
158 				BMessenger target(this);
159 				fOutputPathPanel = new BFilePanel(B_OPEN_PANEL, &target,
160 					NULL, B_DIRECTORY_NODE, false, NULL, new DirectoryRefFilter());
161 				fOutputPathPanel->Window()->SetTitle("Choose directory");
162 				fOutputPathPanel->SetButtonLabel(B_DEFAULT_BUTTON, "Select");
163 			}
164 			fOutputPathPanel->Show();
165 		}	break;
166 
167 		case B_CANCEL: {
168 			fLastSelectedPath->SetMarked(true);
169 		}	break;
170 
171 		case B_REFS_RECEIVED: {
172 			entry_ref ref;
173 			if (message->FindRef("refs", &ref) == B_OK) {
174 				BString path(BPath(&ref).Path());
175 
176 				BMessage* message = new BMessage(kLocationChanged);
177 				message->AddString("path", path.String());
178 
179 				fOutputPathMenu->TruncateString(&path, B_TRUNCATE_MIDDLE,
180 					fOutputPathMenu->StringWidth("SomethingLongHere"));
181 				fLastSelectedPath = new BMenuItem(path.String(), message);
182 				fLastSelectedPath->SetMarked(true);
183 
184 				fOutputPathMenu->AddItem(fLastSelectedPath,
185 					fOutputPathMenu->CountItems() - 2);
186 			}
187 		}	break;
188 
189 		case kFinishScreenshot: {
190 			_SaveScreenshot();
191 			be_app_messenger.SendMessage(B_QUIT_REQUESTED);
192 		}	break;
193 
194 		case kShowOptions: {
195 			BCardLayout* layout = dynamic_cast<BCardLayout*> (GetLayout());
196 			if (layout)
197 				layout->SetVisibleItem(0L);
198 			SetTitle("Take Screenshot");
199 			fBackToSave->SetEnabled(true);
200 		}	break;
201 
202 		default: {
203 			BWindow::MessageReceived(message);
204 		}	break;
205 	};
206 
207 }
208 
209 
210 void
211 ScreenshotWindow::_InitWindow()
212 {
213 	BCardLayout* layout = new BCardLayout();
214 	SetLayout(layout);
215 
216 	_SetupFirstLayoutItem(layout);
217 	_SetupSecondLayoutItem(layout);
218 
219 	if (!fShowConfigWindow) {
220 		_TakeScreenshot();
221 		layout->SetVisibleItem(1L);
222 	} else {
223 		layout->SetVisibleItem(0L);
224 	}
225 }
226 
227 
228 void
229 ScreenshotWindow::_SetupFirstLayoutItem(BCardLayout* layout)
230 {
231 	BStringView* stringView = new BStringView("", "Options");
232 	stringView->SetFont(be_bold_font);
233 	stringView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
234 
235 	fActiveWindow = new BRadioButton("Take active window",
236 		 new BMessage(kScreenshotType));
237 	fWholeDesktop = new BRadioButton("Take whole Desktop",
238 		 new BMessage(kScreenshotType));
239 	fWholeDesktop->SetValue(B_CONTROL_ON);
240 
241 	BString delay;
242 	delay << fDelay / 1000000;
243 	fDelayControl = new BTextControl("", "Take screenshot after a delay of",
244 		delay.String(), NULL);
245 	_DisallowChar(fDelayControl->TextView());
246 	fDelayControl->TextView()->SetAlignment(B_ALIGN_RIGHT);
247 
248 	BStringView* stringView2 = new BStringView("", "seconds");
249 	stringView2->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
250 
251 	fWindowBorder = new BCheckBox("Include window border",
252 		new BMessage(kIncludeBorder));
253 	fWindowBorder->SetEnabled(false);
254 
255 	fShowCursor = new BCheckBox("Include cursor in screenshot",
256 		new BMessage(kShowCursor));
257 	fShowCursor->SetValue(fIncludeCursor);
258 
259 	BBox* divider = new BBox(B_FANCY_BORDER, NULL);
260 	divider->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, 1));
261 
262 	fBackToSave = new BButton("", "Back to save", new BMessage(kBackToSave));
263 	fBackToSave->SetEnabled(false);
264 
265 	fTakeScreenshot = new BButton("", "Take Screenshot",
266 		new BMessage(kTakeScreenshot));
267 
268 	layout->AddView(0, BGroupLayoutBuilder(B_VERTICAL)
269 		.Add(stringView)
270 		.Add(BGridLayoutBuilder()
271 			.Add(BSpaceLayoutItem::CreateHorizontalStrut(15.0), 0, 0)
272 			.Add(fWholeDesktop, 1, 0)
273 			.Add(BSpaceLayoutItem::CreateHorizontalStrut(15.0), 0, 1)
274 			.Add(fActiveWindow, 1, 1)
275 			.SetInsets(0.0, 5.0, 0.0, 0.0))
276 		.AddGroup(B_HORIZONTAL)
277 			.AddStrut(30.0)
278 			.Add(fWindowBorder)
279 			.End()
280 		.AddStrut(10.0)
281 		.AddGroup(B_HORIZONTAL)
282 			.AddStrut(15.0)
283 			.Add(fShowCursor)
284 			.End()
285 		.AddStrut(5.0)
286 		.AddGroup(B_HORIZONTAL, 5.0)
287 			.AddStrut(10.0)
288 			.Add(fDelayControl->CreateLabelLayoutItem())
289 			.Add(fDelayControl->CreateTextViewLayoutItem())
290 			.Add(stringView2)
291 			.End()
292 		.AddStrut(10.0)
293 		.AddGlue()
294 		.Add(divider)
295 		.AddStrut(10)
296 		.AddGroup(B_HORIZONTAL, 10.0)
297 			.Add(fBackToSave)
298 			.AddGlue()
299 			.Add(new BButton("", "Cancel", new BMessage(B_QUIT_REQUESTED)))
300 			.Add(fTakeScreenshot)
301 			.End()
302 		.SetInsets(10.0, 10.0, 10.0, 10.0)
303 	);
304 
305 	if (fGrabActiveWindow) {
306 		fWindowBorder->SetEnabled(true);
307 		fActiveWindow->SetValue(B_CONTROL_ON);
308 		fWindowBorder->SetValue(fIncludeBorder);
309 	}
310 }
311 
312 
313 void
314 ScreenshotWindow::_SetupSecondLayoutItem(BCardLayout* layout)
315 {
316 	fPreviewBox = new BBox(BRect(0.0, 0.0, 200.0, 150.0));
317 	fPreviewBox->SetExplicitMinSize(BSize(200.0, B_SIZE_UNSET));
318 	fPreviewBox->SetFlags(fPreviewBox->Flags() | B_FULL_UPDATE_ON_RESIZE);
319 
320 	fNameControl = new BTextControl("", "Name:", "screenshot", NULL);
321 
322 	_SetupTranslatorMenu(new BMenu("Please select"));
323 	BMenuField* menuField = new BMenuField("Save as:", fTranslatorMenu);
324 
325 	_SetupOutputPathMenu(new BMenu("Please select"));
326 	BMenuField* menuField2 = new BMenuField("Save in:", fOutputPathMenu);
327 
328 	fNameControl->SetText(_FindValidFileName("screenshot").String());
329 
330 	BBox* divider = new BBox(B_FANCY_BORDER, NULL);
331 	divider->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, 1));
332 
333 	BGridLayout* gridLayout = BGridLayoutBuilder(0.0, 5.0)
334 		.Add(fNameControl->CreateLabelLayoutItem(), 0, 0)
335 		.Add(fNameControl->CreateTextViewLayoutItem(), 1, 0)
336 		.Add(menuField->CreateLabelLayoutItem(), 0, 1)
337 		.Add(menuField->CreateMenuBarLayoutItem(), 1, 1)
338 		.Add(menuField2->CreateLabelLayoutItem(), 0, 2)
339 		.Add(menuField2->CreateMenuBarLayoutItem(), 1, 2);
340 	gridLayout->SetMinColumnWidth(1, menuField->StringWidth("SomethingLongHere"));
341 
342 	layout->AddView(1, BGroupLayoutBuilder(B_VERTICAL)
343 		.Add(BGroupLayoutBuilder(B_HORIZONTAL, 10.0)
344 			.Add(fPreviewBox)
345 			.AddGroup(B_VERTICAL)
346 				.Add(gridLayout->View())
347 				.AddGlue()
348 				.End())
349 		.AddStrut(10)
350 		.Add(divider)
351 		.AddStrut(10)
352 		.AddGroup(B_HORIZONTAL, 10.0)
353 			.Add(new BButton("", "Options", new BMessage(kShowOptions)))
354 			.AddGlue()
355 			.Add(new BButton("", "Cancel", new BMessage(B_QUIT_REQUESTED)))
356 			.Add(new BButton("", "Save", new BMessage(kFinishScreenshot)))
357 			.End()
358 		.SetInsets(10.0, 10.0, 10.0, 10.0)
359 	);
360 }
361 
362 
363 void
364 ScreenshotWindow::_DisallowChar(BTextView* textView)
365 {
366 	for (uint32 i = 0; i < '0'; ++i)
367 		textView->DisallowChar(i);
368 
369 	for (uint32 i = '9' + 1; i < 255; ++i)
370 		textView->DisallowChar(i);
371 }
372 
373 
374 void
375 ScreenshotWindow::_SetupTranslatorMenu(BMenu* translatorMenu)
376 {
377 	fTranslatorMenu = translatorMenu;
378 
379 	BMessage message(kImageOutputFormat);
380 	fTranslatorMenu = new BMenu("Please select");
381 	BTranslationUtils::AddTranslationItems(fTranslatorMenu, B_TRANSLATOR_BITMAP,
382 		&message, NULL, NULL, NULL);
383 
384 	fTranslatorMenu->SetLabelFromMarked(true);
385 
386 	if (fTranslatorMenu->ItemAt(0))
387 		fTranslatorMenu->ItemAt(0)->SetMarked(true);
388 
389 	for (int32 i = 0; i < fTranslatorMenu->CountItems(); ++i) {
390 		BMenuItem* item = fTranslatorMenu->ItemAt(i);
391 		if (item && item->Message()) {
392 			item->Message()->FindInt32("be:type", &fImageFileType);
393 			item->Message()->FindInt32("be:translator", &fTranslator);
394 			if (fImageFileType == B_PNG_FORMAT) {
395 				item->SetMarked(true);
396 				break;
397 			}
398 		}
399 	}
400 }
401 
402 
403 void
404 ScreenshotWindow::_SetupOutputPathMenu(BMenu* outputPathMenu)
405 {
406 	fOutputPathMenu = outputPathMenu;
407 
408 	BPath path;
409 	find_directory(B_USER_DIRECTORY, &path);
410 
411 	BMessage* message = new BMessage(kLocationChanged);
412 	message->AddString("path", path.Path());
413 	fOutputPathMenu->AddItem(new BMenuItem("Home directory", message));
414 
415 	path.Append("Desktop");
416 	message = new BMessage(kLocationChanged);
417 	message->AddString("path", path.Path());
418 	fOutputPathMenu->AddItem(new BMenuItem("Desktop", message), 0);
419 
420 	find_directory(B_BEOS_ETC_DIRECTORY, &path);
421 	path.Append("artwork");
422 
423 	message = new BMessage(kLocationChanged);
424 	message->AddString("path", path.Path());
425 	fOutputPathMenu->AddItem(new BMenuItem("Artwork directory", message));
426 
427 	fOutputPathMenu->AddItem(new BSeparatorItem());
428 
429 	fOutputPathMenu->AddItem(new BMenuItem("Choose directory...",
430 		new BMessage(kChooseLocation)));
431 
432 	fOutputPathMenu->SetLabelFromMarked(true);
433 	fOutputPathMenu->ItemAt(1)->SetMarked(true);
434 	fLastSelectedPath = fOutputPathMenu->ItemAt(1);
435 }
436 
437 
438 BString
439 ScreenshotWindow::_FindValidFileName(const char* name) const
440 {
441 	BString fileName(name);
442 	if (!fLastSelectedPath)
443 		return fileName;
444 
445 	const char* path;
446 	BMessage* message = fLastSelectedPath->Message();
447 	if (!message || message->FindString("path", &path) != B_OK)
448 		return fileName;
449 
450 	BPath outputPath(path);
451 	outputPath.Append(name);
452 	if (!BEntry(outputPath.Path()).Exists())
453 		return fileName;
454 
455 	BEntry entry;
456 	int32 index = 1;
457 	char filename[32];
458 	do {
459 		sprintf(filename, "%s%ld", name, index++);
460 		outputPath.SetTo(path);
461 		outputPath.Append(filename);
462 		entry.SetTo(outputPath.Path());
463 	} while (entry.Exists());
464 
465 	return BString(filename);
466 }
467 
468 
469 void
470 ScreenshotWindow::_CenterAndShow()
471 {
472 	if (fSaveScreenshotSilent) {
473 		_SaveScreenshotSilent();
474 		be_app_messenger.SendMessage(B_QUIT_REQUESTED);
475 		return;
476 	}
477 
478 	BSize size = GetLayout()->PreferredSize();
479 	ResizeTo(size.Width(), size.Height());
480 
481 	BRect frame(BScreen(this).Frame());
482 	MoveTo((frame.Width() - size.Width()) / 2.0,
483 		(frame.Height() - size.Height()) / 2.0);
484 
485 	Show();
486 }
487 
488 
489 void
490 ScreenshotWindow::_TakeScreenshot()
491 {
492 	snooze((atoi(fDelayControl->Text()) * 1000000) + 50000);
493 
494 	BRect frame;
495 	delete fScreenshot;
496 	if (_GetActiveWindowFrame(&frame) == B_OK) {
497 		fScreenshot = new BBitmap(frame.OffsetToCopy(B_ORIGIN), B_RGBA32);
498 		BScreen(this).ReadBitmap(fScreenshot, fIncludeCursor, &frame);
499 	} else {
500 		BScreen(this).GetBitmap(&fScreenshot, fIncludeCursor);
501 	}
502 
503 	fPreviewBox->ClearViewBitmap();
504 	fPreviewBox->SetViewBitmap(fScreenshot, fScreenshot->Bounds(),
505 		fPreviewBox->Bounds(), B_FOLLOW_ALL, 0);
506 
507 	BCardLayout* layout = dynamic_cast<BCardLayout*> (GetLayout());
508 	if (layout)
509 		layout->SetVisibleItem(1L);
510 
511 	SetTitle("Save Screenshot");
512 }
513 
514 
515 status_t
516 ScreenshotWindow::_GetActiveWindowFrame(BRect* frame)
517 {
518 	if (!fGrabActiveWindow || !frame)
519 		return B_ERROR;
520 
521 	int32* tokens;
522 	int32 tokenCount;
523 	status_t status = BPrivate::get_window_order(B_CURRENT_WORKSPACE, &tokens,
524 		&tokenCount);
525 	if (status != B_OK || !tokens || tokenCount < 1)
526 		return B_ERROR;
527 
528 	status = B_ERROR;
529 	for (int32 i = 0; i < tokenCount; ++i) {
530 		client_window_info* windowInfo = get_window_info(tokens[i]);
531 		if (!windowInfo->is_mini && !windowInfo->show_hide_level > 0) {
532 			frame->left = windowInfo->window_left;
533 			frame->top = windowInfo->window_top;
534 			frame->right = windowInfo->window_right;
535 			frame->bottom = windowInfo->window_bottom;
536 
537 			status = B_OK;
538 			free(windowInfo);
539 
540 			if (fIncludeBorder) {
541 				// TODO: that's wrong for windows without titlebar, change once
542 				//		 we can access the decorator or get it via window info
543 				frame->InsetBy(-5.0, -5.0);
544 				frame->top -= 22.0;
545 			}
546 
547 			BRect screenFrame(BScreen(this).Frame());
548 			if (frame->left < screenFrame.left)
549 				frame->left = screenFrame.left;
550 			if (frame->top < screenFrame.top)
551 				frame->top = screenFrame.top;
552 			if (frame->right > screenFrame.right)
553 				frame->right = screenFrame.right;
554 			if (frame->bottom > screenFrame.bottom)
555 				frame->bottom = screenFrame.bottom;
556 
557 			break;
558 		}
559 		free(windowInfo);
560 	}
561 	free(tokens);
562 	return status;
563 }
564 
565 
566 void
567 ScreenshotWindow::_SaveScreenshot()
568 {
569 	if (!fScreenshot || !fLastSelectedPath)
570 		return;
571 
572 	const char* path;
573 	BMessage* message = fLastSelectedPath->Message();
574 	if (!message || message->FindString("path", &path) != B_OK)
575 		return;
576 
577 	BDirectory dir(path);
578 	BFile file(&dir, fNameControl->Text(), B_CREATE_FILE |
579 		B_ERASE_FILE | B_WRITE_ONLY);
580 	if (file.InitCheck() != B_OK)
581 		return;
582 
583 	BBitmapStream bitmapStream(fScreenshot);
584 	BTranslatorRoster* roster = BTranslatorRoster::Default();
585 	roster->Translate(&bitmapStream, NULL, NULL, &file, fImageFileType,
586 		B_TRANSLATOR_BITMAP);
587 	fScreenshot = NULL;
588 
589 	BNodeInfo nodeInfo(&file);
590 	if (nodeInfo.InitCheck() != B_OK)
591 		return;
592 
593 	int32 numFormats;
594 	const translation_format* formats = NULL;
595 	if (roster->GetOutputFormats(fTranslator, &formats, &numFormats) != B_OK)
596 		return;
597 
598 	for (int32 i = 0; i < numFormats; ++i) {
599 		if (formats[i].type == uint32(fImageFileType)) {
600 			nodeInfo.SetType(formats[i].MIME);
601 			break;
602 		}
603 	}
604 }
605 
606 
607 void
608 ScreenshotWindow::_SaveScreenshotSilent() const
609 {
610 	if (!fScreenshot)
611 		return;
612 
613 	BPath homePath;
614 	if (find_directory(B_USER_DIRECTORY, &homePath) != B_OK) {
615 		fprintf(stderr, "failed to find user home directory\n");
616 		return;
617 	}
618 
619 	BPath path;
620 	BEntry entry;
621 	int32 index = 1;
622 	do {
623 		char filename[32];
624 		sprintf(filename, "screenshot%ld.png", index++);
625 		path = homePath;
626 		path.Append(filename);
627 		entry.SetTo(path.Path());
628 	} while (entry.Exists());
629 
630 	// Dump to PNG
631 	SaveToPNG(path.Path(), fScreenshot->Bounds(), fScreenshot->ColorSpace(),
632 		fScreenshot->Bits(), fScreenshot->BitsLength(),
633 		fScreenshot->BytesPerRow());
634 }
635