| // Copyright 2026 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/devtools/views/devtools_floaty.h" |
| |
| #include "base/containers/span.h" |
| #include "base/json/json_reader.h" |
| #include "base/scoped_observation.h" |
| #include "base/strings/stringprintf.h" |
| #include "chrome/browser/devtools/devtools_ui_bindings.h" |
| #include "chrome/browser/devtools/devtools_window.h" |
| #include "chrome/browser/devtools/features.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/profiles/profile_observer.h" |
| #include "content/public/browser/context_menu_params.h" |
| #include "content/public/browser/devtools_agent_host.h" |
| #include "content/public/browser/devtools_manager_delegate.h" |
| #include "content/public/browser/navigation_handle.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/render_view_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "ui/base/ui_base_types.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/views/controls/webview/webview.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/dialog_delegate.h" |
| |
| namespace { |
| |
| // A WebContentsDelegate for the DevTools GreenDev Floaty dialog. |
| class DevToolsFloatyWebContentsDelegate : public content::WebContentsDelegate { |
| public: |
| DevToolsFloatyWebContentsDelegate() = default; |
| ~DevToolsFloatyWebContentsDelegate() override = default; |
| |
| // content::WebContentsDelegate: |
| void CloseContents(content::WebContents* source) override {} |
| |
| bool HandleContextMenu(content::RenderFrameHost& render_frame_host, |
| const content::ContextMenuParams& params) override { |
| DevToolsWindow::InspectElement(content::RenderFrameHost::FromID( |
| render_frame_host.GetProcess()->GetID(), |
| render_frame_host.GetRoutingID()), |
| params.x, params.y); |
| return true; |
| } |
| }; |
| |
| class DevToolsFloatyDialogDelegate; |
| std::map<int, DevToolsFloatyDialogDelegate*>& GetFloatyRegistry() { |
| static base::NoDestructor<std::map<int, DevToolsFloatyDialogDelegate*>> |
| registry; |
| return *registry; |
| } |
| |
| // A simple delegate to handle requests for opening the DevTools panel for the |
| // DevToolsUIBindings object. |
| class FloatyBindingsDelegate : public DevToolsUIBindings::Delegate { |
| public: |
| FloatyBindingsDelegate(views::Widget* widget, int process_id, int routing_id) |
| : widget_(widget), process_id_(process_id), routing_id_(routing_id) {} |
| ~FloatyBindingsDelegate() override = default; |
| |
| void OpenInNewTab(const std::string& url) override { |
| content::RenderFrameHost* rfh = |
| content::RenderFrameHost::FromID(process_id_, routing_id_); |
| if (!rfh) { |
| return; |
| } |
| content::WebContents* inspected_web_contents = |
| content::WebContents::FromRenderFrameHost(rfh); |
| |
| if (url == "magic:open-devtools") { |
| Profile* profile = Profile::FromBrowserContext( |
| inspected_web_contents->GetBrowserContext()); |
| content::DevToolsManagerDelegate::DevToolsOptions options("greendev"); |
| DevToolsWindow::OpenDevToolsWindow( |
| inspected_web_contents, profile, |
| DevToolsOpenedByAction::kContextMenuInspect, options); |
| } else { |
| // Open regular URLs in a new tab. |
| content::OpenURLParams params(GURL(url), content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, |
| ui::PAGE_TRANSITION_LINK, false); |
| // We use the inspected contents to open the URL, ensuring it opens in the |
| // correct browser window/context. |
| inspected_web_contents->OpenURL(params, base::DoNothing()); |
| } |
| } |
| |
| void ActivateWindow() override { |
| if (widget_) { |
| widget_->Restore(); |
| widget_->Show(); |
| } |
| } |
| |
| content::WebContents* GetInspectedWebContents() override { return nullptr; } |
| void CloseWindow() override {} |
| void Inspect(scoped_refptr<content::DevToolsAgentHost> host) override {} |
| void SetInspectedPageBounds(const gfx::Rect& rect) override {} |
| void InspectElementCompleted() override {} |
| void SetIsDocked(bool is_docked) override {} |
| void OpenSearchResultsInNewTab(const std::string& query) override {} |
| void SetWhitelistedShortcuts(const std::string& message) override {} |
| void SetEyeDropperActive(bool active) override {} |
| void OpenNodeFrontend() override {} |
| void InspectedContentsClosing() override {} |
| void OnLoadCompleted() override {} |
| void ReadyForTest() override {} |
| void ConnectionReady() override {} |
| void SetOpenNewWindowForPopups(bool value) override {} |
| infobars::ContentInfoBarManager* GetInfoBarManager() override { |
| return nullptr; |
| } |
| void RenderProcessGone(bool crashed) override {} |
| void ShowCertificateViewer(const std::string& cert_chain) override {} |
| int GetDockStateForLogging() override { return 0; } |
| int GetOpenedByForLogging() override { return 0; } |
| int GetClosedByForLogging() override { return 0; } |
| |
| private: |
| raw_ptr<views::Widget> widget_; |
| int process_id_; |
| int routing_id_; |
| }; |
| |
| // The DialogDelegate that handles showing the GreenDev Floaty window, by |
| // creating a WebView and loading the devtools:// entrypoint within. Reacts to |
| // external state changes and handles messaging and cleanup for the delegate. |
| class DevToolsFloatyDialogDelegate : public views::DialogDelegate, |
| public ProfileObserver, |
| public content::WebContentsObserver, |
| public views::WidgetObserver, |
| public content::DevToolsAgentHostClient { |
| public: |
| explicit DevToolsFloatyDialogDelegate( |
| Profile* profile, |
| std::unique_ptr<content::WebContents> web_contents, |
| std::unique_ptr<DevToolsFloatyWebContentsDelegate> web_contents_delegate, |
| int process_id, |
| int routing_id, |
| int backend_node_id) |
| : WebContentsObserver(web_contents.get()), |
| web_contents_(std::move(web_contents)), |
| web_contents_delegate_(std::move(web_contents_delegate)), |
| target_process_id_(process_id), |
| target_routing_id_(routing_id), |
| backend_node_id_(backend_node_id) { |
| profile_observation_.Observe(profile); |
| SetModalType(ui::mojom::ModalType::kNone); |
| SetTitle(u"DevTools Floaty"); |
| SetButtons(0); |
| |
| auto webview = std::make_unique<views::WebView>(profile); |
| webview->SetWebContents(web_contents_.get()); |
| web_contents_->SetDelegate(web_contents_delegate_.get()); |
| SetContentsView(std::move(webview)); |
| |
| if (backend_node_id_) { |
| GetFloatyRegistry()[backend_node_id_] = this; |
| } |
| } |
| |
| ~DevToolsFloatyDialogDelegate() override { |
| if (backend_node_id_) { |
| GetFloatyRegistry().erase(backend_node_id_); |
| } |
| |
| if (GetWidget()) { |
| GetWidget()->RemoveObserver(this); |
| } |
| } |
| |
| // content::DevToolsAgentHostClient: |
| void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host, |
| base::span<const uint8_t> message) override { |
| std::string_view message_sp(reinterpret_cast<const char*>(message.data()), |
| message.size()); |
| |
| std::optional<base::Value> value = |
| base::JSONReader::Read(message_sp, base::JSON_PARSE_RFC); |
| if (!value || !value->is_dict()) { |
| return; |
| } |
| |
| const base::DictValue& dict = value->GetDict(); |
| const std::string* method = dict.FindString("method"); |
| |
| if (method && *method == "Overlay.inspectedElementWindowRestored") { |
| const base::DictValue* params = dict.FindDict("params"); |
| if (params) { |
| std::optional<int> backend_node_id = params->FindInt("backendNodeId"); |
| if (backend_node_id && *backend_node_id == backend_node_id_) { |
| DevToolsFloaty::Restore(*backend_node_id); |
| } |
| } |
| } else if (method && *method == "Overlay.inspectPanelShowRequested") { |
| content::RenderFrameHost* rfh = content::RenderFrameHost::FromID( |
| target_process_id_, target_routing_id_); |
| if (rfh) { |
| content::WebContents* inspected_web_contents = |
| content::WebContents::FromRenderFrameHost(rfh); |
| Profile* profile = Profile::FromBrowserContext( |
| inspected_web_contents->GetBrowserContext()); |
| content::DevToolsManagerDelegate::DevToolsOptions options("greendev"); |
| DevToolsWindow::OpenDevToolsWindow( |
| inspected_web_contents, profile, |
| DevToolsOpenedByAction::kContextMenuInspect, options); |
| } |
| } |
| } |
| |
| void AgentHostClosed(content::DevToolsAgentHost* agent_host) override { |
| agent_host_ = nullptr; |
| } |
| |
| // views::WidgetObserver: |
| void OnWidgetDestroying(views::Widget* widget) override { |
| if (agent_host_ && backend_node_id_) { |
| content::RenderFrameHost* rfh = content::RenderFrameHost::FromID( |
| target_process_id_, target_routing_id_); |
| if (rfh) { |
| content::WebContents* inspected_contents = |
| content::WebContents::FromRenderFrameHost(rfh); |
| DevToolsWindow* window = |
| DevToolsWindow::GetInstanceForInspectedWebContents( |
| inspected_contents); |
| if (window) { |
| content::WebContents* devtools_contents = |
| window->GetDevToolsWebContents(); |
| if (devtools_contents) { |
| DevToolsUIBindings* bindings = |
| DevToolsUIBindings::ForWebContents(devtools_contents); |
| if (bindings) { |
| bindings->CallClientMethod("GreenDevPanel", "closeSession", |
| base::Value(backend_node_id_)); |
| } |
| } |
| } |
| } |
| } |
| // Explicitly destroy WebContents to ensure bindings are detached and |
| // session is closed. |
| web_contents_.reset(); |
| if (agent_host_) { |
| agent_host_->DetachClient(this); |
| } |
| agent_host_ = nullptr; |
| } |
| |
| // content::WebContentsObserver: |
| void ReadyToCommitNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| content::RenderFrameHost* rfh = content::RenderFrameHost::FromID( |
| target_process_id_, target_routing_id_); |
| if (rfh) { |
| DevToolsUIBindings* bindings = |
| DevToolsUIBindings::ForWebContents(web_contents_.get()); |
| if (bindings) { |
| bindings->SetDelegate(new FloatyBindingsDelegate( |
| GetWidget(), target_process_id_, target_routing_id_)); |
| agent_host_ = content::DevToolsAgentHost::GetOrCreateFor( |
| content::WebContents::FromRenderFrameHost(rfh)); |
| bindings->AttachTo(agent_host_); |
| agent_host_->AttachClient(this); |
| const char* enable_overlay_message = |
| "{\"id\":102,\"method\":\"Overlay.enable\"}"; |
| agent_host_->DispatchProtocolMessage( |
| this, base::as_byte_span(std::string(enable_overlay_message))); |
| |
| const std::string message = base::StringPrintf( |
| "{\"id\":1003,\"method\":\"Overlay.setShowInspectedElementAnchor\"," |
| "\"params\":{\"InspectedElementAnchorConfig\":{\"backendNodeId\":%" |
| "d}" |
| "}}", |
| backend_node_id_); |
| agent_host_->DispatchProtocolMessage(this, base::as_byte_span(message)); |
| } |
| } |
| } |
| |
| // ProfileObserver: |
| void OnProfileWillBeDestroyed(Profile* profile) override { |
| profile_observation_.Reset(); |
| |
| views::Widget* widget = GetWidget(); |
| if (!widget) { |
| web_contents_.reset(); |
| return; |
| } |
| |
| auto* web_view = static_cast<views::WebView*>(GetContentsView()); |
| if (web_view) { |
| web_view->SetWebContents(nullptr); |
| } |
| |
| web_contents_.reset(); |
| |
| widget->CloseNow(); |
| } |
| |
| private: |
| std::unique_ptr<content::WebContents> web_contents_; |
| std::unique_ptr<DevToolsFloatyWebContentsDelegate> web_contents_delegate_; |
| base::ScopedObservation<Profile, ProfileObserver> profile_observation_{this}; |
| scoped_refptr<content::DevToolsAgentHost> agent_host_; |
| int target_process_id_; |
| int target_routing_id_; |
| int backend_node_id_; |
| }; |
| |
| void ShowWindow(Profile* profile, |
| int process_id, |
| int routing_id, |
| gfx::Point position, |
| int backend_node_id) { |
| CHECK(base::FeatureList::IsEnabled(features::kDevToolsGreenDevUi)); |
| |
| auto web_contents = |
| content::WebContents::Create(content::WebContents::CreateParams(profile)); |
| content::WebContents* raw_web_contents = web_contents.get(); |
| auto delegate_ptr = std::make_unique<DevToolsFloatyDialogDelegate>( |
| profile, std::move(web_contents), |
| std::make_unique<DevToolsFloatyWebContentsDelegate>(), process_id, |
| routing_id, backend_node_id); |
| DevToolsFloatyDialogDelegate* delegate = delegate_ptr.get(); |
| |
| GURL url = GURL(base::StringPrintf( |
| "devtools://devtools/bundled/entrypoints/greendev_floaty/" |
| "floaty.html#processId=%d&routingId=%d&x=%d&y=%d&backendNodeId=%d", |
| process_id, routing_id, position.x(), position.y(), backend_node_id)); |
| raw_web_contents->GetController().LoadURL(url, content::Referrer(), |
| ui::PAGE_TRANSITION_AUTO_TOPLEVEL, |
| std::string()); |
| |
| content::RenderFrameHost* rfh = |
| content::RenderFrameHost::FromID(process_id, routing_id); |
| if (!rfh) { |
| return; |
| } |
| content::WebContents* inspected_contents = |
| content::WebContents::FromRenderFrameHost(rfh); |
| if (!inspected_contents) { |
| return; |
| } |
| |
| views::Widget* widget = views::DialogDelegate::CreateDialogWidget( |
| std::move(delegate_ptr), inspected_contents->GetTopLevelNativeWindow(), |
| gfx::NativeView()); |
| if (!widget) { |
| return; |
| } |
| widget->AddObserver(delegate); |
| // TODO(https://crbug.com/482553156): Remove these. |
| constexpr int offsetX = 30; |
| constexpr int offsetY = 175; |
| widget->SetBounds( |
| gfx::Rect(position.x() + offsetX, position.y() + offsetY, 400, 400)); |
| widget->Show(); |
| } |
| |
| // An AgentHostClient for the DevTools Floaty object, responsible for |
| // dispatching overlay protocol messages. |
| class InspectElementGeminiClient : public content::DevToolsAgentHostClient { |
| public: |
| InspectElementGeminiClient(content::BrowserContext* browser_context, |
| int render_process_id, |
| int render_frame_id, |
| int x, |
| int y) |
| : browser_context_(browser_context), |
| render_process_id_(render_process_id), |
| render_frame_id_(render_frame_id), |
| x_(x), |
| y_(y) {} |
| ~InspectElementGeminiClient() override = default; |
| |
| void DispatchProtocolMessage(content::DevToolsAgentHost* agent_host, |
| base::span<const uint8_t> message) override { |
| std::string_view message_sp(reinterpret_cast<const char*>(message.data()), |
| message.size()); |
| |
| std::optional<base::Value> value = |
| base::JSONReader::Read(message_sp, base::JSON_PARSE_RFC); |
| if (!value || !value->is_dict()) { |
| return; |
| } |
| |
| const base::DictValue& dict = value->GetDict(); |
| |
| // Handle response to DOM.getNodeForLocation (id: 103) |
| // This is no longer used but kept if we switch back to manual calls. |
| // ... |
| |
| const std::string* method = dict.FindString("method"); |
| if (method && *method == "Overlay.inspectNodeRequested") { |
| const base::DictValue* params = dict.FindDict("params"); |
| if (params) { |
| std::optional<int> backend_node_id = params->FindInt("backendNodeId"); |
| if (backend_node_id) { |
| if (!triggered_) { |
| triggered_ = true; |
| ShowWindow(Profile::FromBrowserContext(browser_context_), |
| render_process_id_, render_frame_id_, gfx::Point(x_, y_), |
| *backend_node_id); |
| keep_alive_host_ = agent_host; |
| } else { |
| DevToolsFloaty::Restore(*backend_node_id); |
| } |
| } |
| } |
| } else if (method && *method == "Overlay.inspectPanelShowRequested") { |
| content::RenderFrameHost* rfh = content::RenderFrameHost::FromID( |
| render_process_id_, render_frame_id_); |
| if (rfh) { |
| content::WebContents* inspected_web_contents = |
| content::WebContents::FromRenderFrameHost(rfh); |
| Profile* profile = Profile::FromBrowserContext( |
| inspected_web_contents->GetBrowserContext()); |
| content::DevToolsManagerDelegate::DevToolsOptions options("greendev"); |
| DevToolsWindow::OpenDevToolsWindow( |
| inspected_web_contents, profile, |
| DevToolsOpenedByAction::kContextMenuInspect, options); |
| } |
| } else { |
| } |
| } |
| |
| void AgentHostClosed(content::DevToolsAgentHost* agent_host) override { |
| delete this; |
| } |
| |
| private: |
| raw_ptr<content::BrowserContext> browser_context_; |
| int render_process_id_; |
| int render_frame_id_; |
| int x_; |
| int y_; |
| bool triggered_ = false; |
| scoped_refptr<content::DevToolsAgentHost> keep_alive_host_; |
| }; |
| |
| } // namespace |
| |
| namespace DevToolsFloaty { |
| |
| void Show(Profile* profile, |
| int process_id, |
| int routing_id, |
| gfx::Point position, |
| int backend_node_id) { |
| content::RenderFrameHost* rfh = |
| content::RenderFrameHost::FromID(process_id, routing_id); |
| if (!rfh) { |
| return; |
| } |
| |
| scoped_refptr<content::DevToolsAgentHost> agent( |
| content::DevToolsAgentHost::GetOrCreateFor( |
| content::WebContents::FromRenderFrameHost(rfh))); |
| InspectElementGeminiClient* client = new InspectElementGeminiClient( |
| rfh->GetBrowserContext(), rfh->GetProcess()->GetDeprecatedID(), |
| rfh->GetRoutingID(), position.x(), position.y()); |
| agent->AttachClient(client); |
| const char* enable_dom_message = "{\"id\":100,\"method\":\"DOM.enable\"}"; |
| agent->DispatchProtocolMessage( |
| client, base::as_byte_span(std::string(enable_dom_message))); |
| const char* enable_overlay_message = |
| "{\"id\":101,\"method\":\"Overlay.enable\"}"; |
| agent->DispatchProtocolMessage( |
| client, base::as_byte_span(std::string(enable_overlay_message))); |
| agent->InspectElement(rfh, position.x(), position.y()); |
| } |
| |
| void Restore(int backend_node_id) { |
| CHECK(base::FeatureList::IsEnabled(features::kDevToolsGreenDevUi)); |
| CHECK(backend_node_id > 0); |
| |
| auto it = GetFloatyRegistry().find(backend_node_id); |
| if (it == GetFloatyRegistry().end()) { |
| return; |
| } |
| |
| views::Widget* widget = it->second->GetWidget(); |
| if (widget) { |
| widget->Restore(); |
| widget->Show(); |
| } |
| } |
| |
| } // namespace DevToolsFloaty |