Skip to content

Commit

Permalink
Make "Snapshot/Screenshot" test page works (#546)
Browse files Browse the repository at this point in the history
* Add ScreenshotManagerTurboModule, but it is not called.

* ...

* Update AppDelegate.mm

* Fix code due to facebook API rename

* Update AppDelegate.mm

* [Pods] Expose boost headers to consumer of RCT-Folly (#1)

* Update AppDelegate.mm

* ScreenshotManagerTurboModule gets called

* reject the promise in takeScreenshot

* ...

* Put TurboModuleRegistry.get call at the right place

* ...

* Successfully took the screenshot

* Remove unnecessary code

* Fix code review comments

* Fix code review comments

* ...

* Fix code review comments

* Fix build break

* Fix build break

* Fix build break

* Update NativeScreenshotManager.js

* Fix build break

* Fix yarn lint errors

Co-authored-by: Eloy Durán <[email protected]>
  • Loading branch information
ZihanChen-MSFT and alloy authored Aug 26, 2020
1 parent 92b68a5 commit e4bc8c4
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 32 deletions.
16 changes: 7 additions & 9 deletions RNTester/NativeModuleExample/NativeScreenshotManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ import * as TurboModuleRegistry from '../../Libraries/TurboModule/TurboModuleReg

export interface Spec extends TurboModule {
+getConstants: () => {||};
takeSnapshot(id: string): Promise<string>;
takeScreenshot(
id: string,
options: {format: string, quality?: number},
): Promise<string>;
}

const NativeModule = TurboModuleRegistry.get<Spec>('ScreenshotManager');

export function takeSnapshot(id: string): Promise<string> {
if (NativeModule != null) {
return NativeModule.takeSnapshot(id);
}
return Promise.reject();
}
export const NativeModule = (TurboModuleRegistry.get<Spec>(
'ScreenshotManager',
): ?Spec);
21 changes: 21 additions & 0 deletions RNTester/NativeModuleExample/ScreenshotMacOS.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <React/RCTViewManager.h>
#import <ReactCommon/RCTTurboModuleManager.h>

@interface ScreenshotManagerTurboModuleManagerDelegate : NSObject<RCTTurboModuleManagerDelegate>
- (std::shared_ptr<facebook::react::TurboModule>)
getTurboModule:(const std::string &)name
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker;

- (std::shared_ptr<facebook::react::TurboModule>)
getTurboModule:(const std::string &)name
instance:(id<RCTTurboModule>)instance
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker;

@end
143 changes: 143 additions & 0 deletions RNTester/NativeModuleExample/ScreenshotMacOS.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "ScreenshotMacOS.h"
#import <React/RCTUIManager.h>
#import <React/RCTUtils.h>
#import <ReactCommon/RCTTurboModuleManager.h>
#import <ReactCommon/TurboModuleUtils.h>

static NSImage* TakeScreenshot()
{
// find the key window
NSWindow* keyWindow;
for (NSWindow* window in NSApp.windows) {
if (window.keyWindow) {
keyWindow = window;
break;
}
}

// take a snapshot of the key window
CGWindowID windowID = (CGWindowID)[keyWindow windowNumber];
CGWindowImageOption imageOptions = kCGWindowImageDefault;
CGWindowListOption listOptions = kCGWindowListOptionIncludingWindow;
CGRect imageBounds = CGRectNull;
CGImageRef windowImage = CGWindowListCreateImage(
imageBounds,
listOptions,
windowID,
imageOptions);
NSImage* image = [[NSImage alloc] initWithCGImage:windowImage size:[keyWindow frame].size];

return image;
}

static NSString* SaveScreenshotToTempFile(NSImage* image)
{
// save to a temp file
NSError *error = nil;
NSString *tempFilePath = RCTTempFilePath(@"jpeg", &error);
NSData* imageData = [image TIFFRepresentation];
NSBitmapImageRep* imageRep = [NSBitmapImageRep imageRepWithData:imageData];
NSDictionary* imageProps =
[NSDictionary
dictionaryWithObject:@0.8
forKey:NSImageCompressionFactor
];
imageData = [imageRep representationUsingType:NSBitmapImageFileTypeJPEG properties:imageProps];
[imageData writeToFile:tempFilePath atomically:NO];

return tempFilePath;
}

class ScreenshotManagerTurboModule : public facebook::react::TurboModule
{
public:
ScreenshotManagerTurboModule(std::shared_ptr<facebook::react::CallInvoker> jsInvoker)
:facebook::react::TurboModule("ScreenshotManager", jsInvoker)
{
}

facebook::jsi::Value get(
facebook::jsi::Runtime& runtime,
const facebook::jsi::PropNameID& propName
) override
{
auto jsInvoker = jsInvoker_;
auto key = propName.utf8(runtime);
if (key == "takeScreenshot")
{
return facebook::jsi::Function::createFromHostFunction(
runtime,
propName,
0,
[jsInvoker](
facebook::jsi::Runtime& runtime,
const facebook::jsi::Value& thisVal,
const facebook::jsi::Value *args,
size_t count)
{
return facebook::react::createPromiseAsJSIValue(
runtime,
[jsInvoker](facebook::jsi::Runtime& runtime, std::shared_ptr<facebook::react::Promise> promise)
{
// ignore arguments, assume to be ('window', {format: 'jpeg', quality: 0.8})

dispatch_async(dispatch_get_main_queue(), ^{
NSImage* screenshotImage = TakeScreenshot();
jsInvoker->invokeAsync([screenshotImage, &runtime, promise]()
{
NSString* tempFilePath = SaveScreenshotToTempFile(screenshotImage);
promise->resolve(facebook::jsi::Value(
runtime,
facebook::jsi::String::createFromUtf8(
runtime,
std::string([tempFilePath UTF8String])
)
));
});
});
}
);
}
);
}
else
{
return facebook::jsi::Value::undefined();
}
}
};

@implementation ScreenshotManagerTurboModuleManagerDelegate

- (std::shared_ptr<facebook::react::TurboModule>)
getTurboModule:(const std::string &)name
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
if (name == "ScreenshotManager")
{
return std::make_shared<ScreenshotManagerTurboModule>(jsInvoker);
}
return nullptr;
}


- (std::shared_ptr<facebook::react::TurboModule>)
getTurboModule:(const std::string &)name
instance:(id<RCTTurboModule>)instance
jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
{
if (name == "ScreenshotManager")
{
return std::make_shared<ScreenshotManagerTurboModule>(jsInvoker);
}
return nullptr;
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,23 @@

#import "AppDelegate.h"

#import <React/JSCExecutorFactory.h>
#import <React/RCTBridge.h>
#import <React/RCTBundleURLProvider.h>
#import <React/RCTCxxBridgeDelegate.h>
#import <React/RCTLinkingManager.h>
#import <React/RCTPushNotificationManager.h>
#import <React/RCTTextAttributes.h>
#import <ReactCommon/TurboModule.h>
#import "../NativeModuleExample/ScreenshotMacOS.h"

const NSString *kBundleNameJS = @"RNTesterApp";

@interface AppDelegate () <RCTBridgeDelegate, NSUserNotificationCenterDelegate>
NSString *kBundleNameJS = @"RNTesterApp";

@interface AppDelegate () <RCTCxxBridgeDelegate, NSUserNotificationCenterDelegate>
{
ScreenshotManagerTurboModuleManagerDelegate *_turboModuleManagerDelegate;
RCTTurboModuleManager *_turboModuleManager;
}
@end

@implementation AppDelegate
Expand Down Expand Up @@ -71,6 +78,25 @@ - (NSURL *)sourceURLForBridge:(__unused RCTBridge *)bridge
fallbackResource:nil];
}

