blob: 2eb8f525aae7602b2e3e8a9f023067c25f97fbf0 [file] [log] [blame]
// Copyright 2023 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/ui/views/compose/compose_dialog_view.h"
#include <vector>
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
#include "components/compose/core/browser/config.h"
#include "components/renderer_context_menu/context_menu_delegate.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/menus/simple_menu_model.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/view_class_properties.h"
namespace {
gfx::Rect FallbackAtBottomShiftUpToPermanentPosition(
gfx::Rect best_location,
const gfx::Rect& screen_work_area,
const gfx::Size& widget_size) {
// Make sure all sizes of the view will fit in the work area
best_location.AdjustToFit(screen_work_area);
// Now make best_location reflect the actual size of the widget.
best_location.set_size(widget_size);
return best_location;
}
gfx::Rect FallbackAtBottomShiftMinimumForVisibility(gfx::Rect best_location) {
// Just use the initial location anchored at bottom left directly.
return best_location;
}
gfx::Rect FallbackCenterOnFormFieldStrategy(const gfx::Rect& screen_work_are,
const gfx::Size& widget_size,
const gfx::Rect& anchor_rect) {
gfx::Rect widget_rect;
widget_rect.set_size(widget_size);
gfx::Vector2d translation = anchor_rect.CenterPoint().OffsetFromOrigin() -
widget_rect.CenterPoint().OffsetFromOrigin();
widget_rect.Offset(translation);
return widget_rect;
}
} // namespace
DEFINE_ELEMENT_IDENTIFIER_VALUE(kComposeWebviewElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ComposeDialogView, kComposeDialogId);
// static
gfx::Rect ComposeDialogView::CalculateBubbleBounds(
gfx::Rect screen_work_area,
gfx::Size widget_size,
gfx::Rect anchor_bounds,
std::optional<gfx::Rect> parent_bounds) {
// Widget can't be smaller than a minimum size.
widget_size.SetToMax({kComposeMaxDialogWidthPx, kComposeMinDialogHeightPx});
// We don't want to render anything within `padding` pixels of the edge of the
// screen work area.
screen_work_area.Inset(kComposeDialogWorkAreaPadding);
// If the param to stay in the window bounds is true, and the window is large
// enough, use that as the work area instead.
if (compose::GetComposeConfig().stay_in_window_bounds &&
parent_bounds.has_value()) {
if ((widget_size.width() <= parent_bounds->width() &&
widget_size.height() <= parent_bounds->height())) {
screen_work_area = parent_bounds.value();
}
}
// We don't want to render anything within `padding` pixels of the edge of the
// anchor rect. But we will if we have to (due to AdjustToFit below).
anchor_bounds.Outset(kComposeDialogAnchorPadding);
// Available space measures the distance from each side of the padded work
// area to the edge of the padded anchor (plus padding).
gfx::Insets available_space = screen_work_area.InsetsFrom(anchor_bounds);
// Ideally we render at the bottom left of the anchor. If the dialog would be
// offscreen, we reposition it.
gfx::Rect best_location(
anchor_bounds.bottom_left(),
gfx::Size(kComposeMaxDialogWidthPx, kComposeMaxDialogHeightPx));
bool space_below = available_space.bottom() >= kComposeMaxDialogHeightPx;
bool space_above = available_space.top() >= kComposeMaxDialogHeightPx;
// the position of the dialog is determined by finding a suitable location for
// a maximum-sized ComposeDialogView, so that we know that the dialog doesn't
// have to switch sides as it resizes. If the dialog is smaller than max, we
// need to apply an alignment within that larger rectangle.
if (!space_below) {
// Not enough room in the preferred location. Try above the anchor.
if (space_above) {
// If it is laid out above the anchor rect then it
// will be bottom-aligned.
best_location.set_y(anchor_bounds.y() - widget_size.height());
} else {
// If not enough space above or below, try one of the following backup
// strategies:
switch (compose::GetComposeConfig().positioning_strategy) {
case compose::DialogFallbackPositioningStrategy::kCenterOnAnchorRect:
best_location = FallbackCenterOnFormFieldStrategy(
screen_work_area, widget_size, anchor_bounds);
break;
case compose::DialogFallbackPositioningStrategy::kShiftUpUntilOnscreen:
best_location =
FallbackAtBottomShiftMinimumForVisibility(best_location);
break;
case compose::DialogFallbackPositioningStrategy::
kShiftUpUntilMaxSizeIsOnscreen:
default:
best_location = FallbackAtBottomShiftUpToPermanentPosition(
best_location, screen_work_area, widget_size);
break;
}
}
}
// Always use the size that the WebUI wants to be in the end.
best_location.set_size(widget_size);
// Always remain completely on screen within the provided insets.
best_location.AdjustToFit(screen_work_area);
return best_location;
}
ComposeDialogView::~ComposeDialogView() = default;
ComposeDialogView::ComposeDialogView(
View* anchor_view,
std::unique_ptr<WebUIContentsWrapperT<ComposeUntrustedUI>> bubble_wrapper,
const gfx::Rect& anchor_bounds,
views::BubbleBorder::Arrow anchor_position)
: WebUIBubbleDialogView(anchor_view,
bubble_wrapper->GetWeakPtr(),
anchor_bounds,
anchor_position),
anchor_bounds_(anchor_bounds),
bubble_wrapper_(std::move(bubble_wrapper)) {
SetProperty(views::kElementIdentifierKey, kComposeDialogId);
web_view()->SetProperty(views::kElementIdentifierKey,
kComposeWebviewElementId);
}
void ComposeDialogView::OnBeforeBubbleWidgetInit(
views::Widget::InitParams* params,
views::Widget* widget) const {
WebUIBubbleDialogView::OnBeforeBubbleWidgetInit(params, widget);
#if BUILDFLAG(IS_LINUX)
// In linux, windows may be clipped to their anchors' bounds,
// resulting in visual errors, unless they use accelerated rendering. See
// crbug.com/1445770 for details.
params->use_accelerated_widget_override = true;
#endif
}
gfx::Rect ComposeDialogView::GetBubbleBounds() {
gfx::Size widget_size = BubbleDialogDelegateView::GetBubbleBounds().size();
std::optional<gfx::Rect> parent_bounds;
if (GetWidget()->parent()) {
parent_bounds = GetWidget()->parent()->GetWindowBoundsInScreen();
}
display::Display display =
display::Screen::GetScreen()->GetDisplayNearestView(
GetAnchorView()->GetWidget()->GetNativeView());
gfx::Rect screen_work_area = display.work_area();
return CalculateBubbleBounds(screen_work_area, widget_size, anchor_bounds_,
parent_bounds);
}
bool ComposeDialogView::HandleContextMenu(
content::RenderFrameHost& render_frame_host,
const content::ContextMenuParams& params) {
ContextMenuDelegate* menu_delegate = ContextMenuDelegate::FromWebContents(
content::WebContents::FromRenderFrameHost(&render_frame_host));
DCHECK(menu_delegate);
std::unique_ptr<RenderViewContextMenuBase> menu =
menu_delegate->BuildMenu(render_frame_host, params);
// Remove everything that is not copy, paste, or cut or spellcheck
// suggestions.
std::vector<int> command_ids;
for (size_t index = 0; index < menu->menu_model().GetItemCount(); index++) {
int command_id = menu->menu_model().GetCommandIdAt(index);
if ((command_id < IDC_CONTENT_CONTEXT_COPY ||
command_id > IDC_CONTENT_CONTEXT_PASTE_AND_MATCH_STYLE) &&
(command_id < IDC_SPELLCHECK_SUGGESTION_0 ||
command_id > IDC_SPELLCHECK_SUGGESTION_LAST) &&
command_id != IDC_CONTENT_CONTEXT_INSPECTELEMENT && command_id > 0) {
command_ids.push_back(command_id);
}
}
for (int command_id : command_ids) {
menu->RemoveMenuItem(command_id);
}
menu->RemoveAdjacentSeparators();
// There's no method to remove the final separator if there is one, so we have
// to hack around it.
menu->RemoveSeparatorBeforeMenuItem(IDC_CONTENT_CONTEXT_INSPECTELEMENT);
menu->RemoveMenuItem(IDC_CONTENT_CONTEXT_INSPECTELEMENT);
// Only show the menu if there are items in it.
if (menu->menu_model().GetItemCount() > 0) {
menu_delegate->ShowMenu(std::move(menu));
}
return true;
}
base::WeakPtr<ComposeDialogView> ComposeDialogView::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
BEGIN_METADATA(ComposeDialogView)
END_METADATA