Skip to content

Commit

Permalink
Merge pull request electron#11300 from electron/external-devtools
Browse files Browse the repository at this point in the history
Add API to set arbitrary WebContents as devtools
  • Loading branch information
zcbenz authored Dec 5, 2017
2 parents bdcdb6e + 52c6f4b commit d598aa1
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 47 deletions.
18 changes: 13 additions & 5 deletions atom/browser/api/atom_api_web_contents.cc
Original file line number Diff line number Diff line change
Expand Up @@ -895,10 +895,11 @@ void WebContents::DevToolsOpened() {
managed_web_contents()->CallClientFunction(
"DevToolsAPI.setInspectedTabId", &tab_id, nullptr, nullptr);

// Inherit owner window in devtools.
if (owner_window())
handle->SetOwnerWindow(managed_web_contents()->GetDevToolsWebContents(),
owner_window());
// Inherit owner window in devtools when it doesn't have one.
auto* devtools = managed_web_contents()->GetDevToolsWebContents();
bool has_window = devtools->GetUserData(NativeWindowRelay::UserDataKey());
if (owner_window() && !has_window)
handle->SetOwnerWindow(devtools, owner_window());

Emit("devtools-opened");
}
Expand Down Expand Up @@ -1178,7 +1179,8 @@ void WebContents::OpenDevTools(mate::Arguments* args) {
std::string state;
if (type_ == WEB_VIEW || !owner_window()) {
state = "detach";
} else if (args && args->Length() == 1) {
}
if (args && args->Length() == 1) {
bool detach = false;
mate::Dictionary options;
if (args->GetNext(&options)) {
Expand Down Expand Up @@ -1808,6 +1810,11 @@ void WebContents::SetEmbedder(const WebContents* embedder) {
}
}

void WebContents::SetDevToolsWebContents(const WebContents* devtools) {
if (managed_web_contents())
managed_web_contents()->SetDevToolsWebContents(devtools->web_contents());
}

v8::Local<v8::Value> WebContents::GetNativeView() const {
gfx::NativeView ptr = web_contents()->GetNativeView();
auto buffer = node::Buffer::Copy(
Expand Down Expand Up @@ -1929,6 +1936,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate,
.SetMethod("copyImageAt", &WebContents::CopyImageAt)
.SetMethod("capturePage", &WebContents::CapturePage)
.SetMethod("setEmbedder", &WebContents::SetEmbedder)
.SetMethod("setDevToolsWebContents", &WebContents::SetDevToolsWebContents)
.SetMethod("getNativeView", &WebContents::GetNativeView)
.SetMethod("setWebRTCIPHandlingPolicy",
&WebContents::SetWebRTCIPHandlingPolicy)
Expand Down
1 change: 1 addition & 0 deletions atom/browser/api/atom_api_web_contents.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ class WebContents : public mate::TrackableObject<WebContents>,
void Print(mate::Arguments* args);
std::vector<printing::PrinterBasicInfo> GetPrinterList();
void SetEmbedder(const WebContents* embedder);
void SetDevToolsWebContents(const WebContents* devtools);
v8::Local<v8::Value> GetNativeView() const;

// Print current page as PDF.
Expand Down
4 changes: 4 additions & 0 deletions atom/browser/native_window.h
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,10 @@ class NativeWindowRelay :
explicit NativeWindowRelay(base::WeakPtr<NativeWindow> window)
: key(UserDataKey()), window(window) {}

static void* UserDataKey() {
return content::WebContentsUserData<NativeWindowRelay>::UserDataKey();
}

void* key;
base::WeakPtr<NativeWindow> window;

Expand Down
1 change: 1 addition & 0 deletions brightray/browser/inspectable_web_contents.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class InspectableWebContents {
virtual void SetDelegate(InspectableWebContentsDelegate* delegate) = 0;
virtual InspectableWebContentsDelegate* GetDelegate() const = 0;

virtual void SetDevToolsWebContents(content::WebContents* devtools) = 0;
virtual void SetDockState(const std::string& state) = 0;
virtual void ShowDevTools() = 0;
virtual void CloseDevTools() = 0;
Expand Down
103 changes: 63 additions & 40 deletions brightray/browser/inspectable_web_contents_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -242,13 +242,14 @@ InspectableWebContentsImpl::InspectableWebContentsImpl(

InspectableWebContentsImpl::~InspectableWebContentsImpl() {
// Unsubscribe from devtools and Clean up resources.
if (devtools_web_contents_) {
devtools_web_contents_->SetDelegate(nullptr);
if (GetDevToolsWebContents()) {
if (managed_devtools_web_contents_)
managed_devtools_web_contents_->SetDelegate(nullptr);
// Calling this also unsubscribes the observer, so WebContentsDestroyed
// won't be called again.
WebContentsDestroyed();
}
// Let destructor destroy devtools_web_contents_.
// Let destructor destroy managed_devtools_web_contents_.
}

InspectableWebContentsView* InspectableWebContentsImpl::GetView() const {
Expand All @@ -261,7 +262,10 @@ content::WebContents* InspectableWebContentsImpl::GetWebContents() const {

content::WebContents* InspectableWebContentsImpl::GetDevToolsWebContents()
const {
return devtools_web_contents_.get();
if (external_devtools_web_contents_)
return external_devtools_web_contents_;
else
return managed_devtools_web_contents_.get();
}

void InspectableWebContentsImpl::InspectElement(int x, int y) {
Expand All @@ -288,43 +292,56 @@ void InspectableWebContentsImpl::SetDockState(const std::string& state) {
}
}

void InspectableWebContentsImpl::SetDevToolsWebContents(
content::WebContents* devtools) {
if (!managed_devtools_web_contents_)
external_devtools_web_contents_ = devtools;
}

void InspectableWebContentsImpl::ShowDevTools() {
if (embedder_message_dispatcher_) {
if (managed_devtools_web_contents_)
view_->ShowDevTools();
return;
}

// Show devtools only after it has done loading, this is to make sure the
// SetIsDocked is called *BEFORE* ShowDevTools.
if (!devtools_web_contents_) {
embedder_message_dispatcher_.reset(
DevToolsEmbedderMessageDispatcher::CreateForDevToolsFrontend(this));

content::WebContents::CreateParams create_params(
web_contents_->GetBrowserContext());
devtools_web_contents_.reset(content::WebContents::Create(create_params));

Observe(devtools_web_contents_.get());
devtools_web_contents_->SetDelegate(this);
embedder_message_dispatcher_.reset(
DevToolsEmbedderMessageDispatcher::CreateForDevToolsFrontend(this));

if (!external_devtools_web_contents_) { // no external devtools
managed_devtools_web_contents_.reset(
content::WebContents::Create(
content::WebContents::CreateParams(
web_contents_->GetBrowserContext())));
managed_devtools_web_contents_->SetDelegate(this);
}

AttachTo(content::DevToolsAgentHost::GetOrCreateFor(web_contents_.get()));
Observe(GetDevToolsWebContents());
AttachTo(content::DevToolsAgentHost::GetOrCreateFor(web_contents_.get()));

devtools_web_contents_->GetController().LoadURL(
GetDevToolsURL(can_dock_),
content::Referrer(),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
std::string());
} else {
view_->ShowDevTools();
}
GetDevToolsWebContents()->GetController().LoadURL(
GetDevToolsURL(can_dock_),
content::Referrer(),
ui::PAGE_TRANSITION_AUTO_TOPLEVEL,
std::string());
}

void InspectableWebContentsImpl::CloseDevTools() {
if (devtools_web_contents_) {
if (GetDevToolsWebContents()) {
frontend_loaded_ = false;
view_->CloseDevTools();
devtools_web_contents_.reset();
if (managed_devtools_web_contents_) {
view_->CloseDevTools();
managed_devtools_web_contents_.reset();
}
embedder_message_dispatcher_.reset();
web_contents_->Focus();
}
}

bool InspectableWebContentsImpl::IsDevToolsViewShowing() {
return devtools_web_contents_ && view_->IsDevToolsViewShowing();
return managed_devtools_web_contents_ && view_->IsDevToolsViewShowing();
}

void InspectableWebContentsImpl::AttachTo(
Expand All @@ -347,7 +364,7 @@ void InspectableWebContentsImpl::CallClientFunction(
const base::Value* arg1,
const base::Value* arg2,
const base::Value* arg3) {
if (!devtools_web_contents_)
if (!GetDevToolsWebContents())
return;

std::string javascript = function_name + "(";
Expand All @@ -365,7 +382,7 @@ void InspectableWebContentsImpl::CallClientFunction(
}
}
javascript.append(");");
devtools_web_contents_->GetMainFrame()->ExecuteJavaScript(
GetDevToolsWebContents()->GetMainFrame()->ExecuteJavaScript(
base::UTF8ToUTF16(javascript));
}

Expand Down Expand Up @@ -400,7 +417,8 @@ void InspectableWebContentsImpl::CloseWindow() {

void InspectableWebContentsImpl::LoadCompleted() {
frontend_loaded_ = true;
view_->ShowDevTools();
if (managed_devtools_web_contents_)
view_->ShowDevTools();

// If the devtools can dock, "SetIsDocked" will be called by devtools itself.
if (!can_dock_) {
Expand All @@ -415,7 +433,7 @@ void InspectableWebContentsImpl::LoadCompleted() {
}
base::string16 javascript = base::UTF8ToUTF16(
"Components.dockController.setDockSide(\"" + dock_state_ + "\");");
devtools_web_contents_->GetMainFrame()->ExecuteJavaScript(javascript);
GetDevToolsWebContents()->GetMainFrame()->ExecuteJavaScript(javascript);
}

if (view_->GetDelegate())
Expand All @@ -428,15 +446,17 @@ void InspectableWebContentsImpl::SetInspectedPageBounds(const gfx::Rect& rect) {
return;

contents_resizing_strategy_.CopyFrom(strategy);
view_->SetContentsResizingStrategy(contents_resizing_strategy_);
if (managed_devtools_web_contents_)
view_->SetContentsResizingStrategy(contents_resizing_strategy_);
}

void InspectableWebContentsImpl::InspectElementCompleted() {
}

void InspectableWebContentsImpl::InspectedURLChanged(const std::string& url) {
view_->SetTitle(base::UTF8ToUTF16(base::StringPrintf(kTitleFormat,
url.c_str())));
if (managed_devtools_web_contents_)
view_->SetTitle(base::UTF8ToUTF16(base::StringPrintf(kTitleFormat,
url.c_str())));
}

void InspectableWebContentsImpl::LoadNetworkResource(
Expand All @@ -452,8 +472,8 @@ void InspectableWebContentsImpl::LoadNetworkResource(
return;
}

auto browser_context =
static_cast<BrowserContext*>(devtools_web_contents_->GetBrowserContext());
auto* browser_context = static_cast<BrowserContext*>(
GetDevToolsWebContents()->GetBrowserContext());

net::URLFetcher* fetcher =
(net::URLFetcher::Create(gurl, net::URLFetcher::GET, this)).release();
Expand All @@ -468,7 +488,8 @@ void InspectableWebContentsImpl::LoadNetworkResource(

void InspectableWebContentsImpl::SetIsDocked(const DispatchCallback& callback,
bool docked) {
view_->SetIsDocked(docked);
if (managed_devtools_web_contents_)
view_->SetIsDocked(docked);
if (!callback.is_null())
callback.Run(nullptr);
}
Expand Down Expand Up @@ -640,7 +661,7 @@ void InspectableWebContentsImpl::DispatchProtocolMessage(
if (message.length() < kMaxMessageChunkSize) {
base::string16 javascript = base::UTF8ToUTF16(
"DevToolsAPI.dispatchMessage(" + message + ");");
devtools_web_contents_->GetMainFrame()->ExecuteJavaScript(javascript);
GetDevToolsWebContents()->GetMainFrame()->ExecuteJavaScript(javascript);
return;
}

Expand All @@ -664,13 +685,15 @@ void InspectableWebContentsImpl::RenderFrameHostChanged(
frontend_host_.reset(content::DevToolsFrontendHost::Create(
new_host,
base::Bind(&InspectableWebContentsImpl::HandleMessageFromDevToolsFrontend,
base::Unretained(this))));
weak_factory_.GetWeakPtr())));
}

void InspectableWebContentsImpl::WebContentsDestroyed() {
frontend_loaded_ = false;
external_devtools_web_contents_ = nullptr;
Observe(nullptr);
Detach();
embedder_message_dispatcher_.reset();

for (const auto& pair : pending_requests_)
delete pair.first;
Expand Down Expand Up @@ -758,7 +781,7 @@ void InspectableWebContentsImpl::ReadyToCommitNavigation(
content::NavigationHandle* navigation_handle) {
if (navigation_handle->IsInMainFrame()) {
if (navigation_handle->GetRenderFrameHost() ==
devtools_web_contents_->GetMainFrame() &&
GetDevToolsWebContents()->GetMainFrame() &&
frontend_host_) {
return;
}
Expand Down
9 changes: 8 additions & 1 deletion brightray/browser/inspectable_web_contents_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class InspectableWebContentsImpl :

void SetDelegate(InspectableWebContentsDelegate* delegate) override;
InspectableWebContentsDelegate* GetDelegate() const override;
void SetDevToolsWebContents(content::WebContents* devtools) override;
void SetDockState(const std::string& state) override;
void ShowDevTools() override;
void CloseDevTools() override;
Expand Down Expand Up @@ -192,7 +193,13 @@ class InspectableWebContentsImpl :
PrefService* pref_service_; // weak reference.

std::unique_ptr<content::WebContents> web_contents_;
std::unique_ptr<content::WebContents> devtools_web_contents_;

// The default devtools created by this class when we don't have an external
// one assigned by SetDevToolsWebContents.
std::unique_ptr<content::WebContents> managed_devtools_web_contents_;
// The external devtools assigned by SetDevToolsWebContents.
content::WebContents* external_devtools_web_contents_ = nullptr;

std::unique_ptr<InspectableWebContentsView> view_;

using ExtensionsAPIs = std::map<std::string, std::string>;
Expand Down
65 changes: 65 additions & 0 deletions docs/api/web-contents.md
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,68 @@ win.webContents.on('devtools-opened', () => {

Removes the specified path from DevTools workspace.

#### `contents.setDevToolsWebContents(devToolsWebContents)`

* `devToolsWebContents` WebContents

Uses the `devToolsWebContents` as the target `WebContents` to show devtools.

The `devToolsWebContents` must not have done any navigation, and it should not
be used for other purposes after the call.

By default Electron manages the devtools by creating an internal `WebContents`
with native view, which developers have very limited control of. With the
`setDevToolsWebContents` method, developers can use any `WebContents` to show
the devtools in it, including `BrowserWindow`, `BrowserView` and `<webview>`
tag.

Note that closing the devtools does not destory the `devToolsWebContents`, it
is caller's responsibility to destroy `devToolsWebContents`.

An example of showing devtools in a `<webview>` tag:

```html
<html>
<head>
<style type="text/css">
* { margin: 0; }
#browser { height: 70%; }
#devtools { height: 30%; }
</style>
</head>
<body>
<webview id="browser" src="https://github.com"></webview>
<webview id="devtools"></webview>
<script>
const browserView = document.getElementById('browser')
const devtoolsView = document.getElementById('devtools')
browserView.addEventListener('dom-ready', () => {
const browser = browserView.getWebContents()
browser.setDevToolsWebContents(devtoolsView.getWebContents())
browser.openDevTools()
})
</script>
</body>
</html>
```

An example of showing devtools in a `BrowserWindow`:

```js
const {app, BrowserWindow} = require('electron')

let win = null
let devtools = null

app.once('ready', () => {
win = new BrowserWindow()
devtools = new BrowserWindow()
win.loadURL('https://github.com')
win.webContents.setDevToolsWebContents(devtools.webContents)
win.webContents.openDevTools({mode: 'detach'})
})
```

#### `contents.openDevTools([options])`

* `options` Object (optional)
Expand All @@ -1083,6 +1145,9 @@ Removes the specified path from DevTools workspace.

Opens the devtools.

When `contents` is a `<webview>` tag, the `mode` would be `detach` by default,
explicitly passing an empty `mode` can force using last used dock state.

#### `contents.closeDevTools()`

Closes the devtools.
Expand Down
Loading

0 comments on commit d598aa1

Please sign in to comment.