#pragma mark - RCTCxxBridgeDelegate Methods

- (std::unique_ptr<facebook::react::JSExecutorFactory>)jsExecutorFactoryForBridge:(RCTBridge *)bridge
{
__weak __typeof(self) weakSelf = self;
return std::make_unique<facebook::react::JSCExecutorFactory>([weakSelf, bridge](facebook::jsi::Runtime &runtime) {
if (!bridge) {
return;
}
__typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf->_turboModuleManagerDelegate = [ScreenshotManagerTurboModuleManagerDelegate new];
strongSelf->_turboModuleManager = [[RCTTurboModuleManager alloc] initWithBridge:bridge
delegate:strongSelf->_turboModuleManagerDelegate];
[strongSelf->_turboModuleManager installJSBindingWithRuntime:&runtime];
}
});
}

# pragma mark - Push Notifications

// Required for the remoteNotificationsRegistered event.
Expand Down
8 changes: 4 additions & 4 deletions RNTester/RNTester-macOS/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="16097.2"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
Expand Down Expand Up @@ -600,7 +600,7 @@
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleSourceList:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
</connections>
</menuItem>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
Expand Down Expand Up @@ -664,7 +664,7 @@
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController storyboardIdentifier="MainWindow" id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="RNTester" allowsToolTipsWhenApplicationIsInactive="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
<window key="window" title="RNTester" allowsToolTipsWhenApplicationIsInactive="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="538" y="299" width="640" height="480"/>
Expand Down
14 changes: 10 additions & 4 deletions RNTester/RNTesterPods.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@
7BD4120C2ED6C1CC8686E344 /* libPods-macOSBuild.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB624ED465B02C53F121EBC8 /* libPods-macOSBuild.a */; };
7C718C7B78F06C4CD1D1771A /* libPods-RNTester-macOSIntegrationTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B8A3294D74D7AC1BE88DC4E /* libPods-RNTester-macOSIntegrationTests.a */; };
8BBFF9F061783A02D50DD2EC /* libPods-RNTesterIntegrationTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8210317F3CE28B1945488740 /* libPods-RNTesterIntegrationTests.a */; };
9F15345F233AB2C4006DFE44 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F15345E233AB2C4006DFE44 /* AppDelegate.m */; };
9F15345F233AB2C4006DFE44 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9F15345E233AB2C4006DFE44 /* AppDelegate.mm */; };
9F153461233AB2C7006DFE44 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9F153460233AB2C7006DFE44 /* Assets.xcassets */; };
9F153467233AB2C7006DFE44 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 9F153466233AB2C7006DFE44 /* main.m */; };
A13DC98BDCE5EE508FFA7BE7 /* libPods-RNTester-macOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A355204268D03CF69ABC11D /* libPods-RNTester-macOS.a */; };
B2F2040A24E76D7600863BE1 /* ScreenshotMacOS.mm in Sources */ = {isa = PBXBuildFile; fileRef = B2F2040924E76D7600863BE1 /* ScreenshotMacOS.mm */; };
CDFF3988A89A5269C488653F /* libPods-iosDeviceBuild.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 287A4762E82517B7FACCE7D3 /* libPods-iosDeviceBuild.a */; };
E59A0FBD0170A092274A6207 /* libPods-RNTesterUnitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 72247071A1BF06D54F0FECC7 /* libPods-RNTesterUnitTests.a */; };
E7C1241A22BEC44B00DA25C0 /* RNTesterIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E7C1241922BEC44B00DA25C0 /* RNTesterIntegrationTests.m */; };
Expand Down Expand Up @@ -218,7 +219,7 @@
9EAC9B16F3BA65B8E1511E5E /* Pods-RNTester-macOSUnitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RNTester-macOSUnitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RNTester-macOSUnitTests/Pods-RNTester-macOSUnitTests.release.xcconfig"; sourceTree = "<group>"; };
9F15345B233AB2C4006DFE44 /* RNTester-macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "RNTester-macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
9F15345D233AB2C4006DFE44 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
9F15345E233AB2C4006DFE44 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
9F15345E233AB2C4006DFE44 /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = "<group>"; };
9F153460233AB2C7006DFE44 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9F153465233AB2C7006DFE44 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9F153466233AB2C7006DFE44 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
Expand All @@ -230,6 +231,8 @@
9F15347C233AB2C7006DFE44 /* RNTesterIntegrationTests_macOS.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNTesterIntegrationTests_macOS.m; sourceTree = "<group>"; };
9F15347E233AB2C7006DFE44 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A112BDF7485AEF790F6D3E26 /* Pods-iosDeviceBuild.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosDeviceBuild.debug.xcconfig"; path = "Pods/Target Support Files/Pods-iosDeviceBuild/Pods-iosDeviceBuild.debug.xcconfig"; sourceTree = "<group>"; };
B2F2040824E76D7600863BE1 /* ScreenshotMacOS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ScreenshotMacOS.h; path = NativeModuleExample/ScreenshotMacOS.h; sourceTree = SOURCE_ROOT; };
B2F2040924E76D7600863BE1 /* ScreenshotMacOS.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ScreenshotMacOS.mm; path = NativeModuleExample/ScreenshotMacOS.mm; sourceTree = SOURCE_ROOT; };
C81D39A606858D29861E5930 /* Pods-iosSimulatorBuild.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosSimulatorBuild.debug.xcconfig"; path = "Pods/Target Support Files/Pods-iosSimulatorBuild/Pods-iosSimulatorBuild.debug.xcconfig"; sourceTree = "<group>"; };
E65D2B0329006BC5338BB7D5 /* libPods-iosSimulatorBuild.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-iosSimulatorBuild.a"; sourceTree = BUILT_PRODUCTS_DIR; };
E68A0B7D2448B4F300228B0B /* RNTesterUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNTesterUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -532,9 +535,11 @@
9F15345C233AB2C4006DFE44 /* RNTester-macOS */ = {
isa = PBXGroup;
children = (
B2F2040824E76D7600863BE1 /* ScreenshotMacOS.h */,
B2F2040924E76D7600863BE1 /* ScreenshotMacOS.mm */,
5101985523AD9EE600118BF1 /* Main.storyboard */,
9F15345D233AB2C4006DFE44 /* AppDelegate.h */,
9F15345E233AB2C4006DFE44 /* AppDelegate.m */,
9F15345E233AB2C4006DFE44 /* AppDelegate.mm */,
5101985823AD9F5B00118BF1 /* ViewController.h */,
5101985923AD9F5B00118BF1 /* ViewController.m */,
9F153460233AB2C7006DFE44 /* Assets.xcassets */,
Expand Down Expand Up @@ -1257,10 +1262,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B2F2040A24E76D7600863BE1 /* ScreenshotMacOS.mm in Sources */,
5101985A23AD9F5B00118BF1 /* ViewController.m in Sources */,
9F153467233AB2C7006DFE44 /* main.m in Sources */,
38CB64352445042D009035CC /* RNTesterTurboModuleProvider.mm in Sources */,
9F15345F233AB2C4006DFE44 /* AppDelegate.m in Sources */,
9F15345F233AB2C4006DFE44 /* AppDelegate.mm in Sources */,
38B2630C2444F628006AB4D5 /* FlexibleSizeExampleView.m in Sources */,
38B2630B2444F5EB006AB4D5 /* UpdatePropertiesExampleView.m in Sources */,
);
Expand Down
28 changes: 16 additions & 12 deletions RNTester/js/examples/Snapshot/SnapshotExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,9 @@
'use strict';

const React = require('react');
const {
Alert,
Image,
NativeModules,
StyleSheet,
Text,
View,
} = require('react-native');
const ScreenshotManager = NativeModules.ScreenshotManager;
const {Alert, Image, StyleSheet, Text, View} = require('react-native');

import {NativeModule as ScreenshotManager} from '../../../NativeModuleExample/NativeScreenshotManager';

class ScreenshotExample extends React.Component<{...}, $FlowFixMeState> {
state = {
Expand All @@ -37,10 +31,20 @@ class ScreenshotExample extends React.Component<{...}, $FlowFixMeState> {
);
}

// TODO(macOS ISS#2323203): alert needs two string arguments, passing an error results in crashing
takeScreenshot = () => {
ScreenshotManager.takeScreenshot('window', {format: 'jpeg', quality: 0.8}) // See UIManager.js for options
.then(uri => this.setState({uri}))
.catch(error => Alert.alert(error));
if (ScreenshotManager !== undefined && ScreenshotManager !== null) {
ScreenshotManager.takeScreenshot('window', {format: 'jpeg', quality: 0.8}) // See UIManager.js for options
.then(uri => this.setState({uri}))
.catch(error =>
Alert.alert('ScreenshotManager.takeScreenshot', error.message),
);
} else {
Alert.alert(
'ScreenshotManager.takeScreenshot',
'The turbo module is not installed.',
);
}
};
}

Expand Down
3 changes: 3 additions & 0 deletions third-party-podspecs/RCT-Folly.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ Pod::Spec.new do |spec|
"CLANG_CXX_LANGUAGE_STANDARD" => "c++14",
"HEADER_SEARCH_PATHS" => "\"$(PODS_TARGET_SRCROOT)\" \"$(PODS_ROOT)/boost-for-react-native\" \"$(PODS_ROOT)/DoubleConversion\"" }

# TODO: The boost spec should really be selecting these files so that dependents of Folly can also access the required headers.
spec.user_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost-for-react-native\"" }

spec.default_subspec = 'Default'

spec.subspec 'Default' do
Expand Down

0 comments on commit e4bc8c4

Please sign in to comment.