diff --git a/app/brave_command_ids.h b/app/brave_command_ids.h index a8d6c73a92b3..e585bb2bfad3 100644 --- a/app/brave_command_ids.h +++ b/app/brave_command_ids.h @@ -126,6 +126,12 @@ #define IDC_WINDOW_BRING_ALL_TABS 56320 +// Screenshot commands +#define IDC_BRAVE_SCREENSHOT_TOOLS 56321 +#define IDC_BRAVE_SCREENSHOTS_START_SELECTION_TO_CLIPBOARD 56322 +#define IDC_BRAVE_SCREENSHOTS_START_VIEWPORT_TO_CLIPBOARD 56323 +#define IDC_BRAVE_SCREENSHOTS_START_FULLPAGE_TO_CLIPBOARD 56324 + // Commands related to split view #define IDC_NEW_SPLIT_VIEW 56325 #define IDC_TILE_TABS 56326 diff --git a/app/brave_generated_resources.grd b/app/brave_generated_resources.grd index ac2d7bdc94a4..5ce1d1541e67 100644 --- a/app/brave_generated_resources.grd +++ b/app/brave_generated_resources.grd @@ -161,6 +161,8 @@ + + @@ -346,7 +348,7 @@ Or change later at $2brave://settings/ext Check details - + Block elements diff --git a/app/brave_screenshots_strings.grdp b/app/brave_screenshots_strings.grdp new file mode 100644 index 000000000000..3c36cb268ada --- /dev/null +++ b/app/brave_screenshots_strings.grdp @@ -0,0 +1,31 @@ + + + + + Screenshot Tools + + + Screenshot Selection to Clipboard + + + Screenshot Viewport to Clipboard + + + Screenshot Full Page to Clipboard + + + + + Screenshot tools + + + Screenshot selection to clipboard + + + Screenshot viewport to clipboard + + + Screenshot full page to clipboard + + + diff --git a/browser/about_flags.cc b/browser/about_flags.cc index ec5befc05f5a..38297dcb34fb 100644 --- a/browser/about_flags.cc +++ b/browser/about_flags.cc @@ -10,6 +10,7 @@ #include "base/strings/string_util.h" #include "brave/browser/brave_browser_features.h" #include "brave/browser/brave_features_internal_names.h" +#include "brave/browser/brave_screenshots/features.h" #include "brave/browser/ethereum_remote_client/buildflags/buildflags.h" #include "brave/browser/ethereum_remote_client/features.h" #include "brave/browser/ui/brave_ui_features.h" @@ -501,6 +502,13 @@ kOsDesktop, \ FEATURE_VALUE_TYPE(features::kBraveNtpSearchWidget), \ }, \ + { \ + "brave-screenshots", \ + "Screenshot Options", \ + "Enables screenshot support via browser commands and context menu.", \ + kOsDesktop, \ + FEATURE_VALUE_TYPE(brave_screenshots::features::kBraveScreenshots), \ + }, \ { \ "brave-adblock-cname-uncloaking", \ "Enable CNAME uncloaking", \ diff --git a/browser/brave_screenshots/BUILD.gn b/browser/brave_screenshots/BUILD.gn new file mode 100644 index 000000000000..1b828c368a17 --- /dev/null +++ b/browser/brave_screenshots/BUILD.gn @@ -0,0 +1,74 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +import("//brave/browser/brave_screenshots/buildflags/buildflags.gni") + +assert(enable_brave_screenshots) + +source_set("utils") { + sources = [ + "screenshots_utils.cc", + "screenshots_utils.h", + ] + + deps = [ + "//chrome/browser/image_editor:image_editor", + "//chrome/browser/ui:ui", + "//chrome/browser/ui/browser_window:browser_window", + "//ui/base/clipboard:clipboard", + ] +} + +source_set("strategies") { + sources = [ + "strategies/fullpage_strategy.cc", + "strategies/fullpage_strategy.h", + "strategies/screenshot_strategy.h", + "strategies/selection_strategy.cc", + "strategies/selection_strategy.h", + "strategies/viewport_strategy.cc", + "strategies/viewport_strategy.h", + ] + + deps = [ + "//chrome/browser/image_editor:image_editor", + "//chrome/browser/ui:ui", + ] +} + +source_set("tabs") { + sources = [ + "screenshots_tab_feature.cc", + "screenshots_tab_feature.h", + ] + + deps = [ + ":strategies", + ":utils", + "//chrome/browser/image_editor:image_editor", + "//chrome/browser/ui:ui", + ] +} + +source_set("features") { + sources = [ + "features.cc", + "features.h", + ] + + deps = [ "//base:base" ] +} + +source_set("unit_tests") { + testonly = true + sources = [ "test/brave_screenshots_unittests.cc" ] + deps = [ + ":features", + "//base:base", + "//brave/app:command_ids", + "//chrome/test:test_support", + "//testing/gtest:gtest", + ] +} diff --git a/browser/brave_screenshots/buildflags/buildflags.gni b/browser/brave_screenshots/buildflags/buildflags.gni new file mode 100644 index 000000000000..83bd9c402375 --- /dev/null +++ b/browser/brave_screenshots/buildflags/buildflags.gni @@ -0,0 +1,8 @@ +# Copyright (c) 2025 The Brave Authors. All rights reserved. +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at https://mozilla.org/MPL/2.0/. + +declare_args() { + enable_brave_screenshots = is_win || is_mac || is_linux +} diff --git a/browser/brave_screenshots/features.cc b/browser/brave_screenshots/features.cc new file mode 100644 index 000000000000..bbfeb1c988fb --- /dev/null +++ b/browser/brave_screenshots/features.cc @@ -0,0 +1,17 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/brave_screenshots/features.h" + +namespace brave_screenshots::features { + +BASE_FEATURE(kBraveScreenshots, + "BraveScreenshots", + base::FEATURE_ENABLED_BY_DEFAULT); + +bool IsBraveScreenshotsEnabled() { + return base::FeatureList::IsEnabled(kBraveScreenshots); +} +} // namespace brave_screenshots::features diff --git a/browser/brave_screenshots/features.h b/browser/brave_screenshots/features.h new file mode 100644 index 000000000000..785ae38105eb --- /dev/null +++ b/browser/brave_screenshots/features.h @@ -0,0 +1,17 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_BRAVE_SCREENSHOTS_FEATURES_H_ +#define BRAVE_BROWSER_BRAVE_SCREENSHOTS_FEATURES_H_ + +#include "base/feature_list.h" + +namespace brave_screenshots::features { +BASE_DECLARE_FEATURE(kBraveScreenshots); + +bool IsBraveScreenshotsEnabled(); +} // namespace brave_screenshots::features + +#endif // BRAVE_BROWSER_BRAVE_SCREENSHOTS_FEATURES_H_ diff --git a/browser/brave_screenshots/screenshots_tab_feature.cc b/browser/brave_screenshots/screenshots_tab_feature.cc new file mode 100644 index 000000000000..6d6443d94921 --- /dev/null +++ b/browser/brave_screenshots/screenshots_tab_feature.cc @@ -0,0 +1,96 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/brave_screenshots/screenshots_tab_feature.h" + +#include "base/notreached.h" +#include "brave/browser/brave_screenshots/screenshots_utils.h" +#include "brave/browser/brave_screenshots/strategies/fullpage_strategy.h" +#include "brave/browser/brave_screenshots/strategies/selection_strategy.h" +#include "brave/browser/brave_screenshots/strategies/viewport_strategy.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "chrome/browser/ui/browser.h" +#include "content/public/browser/web_contents.h" + +namespace { + +// Some screenshots may need to be clipped to avoid the GPU limit. +// See https://crbug.com/1260828. When this happens, we may wish to notify the +// user that only a portion of their page could be captured. +void DisplayScreenshotClippedNotification(base::WeakPtr browser) { + // Issue: https://github.com/brave/brave-browser/issues/43369 + NOTIMPLEMENTED(); +} + +} // namespace +namespace brave_screenshots { + +BraveScreenshotsTabFeature::BraveScreenshotsTabFeature() { + DVLOG(1) << "BraveScreenshotsTabFeature created"; +} + +BraveScreenshotsTabFeature::~BraveScreenshotsTabFeature() { + DVLOG(1) << "BraveScreenshotsTabFeature destroyed"; +} + +void BraveScreenshotsTabFeature::StartScreenshot(Browser* browser, + ScreenshotType type) { + DVLOG(1) << "Called StartScreenshot"; + CHECK(browser); + + browser_ = browser->AsWeakPtr(); + web_contents_ = + browser_->tab_strip_model()->GetActiveWebContents()->GetWeakPtr(); + + // We've determined the appropriate strategy to use + strategy_ = CreateStrategy(type); + + DVLOG(2) << "Starting capture"; + + strategy_->Capture( + web_contents_.get(), + base::BindOnce(&BraveScreenshotsTabFeature::OnCaptureComplete, + weak_factory_.GetWeakPtr())); +} + +std::unique_ptr +BraveScreenshotsTabFeature::CreateStrategy(ScreenshotType type) { + switch (type) { + case ScreenshotType::kFullPage: + DVLOG(3) << "Creating FullPageStrategy"; + return std::make_unique(); + case ScreenshotType::kSelection: + // Based on image_editor::ScreenshotFlow, which requires a WebContents + DVLOG(3) << "Creating SelectionStrategy"; + return std::make_unique(web_contents_.get()); + case ScreenshotType::kViewport: + // Based on image_editor::ScreenshotFlow, which requires a WebContents + DVLOG(3) << "Creating ViewportStrategy"; + return std::make_unique(web_contents_.get()); + default: + NOTREACHED(); + } +} + +void BraveScreenshotsTabFeature::OnCaptureComplete( + const image_editor::ScreenshotCaptureResult& result) { + DVLOG(2) << __func__; + + if (result.image.IsEmpty()) { + DVLOG(2) << "Screenshot capture failed"; + return; + } + + if (strategy_->DidClipScreenshot()) { + DisplayScreenshotClippedNotification(browser_); + } + + if (browser_) { + utils::CopyImageToClipboard(result); + utils::DisplayScreenshotBubble(result, browser_); + } +} + +} // namespace brave_screenshots diff --git a/browser/brave_screenshots/screenshots_tab_feature.h b/browser/brave_screenshots/screenshots_tab_feature.h new file mode 100644 index 000000000000..dea5a71bf2e1 --- /dev/null +++ b/browser/brave_screenshots/screenshots_tab_feature.h @@ -0,0 +1,47 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_BRAVE_SCREENSHOTS_SCREENSHOTS_TAB_FEATURE_H_ +#define BRAVE_BROWSER_BRAVE_SCREENSHOTS_SCREENSHOTS_TAB_FEATURE_H_ + +#include + +#include "base/memory/weak_ptr.h" +#include "brave/browser/brave_screenshots/strategies/screenshot_strategy.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "chrome/browser/ui/browser.h" +#include "content/public/browser/web_contents.h" + +namespace brave_screenshots { + +enum ScreenshotType { + kSelection, + kViewport, + kFullPage, +}; + +class BraveScreenshotsTabFeature { + public: + BraveScreenshotsTabFeature(); + BraveScreenshotsTabFeature(const BraveScreenshotsTabFeature&) = delete; + BraveScreenshotsTabFeature& operator=(const BraveScreenshotsTabFeature&) = + delete; + ~BraveScreenshotsTabFeature(); + + void StartScreenshot(Browser* browser, ScreenshotType type); + + private: + void OnCaptureComplete(const image_editor::ScreenshotCaptureResult& result); + std::unique_ptr CreateStrategy(ScreenshotType type); + base::WeakPtr browser_ = nullptr; + std::unique_ptr strategy_ = nullptr; + base::WeakPtr web_contents_ = nullptr; + base::WeakPtrFactory weak_factory_{this}; + +}; // class BraveScreenshotsTabFeature + +} // namespace brave_screenshots + +#endif // BRAVE_BROWSER_BRAVE_SCREENSHOTS_SCREENSHOTS_TAB_FEATURE_H_ diff --git a/browser/brave_screenshots/screenshots_utils.cc b/browser/brave_screenshots/screenshots_utils.cc new file mode 100644 index 000000000000..295437e6265f --- /dev/null +++ b/browser/brave_screenshots/screenshots_utils.cc @@ -0,0 +1,44 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/brave_screenshots/screenshots_utils.h" + +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_window.h" +#include "ui/base/clipboard/scoped_clipboard_writer.h" + +namespace brave_screenshots::utils { + +using image_editor::ScreenshotCaptureResult; + +void CopyImageToClipboard(const ScreenshotCaptureResult& result) { + DVLOG(2) << __func__; + if (result.image.IsEmpty()) { + DVLOG(2) << "Image is empty"; + return; + } + + DVLOG(2) << "Writing image to clipboard"; + // Copy the image to the user's clipboard + ui::ScopedClipboardWriter(ui::ClipboardBuffer::kCopyPaste) + .WriteImage(*result.image.ToSkBitmap()); +} + +void DisplayScreenshotBubble(const ScreenshotCaptureResult& result, + base::WeakPtr browser) { + DVLOG(2) << __func__; + if (!browser || result.image.IsEmpty()) { + DVLOG(2) << "Browser is null or image is empty"; + return; + } + + DVLOG(2) << "Displaying screenshot bubble"; + // Leverage the screenshot bubble to show the user the screenshot + browser->window()->ShowScreenshotCapturedBubble( + browser->tab_strip_model()->GetActiveWebContents(), result.image); +} + +} // namespace brave_screenshots::utils diff --git a/browser/brave_screenshots/screenshots_utils.h b/browser/brave_screenshots/screenshots_utils.h new file mode 100644 index 000000000000..d8e5e2203c18 --- /dev/null +++ b/browser/brave_screenshots/screenshots_utils.h @@ -0,0 +1,27 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_BRAVE_SCREENSHOTS_SCREENSHOTS_UTILS_H_ +#define BRAVE_BROWSER_BRAVE_SCREENSHOTS_SCREENSHOTS_UTILS_H_ + +#include "base/memory/weak_ptr.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "chrome/browser/ui/browser.h" + +namespace brave_screenshots::utils { + +// While the image will be written to the clipboard, depending on its size it +// may not be displayed within Windows' clipboard history (Win+V). The limit is +// (reportedly) 4MB. Larger screenshots will be written to the clipboard, but +// will not be displayed in the clipboard history. +void CopyImageToClipboard(const image_editor::ScreenshotCaptureResult& result); + +void DisplayScreenshotBubble( + const image_editor::ScreenshotCaptureResult& result, + base::WeakPtr browser); + +} // namespace brave_screenshots::utils + +#endif // BRAVE_BROWSER_BRAVE_SCREENSHOTS_SCREENSHOTS_UTILS_H_ diff --git a/browser/brave_screenshots/strategies/fullpage_strategy.cc b/browser/brave_screenshots/strategies/fullpage_strategy.cc new file mode 100644 index 000000000000..feaad8f5ea26 --- /dev/null +++ b/browser/brave_screenshots/strategies/fullpage_strategy.cc @@ -0,0 +1,199 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/brave_screenshots/strategies/fullpage_strategy.h" + +#include +#include + +#include "base/base64.h" +#include "base/json/json_reader.h" +#include "base/json/json_writer.h" +#include "base/logging.h" +#include "base/values.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "content/public/browser/devtools_agent_host.h" +#include "content/public/browser/devtools_agent_host_client.h" +#include "content/public/browser/web_contents.h" +#include "ui/gfx/image/image.h" + +namespace { +constexpr int kMaxDimensions = 16384; // 16k limit for GPU textures +} // namespace + +namespace brave_screenshots { + +FullPageStrategy::FullPageStrategy() = default; + +FullPageStrategy::~FullPageStrategy() { + DVLOG(2) << __func__; + // If anything is still attached, tear it down + if (devtools_host_ && devtools_host_->IsAttached()) { + devtools_host_->DetachClient(this); + } +} + +void FullPageStrategy::Capture( + content::WebContents* web_contents, + image_editor::ScreenshotCaptureCallback callback) { + // Store the WebContents and callback + web_contents_ = web_contents->GetWeakPtr(); + callback_ = std::move(callback); + + // Attach to the DevToolsAgentHost + devtools_host_ = content::DevToolsAgentHost::GetOrCreateFor(web_contents); + devtools_host_->AttachClient(this); + + // Step 1: Request layout metrics + RequestPageLayoutMetrics(); +} + +void FullPageStrategy::RequestPageLayoutMetrics() { + // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-getLayoutMetrics + SendDevToolsCommand("Page.getLayoutMetrics", base::Value::Dict(), next_id_++); +} + +// Response arrives in DispatchProtocolMessage +void FullPageStrategy::OnLayoutMetricsReceived(int width, int height) { + // Maybe clip dimensions + if (width > kMaxDimensions) { + DVLOG(3) << "Clipping screenshot width to " << kMaxDimensions; + width = kMaxDimensions; + screenshot_was_clipped_ = true; + } + + if (height > kMaxDimensions) { + DVLOG(3) << "Clipping screenshot height to " << kMaxDimensions; + height = kMaxDimensions; + screenshot_was_clipped_ = true; + } + + // Check for invalid dimensions + if (width <= 0 || height <= 0) { + DVLOG(2) << "Invalid dimensions from Page.getLayoutMetrics"; + RunCallback({}); + return; + } + + // Step 2: Having received the layout metrics, request the screenshot + RequestFullPageScreenshot(width, height); +} + +bool FullPageStrategy::DidClipScreenshot() const { + return screenshot_was_clipped_; +} + +// We pass explicit dimensions to avoid hitting the GPU limit. If the page is +// small enough, the dimensions we pass will be the same as the document itself. +// If the page is too large, we'll cap either value to 16384. +void FullPageStrategy::RequestFullPageScreenshot(int width, int height) { + DVLOG(2) << "Requesting full page screenshot with dimensions: " << width + << "x" << height; + // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot + base::Value::Dict clip; + clip.Set("x", 0); + clip.Set("y", 0); + clip.Set("width", width); + clip.Set("height", height); + clip.Set("scale", 1); + + base::Value::Dict params; + params.Set("captureBeyondViewport", true); + params.Set("clip", std::move(clip)); + + SendDevToolsCommand("Page.captureScreenshot", std::move(params), next_id_++); +} + +// Actually sends the JSON command +void FullPageStrategy::SendDevToolsCommand(const std::string& command, + base::Value::Dict params, + int command_id) { + base::Value::Dict message; + message.Set("id", command_id); + message.Set("method", command); + message.Set("params", std::move(params)); + + std::string json; + base::JSONWriter::Write(message, &json); + devtools_host_->DispatchProtocolMessage( + this, base::as_bytes(base::make_span(json))); +} + +// DevToolsAgentHostClient overrides +void FullPageStrategy::DispatchProtocolMessage( + content::DevToolsAgentHost* host, + base::span message) { + // Convert the incoming message to a string + std::string message_str(message.begin(), message.end()); + + // Determine whether a response to "getLayoutMetrics" or "captureScreenshot" + auto parsed = base::JSONReader::Read(message_str); + if (!parsed || !parsed->is_dict()) { + LOG(ERROR) << "Invalid JSON response from DevTools protocol"; + RunCallback({}); + return; + } + + // Is this a content-size response? + // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-getLayoutMetrics + const base::Value::Dict* css_content_size = + parsed->GetDict().FindDictByDottedPath("result.cssContentSize"); + if (css_content_size) { + DVLOG(2) << "Layout metrics received"; + + int width = css_content_size->FindInt("width").value_or(0); + int height = css_content_size->FindInt("height").value_or(0); + + OnLayoutMetricsReceived(width, height); + return; + } + + // Is this a screenshot response? + // https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-captureScreenshot + const std::string* encoded_png = + parsed->GetDict().FindStringByDottedPath("result.data"); + if (encoded_png) { + DVLOG(2) << "Screenshot captured"; + + std::string decoded_png; + image_editor::ScreenshotCaptureResult result; + + DVLOG(2) << "Decoding PNG"; + base::Base64Decode(*encoded_png, &decoded_png); + + DVLOG(2) << "Creating image from PNG"; + result.image = gfx::Image::CreateFrom1xPNGBytes( + base::as_bytes(base::make_span(decoded_png))); + + RunCallback(result); + return; + } + + // If we get here, it's an unknown response + DLOG(WARNING) << "Unknown/Unhandled DevTools response: " << message_str; + + RunCallback({}); +} + +void FullPageStrategy::AgentHostClosed(content::DevToolsAgentHost* host) { + DVLOG(2) << __func__; + RunCallback({}); +} + +void FullPageStrategy::RunCallback( + const image_editor::ScreenshotCaptureResult& result) { + DVLOG(2) << __func__; + // Run the callback, if it exists + if (callback_) { + std::move(callback_).Run(result); + } + + // Detach from the DevToolsAgentHost + if (devtools_host_ && devtools_host_->IsAttached()) { + devtools_host_->DetachClient(this); + } +} + +} // namespace brave_screenshots diff --git a/browser/brave_screenshots/strategies/fullpage_strategy.h b/browser/brave_screenshots/strategies/fullpage_strategy.h new file mode 100644 index 000000000000..ddb482e41f98 --- /dev/null +++ b/browser/brave_screenshots/strategies/fullpage_strategy.h @@ -0,0 +1,68 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_FULLPAGE_STRATEGY_H_ +#define BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_FULLPAGE_STRATEGY_H_ + +#include + +#include "brave/browser/brave_screenshots/strategies/screenshot_strategy.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "content/public/browser/devtools_agent_host.h" +#include "content/public/browser/devtools_agent_host_client.h" +#include "content/public/browser/web_contents.h" + +namespace brave_screenshots { + +class FullPageStrategy : public BraveScreenshotStrategy, + public content::DevToolsAgentHostClient { + public: + FullPageStrategy(); + FullPageStrategy(const FullPageStrategy&) = delete; + FullPageStrategy& operator=(const FullPageStrategy&) = delete; + ~FullPageStrategy() override; + + // BraveScreenshotStrategy implementation + void Capture(content::WebContents* web_contents, + image_editor::ScreenshotCaptureCallback callback) override; + + // DevToolsAgentHostClient overrides + void DispatchProtocolMessage(content::DevToolsAgentHost* host, + base::span message) override; + + bool DidClipScreenshot() const override; + + private: + // DevToolsAgentHostClient overrides + void AgentHostClosed(content::DevToolsAgentHost* host) override; + + // Steps: + void RequestPageLayoutMetrics(); + + // Some screenshots may need to be clipped to avoid the GPU limit. See + // https://crbug.com/1260828 for more information. The developer tools also + // have an explicit limit in place (see src/third_party/devtools-frontend/ + // src/front_end/panels/emulation/DeviceModeView.ts) + void OnLayoutMetricsReceived(int width, int height); + void RequestFullPageScreenshot(int width, int height); + + // Utility functions + void SendDevToolsCommand(const std::string& command, + base::Value::Dict params, + int command_id); + + // Called on success and failure + void RunCallback(const image_editor::ScreenshotCaptureResult& result); + + image_editor::ScreenshotCaptureCallback callback_; + base::WeakPtr web_contents_; + scoped_refptr devtools_host_; + bool screenshot_was_clipped_ = false; + int next_id_ = 1; +}; + +} // namespace brave_screenshots + +#endif // BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_FULLPAGE_STRATEGY_H_ diff --git a/browser/brave_screenshots/strategies/screenshot_strategy.h b/browser/brave_screenshots/strategies/screenshot_strategy.h new file mode 100644 index 000000000000..c4a3a81abf99 --- /dev/null +++ b/browser/brave_screenshots/strategies/screenshot_strategy.h @@ -0,0 +1,27 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_SCREENSHOT_STRATEGY_H_ +#define BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_SCREENSHOT_STRATEGY_H_ + +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "content/public/browser/web_contents.h" + +namespace brave_screenshots { + +class BraveScreenshotStrategy { + public: + virtual ~BraveScreenshotStrategy() = default; + + // Did the strategy clip/resize the screenshot? + virtual bool DidClipScreenshot() const = 0; + + virtual void Capture( + content::WebContents* web_contents, + const image_editor::ScreenshotCaptureCallback callback) = 0; +}; +} // namespace brave_screenshots + +#endif // BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_SCREENSHOT_STRATEGY_H_ diff --git a/browser/brave_screenshots/strategies/selection_strategy.cc b/browser/brave_screenshots/strategies/selection_strategy.cc new file mode 100644 index 000000000000..3d91b8377067 --- /dev/null +++ b/browser/brave_screenshots/strategies/selection_strategy.cc @@ -0,0 +1,37 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/brave_screenshots/strategies/selection_strategy.h" + +#include + +#include "base/logging.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "content/public/browser/web_contents.h" + +namespace brave_screenshots { + +SelectionStrategy::SelectionStrategy(content::WebContents* web_contents) + : image_editor::ScreenshotFlow(web_contents) { + DVLOG(2) << "SelectionStrategy created"; +} + +SelectionStrategy::~SelectionStrategy() { + DVLOG(2) << "SelectionStrategy destroyed"; +} + +// Requests the user to select a region of the screen to capture +void SelectionStrategy::Capture( + content::WebContents* web_contents, + image_editor::ScreenshotCaptureCallback callback) { + DVLOG(2) << "SelectionStrategy::Capture"; + image_editor::ScreenshotFlow::Start(std::move(callback)); +} + +bool SelectionStrategy::DidClipScreenshot() const { + return false; +} + +} // namespace brave_screenshots diff --git a/browser/brave_screenshots/strategies/selection_strategy.h b/browser/brave_screenshots/strategies/selection_strategy.h new file mode 100644 index 000000000000..567880cb9e36 --- /dev/null +++ b/browser/brave_screenshots/strategies/selection_strategy.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_SELECTION_STRATEGY_H_ +#define BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_SELECTION_STRATEGY_H_ + +#include "brave/browser/brave_screenshots/strategies/screenshot_strategy.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "content/public/browser/web_contents.h" + +namespace brave_screenshots { +class SelectionStrategy : public BraveScreenshotStrategy, + public image_editor::ScreenshotFlow { + public: + explicit SelectionStrategy(content::WebContents* web_contents); + // Disable copy and assign + SelectionStrategy(const SelectionStrategy&) = delete; + SelectionStrategy& operator=(const SelectionStrategy&) = delete; + ~SelectionStrategy() override; + // Requests the user to select a region of the screen to capture + void Capture(content::WebContents* web_contents, + image_editor::ScreenshotCaptureCallback callback) override; + + bool DidClipScreenshot() const override; +}; +} // namespace brave_screenshots + +#endif // BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_SELECTION_STRATEGY_H_ diff --git a/browser/brave_screenshots/strategies/viewport_strategy.cc b/browser/brave_screenshots/strategies/viewport_strategy.cc new file mode 100644 index 000000000000..e102e2c1796a --- /dev/null +++ b/browser/brave_screenshots/strategies/viewport_strategy.cc @@ -0,0 +1,37 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#include "brave/browser/brave_screenshots/strategies/viewport_strategy.h" + +#include + +#include "base/logging.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "content/public/browser/web_contents.h" + +namespace brave_screenshots { + +ViewportStrategy::ViewportStrategy(content::WebContents* web_contents) + : image_editor::ScreenshotFlow(web_contents) { + DVLOG(2) << "ViewportStrategy created"; +} + +ViewportStrategy::~ViewportStrategy() { + DVLOG(2) << "ViewportStrategy destroyed"; +} + +// Captures the visible portion of the page (viewport) +void ViewportStrategy::Capture( + content::WebContents* web_contents, + image_editor::ScreenshotCaptureCallback callback) { + DVLOG(2) << "ViewportStrategy::Capture"; + image_editor::ScreenshotFlow::StartFullscreenCapture(std::move(callback)); +} + +bool ViewportStrategy::DidClipScreenshot() const { + return false; +} + +} // namespace brave_screenshots diff --git a/browser/brave_screenshots/strategies/viewport_strategy.h b/browser/brave_screenshots/strategies/viewport_strategy.h new file mode 100644 index 000000000000..1138e5a660e7 --- /dev/null +++ b/browser/brave_screenshots/strategies/viewport_strategy.h @@ -0,0 +1,30 @@ +// Copyright (c) 2025 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_VIEWPORT_STRATEGY_H_ +#define BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_VIEWPORT_STRATEGY_H_ + +#include "brave/browser/brave_screenshots/strategies/screenshot_strategy.h" +#include "chrome/browser/image_editor/screenshot_flow.h" +#include "content/public/browser/web_contents.h" + +namespace brave_screenshots { +class ViewportStrategy : public BraveScreenshotStrategy, + public image_editor::ScreenshotFlow { + public: + explicit ViewportStrategy(content::WebContents* web_contents); + // Disable copy and assign + ViewportStrategy(const ViewportStrategy&) = delete; + ViewportStrategy& operator=(const ViewportStrategy&) = delete; + ~ViewportStrategy() override; + // Captures the visible portion of the page (viewport) + void Capture(content::WebContents* web_contents, + image_editor::ScreenshotCaptureCallback callback) override; + + bool DidClipScreenshot() const override; +}; +} // namespace brave_screenshots + +#endif // BRAVE_BROWSER_BRAVE_SCREENSHOTS_STRATEGIES_VIEWPORT_STRATEGY_H_ diff --git a/browser/brave_screenshots/test/brave_screenshots_unittests.cc b/browser/brave_screenshots/test/brave_screenshots_unittests.cc new file mode 100644 index 000000000000..d3cf1a4e8876 --- /dev/null +++ b/browser/brave_screenshots/test/brave_screenshots_unittests.cc @@ -0,0 +1,138 @@ +/* Copyright (c) 2023 The Brave Authors. All rights reserved. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include + +#include "base/test/scoped_feature_list.h" +#include "brave/app/brave_command_ids.h" +#include "brave/browser/brave_screenshots/features.h" +#include "chrome/browser/renderer_context_menu/render_view_context_menu.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/test/base/scoped_testing_local_state.h" +#include "chrome/test/base/test_browser_window.h" +#include "chrome/test/base/testing_browser_process.h" +#include "chrome/test/base/testing_profile.h" +#include "content/public/browser/context_menu_params.h" +#include "content/public/browser/web_contents.h" +#include "content/public/test/browser_task_environment.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "ui/base/clipboard/clipboard.h" + +class BraveRenderViewContextMenuMock : public BraveRenderViewContextMenu { + public: + using BraveRenderViewContextMenu::BraveRenderViewContextMenu; + + void Show() override {} + void SetBrowser(Browser* browser) { browser_ = browser; } + + Browser* GetBrowser() const override { + if (browser_) { + return browser_; + } + return BraveRenderViewContextMenu::GetBrowser(); + } + + private: + raw_ptr browser_ = nullptr; +}; + +class BraveScreenshotsContextMenuTest : public testing::Test { + protected: + BraveScreenshotsContextMenuTest() + : testing_local_state_(TestingBrowserProcess::GetGlobal()) {} + + content::WebContents* GetWebContents() { return web_contents_.get(); } + + // Returns a test context menu. + std::unique_ptr CreateContextMenu( + content::WebContents* web_contents, + content::ContextMenuParams params, + bool is_pwa_browser = false) { + auto menu = std::make_unique( + *web_contents->GetPrimaryMainFrame(), params); + Browser::CreateParams create_params( + is_pwa_browser ? Browser::Type::TYPE_APP : Browser::Type::TYPE_NORMAL, + profile_.get(), true); + auto test_window = std::make_unique(); + create_params.window = test_window.get(); + browser_.reset(Browser::Create(create_params)); + menu->SetBrowser(browser_.get()); + menu->Init(); + return menu; + } + + void SetUp() override { + TestingProfile::Builder builder; + profile_ = builder.Build(); + web_contents_ = content::WebContents::Create( + content::WebContents::CreateParams(profile_.get())); + } + + void SetBraveScreenshotsFeatureState(bool enabled) { + features_.Reset(); + features_.InitWithFeatureState( + brave_screenshots::features::kBraveScreenshots, enabled); + } + + void TearDown() override { + web_contents_.reset(); + browser_.reset(); + profile_.reset(); + + // We run into a DCHECK on Windows. The scenario is addressed explicitly + // in Chromium's source for MessageWindow::WindowClass::~WindowClass(). + // See base/win/message_window.cc for more information. + ui::Clipboard::DestroyClipboardForCurrentThread(); + } + + PrefService* GetPrefs() { return profile_->GetPrefs(); } + + private: + // NOLINTNEXTLINE (clang-tidy readability-identifier-naming) + content::BrowserTaskEnvironment browser_task_environment; + ScopedTestingLocalState testing_local_state_; + base::test::ScopedFeatureList features_; + std::unique_ptr profile_; + std::unique_ptr browser_; + std::unique_ptr web_contents_; +}; + +// We expect screenshot menu items to be present only when enabled +TEST_F(BraveScreenshotsContextMenuTest, MenuForWebPage) { + content::ContextMenuParams params; + params.page_url = GURL("https://example.com"); + + for (auto enabled : {true, false}) { + SetBraveScreenshotsFeatureState(enabled); + auto context_menu = CreateContextMenu(GetWebContents(), params); + EXPECT_TRUE(context_menu); + + // Check for the main submenu label + std::optional index = + context_menu->menu_model().GetIndexOfCommandId( + IDC_BRAVE_SCREENSHOT_TOOLS); + + if (enabled) { + EXPECT_TRUE(index.has_value()); + EXPECT_TRUE(context_menu->menu_model().GetSubmenuModelAt(*index)); + } else { + EXPECT_FALSE(index.has_value()); + } + } +} + +// We expect all menu items to be absent within developer tools' context menu +TEST_F(BraveScreenshotsContextMenuTest, MenuForDevTools) { + SetBraveScreenshotsFeatureState(true); + + content::ContextMenuParams params; + params.page_url = GURL("devtools://devtools"); + + auto context_menu = CreateContextMenu(GetWebContents(), params); + EXPECT_TRUE(context_menu); + EXPECT_FALSE(context_menu->menu_model() + .GetIndexOfCommandId(IDC_BRAVE_SCREENSHOT_TOOLS) + .has_value()); +} diff --git a/browser/sources.gni b/browser/sources.gni index d0c67b924f6a..2db99a485bd7 100644 --- a/browser/sources.gni +++ b/browser/sources.gni @@ -8,6 +8,7 @@ import("//brave/browser/autocomplete/sources.gni") import("//brave/browser/brave_news/sources.gni") import("//brave/browser/brave_referrals/sources.gni") import("//brave/browser/brave_rewards/android/sources.gni") +import("//brave/browser/brave_screenshots/buildflags/buildflags.gni") import("//brave/browser/brave_shields/sources.gni") import("//brave/browser/brave_stats/sources.gni") import("//brave/browser/brave_vpn/sources.gni") @@ -308,6 +309,13 @@ if (enable_commander) { ] } +if (enable_brave_screenshots) { + brave_chrome_browser_deps += [ + "//brave/browser/brave_screenshots:features", + "//brave/browser/brave_screenshots:tabs", + ] +} + if (is_mac) { brave_chrome_browser_sources += [ "//brave/browser/brave_app_controller_mac.h", diff --git a/browser/ui/BUILD.gn b/browser/ui/BUILD.gn index e9ef106a20d8..a55b898975b1 100644 --- a/browser/ui/BUILD.gn +++ b/browser/ui/BUILD.gn @@ -3,6 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at https://mozilla.org/MPL/2.0/. +import("//brave/browser/brave_screenshots/buildflags/buildflags.gni") import("//brave/browser/ethereum_remote_client/buildflags/buildflags.gni") import("//brave/browser/shell_integrations/buildflags/buildflags.gni") import("//brave/browser/ui/config.gni") @@ -103,6 +104,18 @@ source_set("ui") { "webui/skus_internals_ui.h", ] + if (enable_brave_screenshots) { + sources += [ + "//brave/browser/brave_screenshots/features.h", + "//brave/browser/brave_screenshots/screenshots_tab_feature.h", + "//brave/browser/brave_screenshots/strategies/fullpage_strategy.h", + "//brave/browser/brave_screenshots/strategies/screenshot_strategy.h", + "//brave/browser/brave_screenshots/strategies/selection_strategy.h", + "//brave/browser/brave_screenshots/strategies/viewport_strategy.h", + ] + deps += [ "//chrome/browser/image_editor:image_editor" ] + } + # It doesn't make sense to view the webcompat webui on iOS & Android. if (!is_android && !is_ios) { sources += [ diff --git a/browser/ui/brave_browser_command_controller.cc b/browser/ui/brave_browser_command_controller.cc index 6684ac369a8d..9e0ba982057c 100644 --- a/browser/ui/brave_browser_command_controller.cc +++ b/browser/ui/brave_browser_command_controller.cc @@ -13,6 +13,8 @@ #include "base/types/to_address.h" #include "brave/app/brave_command_ids.h" #include "brave/browser/ai_chat/ai_chat_utils.h" +#include "brave/browser/brave_screenshots/features.h" +#include "brave/browser/brave_screenshots/screenshots_tab_feature.h" #include "brave/browser/profiles/profile_util.h" #include "brave/browser/ui/brave_pages.h" #include "brave/browser/ui/browser_commands.h" @@ -322,6 +324,16 @@ void BraveBrowserCommandController::InitBraveCommandState() { browser_->is_type_normal()) { UpdateCommandForSplitView(); } + + if (brave_screenshots::features::IsBraveScreenshotsEnabled()) { + UpdateCommandEnabled(IDC_BRAVE_SCREENSHOT_TOOLS, true); + UpdateCommandEnabled(IDC_BRAVE_SCREENSHOTS_START_SELECTION_TO_CLIPBOARD, + true); + UpdateCommandEnabled(IDC_BRAVE_SCREENSHOTS_START_VIEWPORT_TO_CLIPBOARD, + true); + UpdateCommandEnabled(IDC_BRAVE_SCREENSHOTS_START_FULLPAGE_TO_CLIPBOARD, + true); + } } void BraveBrowserCommandController::UpdateCommandForBraveRewards() { @@ -711,6 +723,11 @@ bool BraveBrowserCommandController::ExecuteBraveCommandWithDisposition( case IDC_SWAP_SPLIT_VIEW: brave::SwapTabsInTile(&*browser_); break; + case IDC_BRAVE_SCREENSHOTS_START_SELECTION_TO_CLIPBOARD: + case IDC_BRAVE_SCREENSHOTS_START_VIEWPORT_TO_CLIPBOARD: + case IDC_BRAVE_SCREENSHOTS_START_FULLPAGE_TO_CLIPBOARD: + brave::TakeScreenshot(&*browser_, id); + break; default: LOG(WARNING) << "Received Unimplemented Command: " << id; break; diff --git a/browser/ui/browser_commands.cc b/browser/ui/browser_commands.cc index 7c19b8366885..baeeed875b14 100644 --- a/browser/ui/browser_commands.cc +++ b/browser/ui/browser_commands.cc @@ -20,6 +20,7 @@ #include "base/ranges/algorithm.h" #include "base/strings/utf_string_conversions.h" #include "brave/app/brave_command_ids.h" +#include "brave/browser/brave_screenshots/screenshots_tab_feature.h" #include "brave/browser/brave_shields/brave_shields_tab_helper.h" #include "brave/browser/debounce/debounce_service_factory.h" #include "brave/browser/ui/bookmark/brave_bookmark_prefs.h" @@ -51,6 +52,7 @@ #include "chrome/browser/ui/browser_tabstrip.h" #include "chrome/browser/ui/browser_window/public/browser_window_features.h" #include "chrome/browser/ui/profiles/profile_picker.h" +#include "chrome/browser/ui/tabs/public/tab_features.h" #include "chrome/browser/ui/tabs/tab_enums.h" #include "chrome/browser/ui/tabs/tab_group.h" #include "chrome/browser/ui/tabs/tab_group_model.h" @@ -1157,4 +1159,32 @@ void SwapTabsInTile(Browser* browser) { /*select_after_move*/ false); } +void TakeScreenshot(Browser* browser, int command_id) { + content::WebContents* web_contents = + browser->tab_strip_model()->GetActiveWebContents(); + + using brave_screenshots::ScreenshotType; + + ScreenshotType type; + + switch (command_id) { + case IDC_BRAVE_SCREENSHOTS_START_SELECTION_TO_CLIPBOARD: + type = ScreenshotType::kSelection; + break; + case IDC_BRAVE_SCREENSHOTS_START_VIEWPORT_TO_CLIPBOARD: + type = ScreenshotType::kViewport; + break; + case IDC_BRAVE_SCREENSHOTS_START_FULLPAGE_TO_CLIPBOARD: + type = ScreenshotType::kFullPage; + break; + default: + NOTREACHED(); + } + + tabs::TabInterface::GetFromContents(web_contents) + ->GetTabFeatures() + ->brave_screenshots_tab_feature() + ->StartScreenshot(browser, type); +} + } // namespace brave diff --git a/browser/ui/browser_commands.h b/browser/ui/browser_commands.h index 9949e6204767..8c4720f0e59a 100644 --- a/browser/ui/browser_commands.h +++ b/browser/ui/browser_commands.h @@ -128,6 +128,7 @@ void BreakTiles(Browser* browser, const std::vector& indices = {}); bool IsTabsTiled(Browser* browser, const std::vector& indices = {}); bool CanTileTabs(Browser* browser, const std::vector& indices = {}); void SwapTabsInTile(Browser* browser); +void TakeScreenshot(Browser* browser, int command_id); } // namespace brave diff --git a/browser/ui/commander/simple_command_source.cc b/browser/ui/commander/simple_command_source.cc index 8673cb4c4e98..271fd859bb81 100644 --- a/browser/ui/commander/simple_command_source.cc +++ b/browser/ui/commander/simple_command_source.cc @@ -71,6 +71,12 @@ CommandSource::CommandResults SimpleCommandSource::GetCommands( ui::Accelerator accelerator; ui::AcceleratorProvider* provider = chrome::AcceleratorProviderForBrowser(browser); + + // Provider may be nullptr during unit tests. + if (!provider) { + continue; + } + if (provider->GetAcceleratorForCommandId(command_id, &accelerator)) { item->annotation = accelerator.GetShortcutText(); } diff --git a/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.cc b/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.cc index a2cf59ffa78f..781ffa5d753d 100644 --- a/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.cc +++ b/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.cc @@ -12,6 +12,8 @@ #include "base/feature_list.h" #include "base/strings/string_util.h" #include "brave/browser/autocomplete/brave_autocomplete_scheme_classifier.h" +#include "brave/browser/brave_screenshots/features.h" +#include "brave/browser/brave_screenshots/screenshots_tab_feature.h" #include "brave/browser/brave_shields/brave_shields_tab_helper.h" #include "brave/browser/cosmetic_filters/cosmetic_filters_tab_helper.h" #include "brave/browser/renderer_context_menu/brave_spelling_options_submenu_observer.h" @@ -372,7 +374,8 @@ BraveRenderViewContextMenu::BraveRenderViewContextMenu( ai_chat_submenu_model_(this), ai_chat_change_tone_submenu_model_(this), ai_chat_change_length_submenu_model_(this), - ai_chat_social_media_post_submenu_model_(this) {} + ai_chat_social_media_post_submenu_model_(this), + brave_screenshots_submenu_model_(this) {} BraveRenderViewContextMenu::~BraveRenderViewContextMenu() = default; @@ -427,6 +430,11 @@ bool BraveRenderViewContextMenu::IsCommandIdEnabled(int id) const { #endif case IDC_CONTENT_CONTEXT_OPENLINK_SPLIT_VIEW: return CanOpenSplitViewForWebContents(source_web_contents_->GetWeakPtr()); + case IDC_BRAVE_SCREENSHOT_TOOLS: + case IDC_BRAVE_SCREENSHOTS_START_SELECTION_TO_CLIPBOARD: + case IDC_BRAVE_SCREENSHOTS_START_VIEWPORT_TO_CLIPBOARD: + case IDC_BRAVE_SCREENSHOTS_START_FULLPAGE_TO_CLIPBOARD: + return brave_screenshots::features::IsBraveScreenshotsEnabled(); case IDC_ADBLOCK_CONTEXT_BLOCK_ELEMENTS: return true; default: @@ -497,6 +505,11 @@ void BraveRenderViewContextMenu::ExecuteCommand(int id, int event_flags) { case IDC_CONTENT_CONTEXT_OPENLINK_SPLIT_VIEW: OpenLinkInSplitView(source_web_contents_->GetWeakPtr(), params_.link_url); break; + case IDC_BRAVE_SCREENSHOTS_START_SELECTION_TO_CLIPBOARD: + case IDC_BRAVE_SCREENSHOTS_START_VIEWPORT_TO_CLIPBOARD: + case IDC_BRAVE_SCREENSHOTS_START_FULLPAGE_TO_CLIPBOARD: + brave::TakeScreenshot(GetBrowser(), id); + break; case IDC_ADBLOCK_CONTEXT_BLOCK_ELEMENTS: cosmetic_filters::CosmeticFiltersTabHelper::LaunchContentPicker( source_web_contents_); @@ -676,6 +689,49 @@ void BraveRenderViewContextMenu::BuildAIChatMenu() { IDS_AI_CHAT_CONTEXT_LEO_TOOLS, &ai_chat_submenu_model_); } +void BraveRenderViewContextMenu::MaybeBuildBraveScreenshotsMenu() { + if (!brave_screenshots::features::IsBraveScreenshotsEnabled()) { + return; + } + + // Don't show the screenshots submenu on devtools (for now) + if (IsDevToolsURL(params_.page_url)) { + return; + } + + // Selection Screenshots + brave_screenshots_submenu_model_.AddItemWithStringId( + IDC_BRAVE_SCREENSHOTS_START_SELECTION_TO_CLIPBOARD, + IDS_IDC_BRAVE_SCREENSHOTS_START_SELECTION_TO_CLIPBOARD); + + // Viewport Screenshots + brave_screenshots_submenu_model_.AddItemWithStringId( + IDC_BRAVE_SCREENSHOTS_START_VIEWPORT_TO_CLIPBOARD, + IDS_IDC_BRAVE_SCREENSHOTS_START_VIEWPORT_TO_CLIPBOARD); + + // Fullpage Screenshots + brave_screenshots_submenu_model_.AddItemWithStringId( + IDC_BRAVE_SCREENSHOTS_START_FULLPAGE_TO_CLIPBOARD, + IDS_IDC_BRAVE_SCREENSHOTS_START_FULLPAGE_TO_CLIPBOARD); + + // Preferably insert the screenshots submenu after the inspect element + std::optional inspect_index = + menu_model_.GetIndexOfCommandId(IDC_CONTENT_CONTEXT_INSPECTELEMENT); + + if (inspect_index.has_value()) { + // Add screenshot submenu to the context menu + menu_model_.InsertSubMenuWithStringIdAt( + inspect_index.value(), IDC_BRAVE_SCREENSHOT_TOOLS, + IDS_IDC_BRAVE_SCREENSHOT_TOOLS, &brave_screenshots_submenu_model_); + return; + } + + // Otherwise, add it to the end of the context menu + menu_model_.AddSubMenuWithStringId(IDC_BRAVE_SCREENSHOT_TOOLS, + IDS_IDC_BRAVE_SCREENSHOT_TOOLS, + &brave_screenshots_submenu_model_); +} + void BraveRenderViewContextMenu::AddSpellCheckServiceItem(bool is_checked) { // Call our implementation, not the one in the base class. // Assumption: @@ -786,7 +842,8 @@ void BraveRenderViewContextMenu::InitMenu() { link_index.value() + 1, IDC_COPY_CLEAN_LINK, IDS_COPY_CLEAN_LINK); } } - if (GetSelectedURL(GetProfile(), params_.selection_text).has_value()) { + if (!params_.selection_text.empty() && + GetSelectedURL(GetProfile(), params_.selection_text).has_value()) { std::optional copy_index = menu_model_.GetIndexOfCommandId(IDC_CONTENT_CONTEXT_COPY); if (copy_index.has_value() && @@ -808,6 +865,8 @@ void BraveRenderViewContextMenu::InitMenu() { index.value() + 1, IDC_CONTENT_CONTEXT_OPENLINK_SPLIT_VIEW, IDS_CONTENT_CONTEXT_SPLIT_VIEW); } + + MaybeBuildBraveScreenshotsMenu(); } void BraveRenderViewContextMenu::NotifyMenuShown() { diff --git a/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.h b/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.h index 87bfbb5a3e57..3ef82bd141f2 100644 --- a/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.h +++ b/chromium_src/chrome/browser/renderer_context_menu/render_view_context_menu.h @@ -68,6 +68,7 @@ class BraveRenderViewContextMenu : public RenderViewContextMenu_Chromium { bool IsAIChatEnabled() const; void ExecuteAIChatCommand(int command); void BuildAIChatMenu(); + void MaybeBuildBraveScreenshotsMenu(); #if BUILDFLAG(ENABLE_TEXT_RECOGNITION) void CopyTextFromImage(); @@ -78,6 +79,7 @@ class BraveRenderViewContextMenu : public RenderViewContextMenu_Chromium { ui::SimpleMenuModel ai_chat_change_tone_submenu_model_; ui::SimpleMenuModel ai_chat_change_length_submenu_model_; ui::SimpleMenuModel ai_chat_social_media_post_submenu_model_; + ui::SimpleMenuModel brave_screenshots_submenu_model_; }; // Use our own subclass as the real RenderViewContextMenu. diff --git a/chromium_src/chrome/browser/ui/tabs/public/tab_features.h b/chromium_src/chrome/browser/ui/tabs/public/tab_features.h index f6e6a39aad69..69d48f657331 100644 --- a/chromium_src/chrome/browser/ui/tabs/public/tab_features.h +++ b/chromium_src/chrome/browser/ui/tabs/public/tab_features.h @@ -7,9 +7,18 @@ #define BRAVE_CHROMIUM_SRC_CHROME_BROWSER_UI_TABS_PUBLIC_TAB_FEATURES_H_ #include "base/callback_list.h" +#include "brave/browser/brave_screenshots/screenshots_tab_feature.h" -#define Init(...) \ - Init_ChromiumImpl(__VA_ARGS__); \ +#define Init(...) \ + Init_ChromiumImpl(__VA_ARGS__); \ + \ + std::unique_ptr \ + brave_screenshots_tab_feature_; \ + brave_screenshots::BraveScreenshotsTabFeature* \ + brave_screenshots_tab_feature() { \ + return brave_screenshots_tab_feature_.get(); \ + } \ + \ virtual void Init(__VA_ARGS__) #include "src/chrome/browser/ui/tabs/public/tab_features.h" // IWYU pragma: export diff --git a/chromium_src/chrome/browser/ui/tabs/tab_features.cc b/chromium_src/chrome/browser/ui/tabs/tab_features.cc index 28cfff0d23bc..e44ec1636c9d 100644 --- a/chromium_src/chrome/browser/ui/tabs/tab_features.cc +++ b/chromium_src/chrome/browser/ui/tabs/tab_features.cc @@ -5,6 +5,8 @@ #include "chrome/browser/ui/tabs/public/tab_features.h" +#include "brave/browser/brave_screenshots/features.h" +#include "brave/browser/brave_screenshots/screenshots_tab_feature.h" #include "brave/browser/ui/side_panel/brave_side_panel_utils.h" #define Init Init_ChromiumImpl @@ -19,6 +21,12 @@ void TabFeatures::Init(TabInterface& tab, Profile* profile) { CHECK(side_panel_registry_.get()); brave::RegisterContextualSidePanel(side_panel_registry_.get(), tab.GetContents()); + + // Brave Screenshots (via Context Menu and Commander) + if (brave_screenshots::features::IsBraveScreenshotsEnabled()) { + brave_screenshots_tab_feature_ = + std::make_unique(); + } } } // namespace tabs diff --git a/chromium_src/components/policy/resources/policy_templates.py b/chromium_src/components/policy/resources/policy_templates.py index 5912c4d4bbfc..aa66979e3018 100644 --- a/chromium_src/components/policy/resources/policy_templates.py +++ b/chromium_src/components/policy/resources/policy_templates.py @@ -25,7 +25,7 @@ def _LoadPolicies(orig_func): # Brave uses the group name "BraveSoftware". The child element for the # group is the policy itself (those are the yaml files in the folder). # - # Brave specific entries are get copied into place by `update_policy_files`. + # Brave specific entries are copied into place by `update_policy_files`. # We copy the files from: # `//brave/components/policy/resources/templates/policy_definitions/BraveSoftware` # pylint: disable=line-too-long # to: diff --git a/test/BUILD.gn b/test/BUILD.gn index c5dc2f598c9e..af2a1448e40c 100644 --- a/test/BUILD.gn +++ b/test/BUILD.gn @@ -4,6 +4,7 @@ # You can obtain one at https://mozilla.org/MPL/2.0/. import("//brave/android/android_browser_tests.gni") +import("//brave/browser/brave_screenshots/buildflags/buildflags.gni") import("//brave/browser/ethereum_remote_client/buildflags/buildflags.gni") import("//brave/browser/metrics/buildflags/buildflags.gni") import("//brave/build/config.gni") @@ -458,6 +459,10 @@ test("brave_unit_tests") { deps += [ "//brave/browser/ui/commander:unit_tests" ] } + if (enable_brave_screenshots) { + deps += [ "//brave/browser/brave_screenshots:unit_tests" ] + } + if (enable_captive_portal_detection) { deps += [ "//brave/components/captive_portal/content:unit_tests" ] }