Repository: electron/electron (language: cpp)

Issue:
[Feature Request]: Add a parameter to openDevTools() that sets the DevTools window title bar
### Preflight Checklist

- [X] I have read the [Contributing Guidelines](https://github.com/electron/electron/blob/main/CONTRIBUTING.md) for this project.
- [X] I agree to follow the [Code of Conduct](https://github.com/electron/electron/blob/main/CODE_OF_CONDUCT.md) that this project adheres to.
- [X] I have searched the [issue tracker](https://www.github.com/electron/electron/issues) for a feature request that matches the one I want to file, without success.

### Problem Description

When the DevTools window needs to be displayed, the following code is usually used:
```
   mainWindow.webContents.openDevTools({mode:'detach',activate:false})
```
However, if you need to set the title bar of the Devtools window, you need to call extra code to achieve it. If you can add a `title` parameter to set the title bar in 111, it can be more convenient.as https://github.com/electron/electron/issues/32545#issuecomment-1322063031
```
   mainWindow.webContents.openDevTools({mode:'detach',activate:false,title:'testTitle'})
```


### Proposed Solution

Support `title`in `openDevTools()`

### Alternatives Considered

None

### Additional Information

I would be more than willing to work for this,if the proposal is reasonable.


Discussion / hints:

Fix patch:
diff --git a/docs/api/web-contents.md b/docs/api/web-contents.md
index 0b4cdfe4a0cd3..fc2867d7a4bb3 100644
--- a/docs/api/web-contents.md
+++ b/docs/api/web-contents.md
@@ -1777,6 +1777,7 @@ app.whenReady().then(() => {
     In `undocked` mode it's possible to dock back. In `detach` mode it's not.
   * `activate` boolean (optional) - Whether to bring the opened devtools window
     to the foreground. The default is `true`.
+  * `title` string (optional) - A title for the DevTools window (only in `undocked` or `detach` mode).
 
 Opens the devtools.
 
@@ -1797,6 +1798,18 @@ Returns `boolean` - Whether the devtools is opened.
 
 Returns `boolean` - Whether the devtools view is focused .
 
+#### `contents.getDevToolsTitle()`
+
+Returns `string` - the current title of the DevTools window. This will only be visible
+if DevTools is opened in `undocked` or `detach` mode.
+
+#### `contents.setDevToolsTitle(title)`
+
+* `title` string
+
+Changes the title of the DevTools window to `title`. This will only be visible if DevTools is
+opened in `undocked` or `detach` mode.
+
 #### `contents.toggleDevTools()`
 
 Toggles the developer tools.
diff --git a/shell/browser/api/electron_api_web_contents.cc b/shell/browser/api/electron_api_web_contents.cc
index f1f21d35e66c1..5eace4ff21f60 100644
--- a/shell/browser/api/electron_api_web_contents.cc
+++ b/shell/browser/api/electron_api_web_contents.cc
@@ -2649,16 +2649,19 @@ void WebContents::OpenDevTools(gin::Arguments* args) {
 #endif
 
   bool activate = true;
+  std::string title;
   if (args && args->Length() == 1) {
     gin_helper::Dictionary options;
     if (args->GetNext(&options)) {
       options.Get("mode", &state);
       options.Get("activate", &activate);
+      options.Get("title", &title);
     }
   }
 
   DCHECK(inspectable_web_contents_);
   inspectable_web_contents_->SetDockState(state);
+  inspectable_web_contents_->SetDevToolsTitle(base::UTF8ToUTF16(title));
   inspectable_web_contents_->ShowDevTools(activate);
 }
 
@@ -2678,6 +2681,18 @@ bool WebContents::IsDevToolsOpened() {
   return inspectable_web_contents_->IsDevToolsViewShowing();
 }
 
+std::u16string WebContents::GetDevToolsTitle() {
+  if (type_ == Type::kRemote)
+    return std::u16string();
+
+  DCHECK(inspectable_web_contents_);
+  return inspectable_web_contents_->GetDevToolsTitle();
+}
+
+void WebContents::SetDevToolsTitle(const std::u16string& title) {
+  inspectable_web_contents_->SetDevToolsTitle(title);
+}
+
 bool WebContents::IsDevToolsFocused() {
   if (type_ == Type::kRemote)
     return false;
@@ -4214,6 +4229,8 @@ void WebContents::FillObjectTemplate(v8::Isolate* isolate,
       .SetMethod("closeDevTools", &WebContents::CloseDevTools)
       .SetMethod("isDevToolsOpened", &WebContents::IsDevToolsOpened)
       .SetMethod("isDevToolsFocused", &WebContents::IsDevToolsFocused)
+      .SetMethod("getDevToolsTitle", &WebContents::GetDevToolsTitle)
+      .SetMethod("setDevToolsTitle", &WebContents::SetDevToolsTitle)
       .SetMethod("enableDeviceEmulation", &WebContents::EnableDeviceEmulation)
       .SetMethod("disableDeviceEmulation", &WebContents::DisableDeviceEmulation)
       .SetMethod("toggleDevTools", &WebContents::ToggleDevTools)
diff --git a/shell/browser/api/electron_api_web_contents.h b/shell/browser/api/electron_api_web_contents.h
index 9273cdf95ad6f..50e552344ed3f 100644
--- a/shell/browser/api/electron_api_web_contents.h
+++ b/shell/browser/api/electron_api_web_contents.h
@@ -200,6 +200,8 @@ class WebContents : public ExclusiveAccessContext,
   void CloseDevTools();
   bool IsDevToolsOpened();
   bool IsDevToolsFocused();
+  std::u16string GetDevToolsTitle();
+  void SetDevToolsTitle(const std::u16string& title);
   void ToggleDevTools();
   void EnableDeviceEmulation(const blink::DeviceEmulationParams& params);
   void DisableDeviceEmulation();
diff --git a/shell/browser/ui/cocoa/electron_inspectable_web_contents_view.h b/shell/browser/ui/cocoa/electron_inspectable_web_contents_view.h
index 5d9d3b1b2ce9d..9492feeab0869 100644
--- a/shell/browser/ui/cocoa/electron_inspectable_web_contents_view.h
+++ b/shell/browser/ui/cocoa/electron_inspectable_web_contents_view.h
@@ -50,6 +50,7 @@ using electron::InspectableWebContentsViewMac;
 - (void)setContentsResizingStrategy:
     (const DevToolsContentsResizingStrategy&)strategy;
 - (void)setTitle:(NSString*)title;
+- (NSString*)getTitle;
 
 - (void)redispatchContextMenuEvent:(base::apple::OwnedNSEvent)theEvent;
 
diff --git a/shell/browser/ui/cocoa/electron_inspectable_web_contents_view.mm b/shell/browser/ui/cocoa/electron_inspectable_web_contents_view.mm
index 7e895183ae840..edaa6f1e319f4 100644
--- a/shell/browser/ui/cocoa/electron_inspectable_web_contents_view.mm
+++ b/shell/browser/ui/cocoa/electron_inspectable_web_contents_view.mm
@@ -244,6 +244,10 @@ - (void)setTitle:(NSString*)title {
   [devtools_window_ setTitle:title];
 }
 
+- (NSString*)getTitle {
+  return [devtools_window_ title];
+}
+
 - (void)viewDidBecomeFirstResponder:(NSNotification*)notification {
   auto* inspectable_web_contents =
       inspectableWebContentsView_->inspectable_web_contents();
diff --git a/shell/browser/ui/inspectable_web_contents.cc b/shell/browser/ui/inspectable_web_contents.cc
index a0406e8aeadf4..c718ab3249481 100644
--- a/shell/browser/ui/inspectable_web_contents.cc
+++ b/shell/browser/ui/inspectable_web_contents.cc
@@ -426,6 +426,11 @@ void InspectableWebContents::SetDockState(const std::string& state) {
   }
 }
 
+void InspectableWebContents::SetDevToolsTitle(const std::u16string& title) {
+  devtools_title_ = title;
+  view_->SetTitle(devtools_title_);
+}
+
 void InspectableWebContents::SetDevToolsWebContents(
     content::WebContents* devtools) {
   if (!managed_devtools_web_contents_)
@@ -481,6 +486,10 @@ bool InspectableWebContents::IsDevToolsViewShowing() {
   return managed_devtools_web_contents_ && view_->IsDevToolsViewShowing();
 }
 
+std::u16string InspectableWebContents::GetDevToolsTitle() {
+  return view_->GetTitle();
+}
+
 void InspectableWebContents::AttachTo(
     scoped_refptr<content::DevToolsAgentHost> host) {
   Detach();
@@ -565,6 +574,9 @@ void InspectableWebContents::LoadCompleted() {
   // If the devtools can dock, "SetIsDocked" will be called by devtools itself.
   if (!can_dock_) {
     SetIsDocked(DispatchCallback(), false);
+    if (!devtools_title_.empty()) {
+      view_->SetTitle(devtools_title_);
+    }
   } else {
     if (dock_state_.empty()) {
       const base::Value::Dict& prefs =
@@ -635,9 +647,12 @@ void InspectableWebContents::SetInspectedPageBounds(const gfx::Rect& rect) {
 void InspectableWebContents::InspectElementCompleted() {}
 
 void InspectableWebContents::InspectedURLChanged(const std::string& url) {
-  if (managed_devtools_web_contents_)
-    view_->SetTitle(
-        base::UTF8ToUTF16(base::StringPrintf(kTitleFormat, url.c_str())));
+  if (managed_devtools_web_contents_) {
+    if (devtools_title_.empty()) {
+      view_->SetTitle(
+          base::UTF8ToUTF16(base::StringPrintf(kTitleFormat, url.c_str())));
+    }
+  }
 }
 
 void InspectableWebContents::LoadNetworkResource(DispatchCallback callback,
diff --git a/shell/browser/ui/inspectable_web_contents.h b/shell/browser/ui/inspectable_web_contents.h
index c5815528c5798..8bf0df37082cb 100644
--- a/shell/browser/ui/inspectable_web_contents.h
+++ b/shell/browser/ui/inspectable_web_contents.h
@@ -59,9 +59,11 @@ class InspectableWebContents
   void ReleaseWebContents();
   void SetDevToolsWebContents(content::WebContents* devtools);
   void SetDockState(const std::string& state);
+  void SetDevToolsTitle(const std::u16string& title);
   void ShowDevTools(bool activate);
   void CloseDevTools();
   bool IsDevToolsViewShowing();
+  std::u16string GetDevToolsTitle();
   void AttachTo(scoped_refptr<content::DevToolsAgentHost>);
   void Detach();
   void CallClientFunction(
@@ -207,6 +209,7 @@ class InspectableWebContents
   gfx::Rect devtools_bounds_;
   bool can_dock_ = true;
   std::string dock_state_;
+  std::u16string devtools_title_;
   bool activate_ = true;
 
   raw_ptr<InspectableWebContentsDelegate> delegate_ =
diff --git a/shell/browser/ui/inspectable_web_contents_view.h b/shell/browser/ui/inspectable_web_contents_view.h
index 46cbda331219d..c18b8a1de05b5 100644
--- a/shell/browser/ui/inspectable_web_contents_view.h
+++ b/shell/browser/ui/inspectable_web_contents_view.h
@@ -57,6 +57,7 @@ class InspectableWebContentsView {
   virtual void SetContentsResizingStrategy(
       const DevToolsContentsResizingStrategy& strategy) = 0;
   virtual void SetTitle(const std::u16string& title) = 0;
+  virtual const std::u16string GetTitle() = 0;
 
  protected:
   // Owns us.
diff --git a/shell/browser/ui/inspectable_web_contents_view_mac.h b/shell/browser/ui/inspectable_web_contents_view_mac.h
index 7021097af02b3..46ab871d8e0cb 100644
--- a/shell/browser/ui/inspectable_web_contents_view_mac.h
+++ b/shell/browser/ui/inspectable_web_contents_view_mac.h
@@ -34,6 +34,7 @@ class InspectableWebContentsViewMac : public InspectableWebContentsView {
   void SetContentsResizingStrategy(
       const DevToolsContentsResizingStrategy& strategy) override;
   void SetTitle(const std::u16string& title) override;
+  const std::u16string GetTitle() override;
 
  private:
   ElectronInspectableWebContentsView* __strong view_;
diff --git a/shell/browser/ui/inspectable_web_contents_view_mac.mm b/shell/browser/ui/inspectable_web_contents_view_mac.mm
index 175678f037bfc..3bed31470f636 100644
--- a/shell/browser/ui/inspectable_web_contents_view_mac.mm
+++ b/shell/browser/ui/inspectable_web_contents_view_mac.mm
@@ -60,4 +60,8 @@
   [view_ setTitle:base::SysUTF16ToNSString(title)];
 }
 
+const std::u16string InspectableWebContentsViewMac::GetTitle() {
+  return base::SysNSStringToUTF16([view_ getTitle]);
+}
+
 }  // namespace electron
diff --git a/shell/browser/ui/views/inspectable_web_contents_view_views.cc b/shell/browser/ui/views/inspectable_web_contents_view_views.cc
index b9def2941a150..bb5085c02af82 100644
--- a/shell/browser/ui/views/inspectable_web_contents_view_views.cc
+++ b/shell/browser/ui/views/inspectable_web_contents_view_views.cc
@@ -209,6 +209,10 @@ void InspectableWebContentsViewViews::SetTitle(const std::u16string& title) {
   }
 }
 
+const std::u16string InspectableWebContentsViewViews::GetTitle() {
+  return title_;
+}
+
 void InspectableWebContentsViewViews::Layout() {
   if (!devtools_web_view_->GetVisible()) {
     contents_web_view_->SetBoundsRect(GetContentsBounds());
diff --git a/shell/browser/ui/views/inspectable_web_contents_view_views.h b/shell/browser/ui/views/inspectable_web_contents_view_views.h
index f3c364b196a26..f5b45008e6085 100644
--- a/shell/browser/ui/views/inspectable_web_contents_view_views.h
+++ b/shell/browser/ui/views/inspectable_web_contents_view_views.h
@@ -39,12 +39,11 @@ class InspectableWebContentsViewViews : public InspectableWebContentsView,
   void SetContentsResizingStrategy(
       const DevToolsContentsResizingStrategy& strategy) override;
   void SetTitle(const std::u16string& title) override;
+  const std::u16string GetTitle() override;
 
   // views::View:
   void Layout() override;
 
-  const std::u16string& GetTitle() const { return title_; }
-
  private:
   std::unique_ptr<views::Widget> devtools_window_;
   raw_ptr<views::WebView> devtools_window_web_view_ = nullptr;
diff --git a/spec/api-web-contents-spec.ts b/spec/api-web-contents-spec.ts
index aeb2a11349882..6e451ce854235 100644
--- a/spec/api-web-contents-spec.ts
+++ b/spec/api-web-contents-spec.ts
@@ -608,6 +608,27 @@ describe('webContents module', () => {
       await devtoolsOpened;
       expect(w.webContents.isDevToolsOpened()).to.be.true();
     });
+
+    it('can show a DevTools window with custom title', async () => {
+      const w = new BrowserWindow({ show: false });
+      const devtoolsOpened = once(w.webContents, 'devtools-opened');
+      w.webContents.openDevTools({ mode: 'detach', activate: false, title: 'myTitle' });
+      await devtoolsOpened;
+      expect(w.webContents.getDevToolsTitle()).to.equal('myTitle');
+    });
+  });
+
+  describe('setDevToolsTitle() API', () => {
+    afterEach(closeAllWindows);
+    it('can set devtools title with function', async () => {
+      const w = new BrowserWindow({ show: false });
+      const devtoolsOpened = once(w.webContents, 'devtools-opened');
+      w.webContents.openDevTools({ mode: 'detach', activate: false });
+      await devtoolsOpened;
+      expect(w.webContents.isDevToolsOpened()).to.be.true();
+      w.webContents.setDevToolsTitle('newTitle');
+      expect(w.webContents.getDevToolsTitle()).to.equal('newTitle');
+    });
   });
 
   describe('before-input-event event', () => {


Test patch: