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