| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // 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/apps/chrome_native_app_window_views.h" |
| |
| #include <stddef.h> |
| #include <utility> |
| |
| #include "apps/ui/views/app_window_frame_view.h" |
| #include "base/no_destructor.h" |
| #include "base/stl_util.h" |
| #include "build/build_config.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/browser/app_mode/app_mode_utils.h" |
| #include "chrome/browser/extensions/chrome_app_icon.h" |
| #include "chrome/browser/extensions/chrome_app_icon_service.h" |
| #include "chrome/browser/extensions/extension_util.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/views/accelerator_table.h" |
| #include "chrome/browser/ui/views/extensions/extension_keybinding_registry_views.h" |
| #include "components/favicon/content/content_favicon_driver.h" |
| #include "components/zoom/page_zoom.h" |
| #include "components/zoom/zoom_controller.h" |
| #include "extensions/browser/app_window/app_delegate.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/views/controls/webview/webview.h" |
| #include "ui/views/widget/widget.h" |
| |
| using extensions::AppWindow; |
| |
| namespace { |
| |
| const AcceleratorMapping kAppWindowAcceleratorMap[] = { |
| { ui::VKEY_W, ui::EF_CONTROL_DOWN, IDC_CLOSE_WINDOW }, |
| { ui::VKEY_W, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, IDC_CLOSE_WINDOW }, |
| { ui::VKEY_F4, ui::EF_ALT_DOWN, IDC_CLOSE_WINDOW }, |
| }; |
| |
| // These accelerators will only be available in kiosk mode. These allow the |
| // user to manually zoom app windows. This is only necessary in kiosk mode |
| // (in normal mode, the user can zoom via the screen magnifier). |
| // TODO(xiyuan): Write a test for kiosk accelerators. |
| const AcceleratorMapping kAppWindowKioskAppModeAcceleratorMap[] = { |
| { ui::VKEY_OEM_MINUS, ui::EF_CONTROL_DOWN, IDC_ZOOM_MINUS }, |
| { ui::VKEY_OEM_MINUS, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, |
| IDC_ZOOM_MINUS }, |
| { ui::VKEY_SUBTRACT, ui::EF_CONTROL_DOWN, IDC_ZOOM_MINUS }, |
| { ui::VKEY_OEM_PLUS, ui::EF_CONTROL_DOWN, IDC_ZOOM_PLUS }, |
| { ui::VKEY_OEM_PLUS, ui::EF_SHIFT_DOWN | ui::EF_CONTROL_DOWN, IDC_ZOOM_PLUS }, |
| { ui::VKEY_ADD, ui::EF_CONTROL_DOWN, IDC_ZOOM_PLUS }, |
| { ui::VKEY_0, ui::EF_CONTROL_DOWN, IDC_ZOOM_NORMAL }, |
| { ui::VKEY_NUMPAD0, ui::EF_CONTROL_DOWN, IDC_ZOOM_NORMAL }, |
| }; |
| |
| std::map<ui::Accelerator, int> AcceleratorsFromMapping( |
| const AcceleratorMapping mapping_array[], |
| size_t mapping_length) { |
| std::map<ui::Accelerator, int> mapping; |
| for (size_t i = 0; i < mapping_length; ++i) { |
| ui::Accelerator accelerator(mapping_array[i].keycode, |
| mapping_array[i].modifiers); |
| mapping.insert(std::make_pair(accelerator, mapping_array[i].command_id)); |
| } |
| |
| return mapping; |
| } |
| |
| const std::map<ui::Accelerator, int>& GetAcceleratorTable() { |
| if (!chrome::IsRunningInForcedAppMode()) { |
| static base::NoDestructor<std::map<ui::Accelerator, int>> accelerators( |
| AcceleratorsFromMapping(kAppWindowAcceleratorMap, |
| base::size(kAppWindowAcceleratorMap))); |
| return *accelerators; |
| } |
| |
| static base::NoDestructor<std::map<ui::Accelerator, int>> |
| app_mode_accelerators([]() { |
| std::map<ui::Accelerator, int> mapping = AcceleratorsFromMapping( |
| kAppWindowAcceleratorMap, base::size(kAppWindowAcceleratorMap)); |
| std::map<ui::Accelerator, int> kiosk_mapping = AcceleratorsFromMapping( |
| kAppWindowKioskAppModeAcceleratorMap, |
| base::size(kAppWindowKioskAppModeAcceleratorMap)); |
| mapping.insert(std::begin(kiosk_mapping), std::end(kiosk_mapping)); |
| return mapping; |
| }()); |
| return *app_mode_accelerators; |
| } |
| |
| } // namespace |
| |
| ChromeNativeAppWindowViews::ChromeNativeAppWindowViews() |
| : has_frame_color_(false), |
| active_frame_color_(SK_ColorBLACK), |
| inactive_frame_color_(SK_ColorBLACK) { |
| } |
| |
| ChromeNativeAppWindowViews::~ChromeNativeAppWindowViews() {} |
| |
| void ChromeNativeAppWindowViews::OnBeforeWidgetInit( |
| const AppWindow::CreateParams& create_params, |
| views::Widget::InitParams* init_params, |
| views::Widget* widget) { |
| } |
| |
| void ChromeNativeAppWindowViews::InitializeDefaultWindow( |
| const AppWindow::CreateParams& create_params) { |
| views::Widget::InitParams init_params(views::Widget::InitParams::TYPE_WINDOW); |
| init_params.delegate = this; |
| init_params.remove_standard_frame = ShouldRemoveStandardFrame(); |
| init_params.use_system_default_icon = true; |
| if (create_params.alpha_enabled) { |
| init_params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; |
| |
| // The given window is most likely not rectangular since it uses |
| // transparency and has no standard frame, don't show a shadow for it. |
| // TODO(skuhne): If we run into an application which should have a shadow |
| // but does not have, a new attribute has to be added. |
| if (IsFrameless()) |
| init_params.shadow_type = views::Widget::InitParams::SHADOW_TYPE_NONE; |
| } |
| init_params.keep_on_top = create_params.always_on_top; |
| init_params.visible_on_all_workspaces = |
| create_params.visible_on_all_workspaces; |
| |
| OnBeforeWidgetInit(create_params, &init_params, widget()); |
| widget()->Init(init_params); |
| |
| // The frame insets are required to resolve the bounds specifications |
| // correctly. So we set the window bounds and constraints now. |
| gfx::Insets frame_insets = GetFrameInsets(); |
| gfx::Rect window_bounds = create_params.GetInitialWindowBounds(frame_insets); |
| SetContentSizeConstraints(create_params.GetContentMinimumSize(frame_insets), |
| create_params.GetContentMaximumSize(frame_insets)); |
| if (!window_bounds.IsEmpty()) { |
| using BoundsSpecification = AppWindow::BoundsSpecification; |
| bool position_specified = |
| window_bounds.x() != BoundsSpecification::kUnspecifiedPosition && |
| window_bounds.y() != BoundsSpecification::kUnspecifiedPosition; |
| if (!position_specified) |
| widget()->CenterWindow(window_bounds.size()); |
| else |
| widget()->SetBounds(window_bounds); |
| } |
| |
| #if defined(OS_CHROMEOS) |
| if (create_params.is_ime_window) |
| return; |
| #endif |
| |
| // Register accelarators supported by app windows. |
| views::FocusManager* focus_manager = GetFocusManager(); |
| const std::map<ui::Accelerator, int>& accelerator_table = |
| GetAcceleratorTable(); |
| const bool is_kiosk_app_mode = chrome::IsRunningInForcedAppMode(); |
| |
| // Ensures that kiosk mode accelerators are enabled when in kiosk mode (to be |
| // future proof). This is needed because GetAcceleratorTable() uses a static |
| // to store data and only checks kiosk mode once. If a platform app is |
| // launched before kiosk mode starts, the kiosk accelerators will not be |
| // registered. This CHECK catches the case. |
| CHECK(!is_kiosk_app_mode || |
| accelerator_table.size() == |
| base::size(kAppWindowAcceleratorMap) + |
| base::size(kAppWindowKioskAppModeAcceleratorMap)); |
| |
| // Ensure there is a ZoomController in kiosk mode, otherwise the processing |
| // of the accelerators will cause a crash. Note CHECK here because DCHECK |
| // will not be noticed, as this could only be relevant on real hardware. |
| CHECK(!is_kiosk_app_mode || |
| zoom::ZoomController::FromWebContents(web_view()->GetWebContents())); |
| |
| for (auto iter = accelerator_table.begin(); iter != accelerator_table.end(); |
| ++iter) { |
| if (is_kiosk_app_mode && !chrome::IsCommandAllowedInAppMode(iter->second)) |
| continue; |
| |
| focus_manager->RegisterAccelerator( |
| iter->first, ui::AcceleratorManager::kNormalPriority, this); |
| } |
| } |
| |
| views::NonClientFrameView* |
| ChromeNativeAppWindowViews::CreateStandardDesktopAppFrame() { |
| return views::WidgetDelegateView::CreateNonClientFrameView(widget()); |
| } |
| |
| bool ChromeNativeAppWindowViews::ShouldRemoveStandardFrame() { |
| return IsFrameless() || has_frame_color_; |
| } |
| |
| // ui::BaseWindow implementation. |
| |
| gfx::Rect ChromeNativeAppWindowViews::GetRestoredBounds() const { |
| return widget()->GetRestoredBounds(); |
| } |
| |
| ui::WindowShowState ChromeNativeAppWindowViews::GetRestoredState() const { |
| if (IsMaximized()) |
| return ui::SHOW_STATE_MAXIMIZED; |
| if (IsFullscreen()) |
| return ui::SHOW_STATE_FULLSCREEN; |
| |
| return ui::SHOW_STATE_NORMAL; |
| } |
| |
| bool ChromeNativeAppWindowViews::IsAlwaysOnTop() const { |
| return widget()->IsAlwaysOnTop(); |
| } |
| |
| // views::WidgetDelegate implementation. |
| |
| gfx::ImageSkia ChromeNativeAppWindowViews::GetWindowAppIcon() { |
| // Resulting icon is cached in aura::client::kAppIconKey window property. |
| const gfx::Image& custom_image = app_window()->custom_app_icon(); |
| if (app_window()->app_icon_url().is_valid() && |
| app_window()->show_in_shelf()) { |
| EnsureAppIconCreated(); |
| gfx::Image base_image = |
| !custom_image.IsEmpty() |
| ? custom_image |
| : gfx::Image(extensions::util::GetDefaultAppIcon()); |
| // Scale the icon to EXTENSION_ICON_LARGE. |
| const int large_icon_size = extension_misc::EXTENSION_ICON_LARGE; |
| if (base_image.Width() != large_icon_size || |
| base_image.Height() != large_icon_size) { |
| gfx::ImageSkia resized_image = |
| gfx::ImageSkiaOperations::CreateResizedImage( |
| base_image.AsImageSkia(), skia::ImageOperations::RESIZE_BEST, |
| gfx::Size(large_icon_size, large_icon_size)); |
| return gfx::ImageSkiaOperations::CreateIconWithBadge( |
| resized_image, app_icon_->image_skia()); |
| } |
| return gfx::ImageSkiaOperations::CreateIconWithBadge( |
| base_image.AsImageSkia(), app_icon_->image_skia()); |
| } |
| |
| if (!custom_image.IsEmpty()) |
| return *custom_image.ToImageSkia(); |
| EnsureAppIconCreated(); |
| return app_icon_->image_skia(); |
| } |
| |
| gfx::ImageSkia ChromeNativeAppWindowViews::GetWindowIcon() { |
| // Resulting icon is cached in aura::client::kWindowIconKey window property. |
| content::WebContents* web_contents = app_window()->web_contents(); |
| if (web_contents) { |
| favicon::FaviconDriver* favicon_driver = |
| favicon::ContentFaviconDriver::FromWebContents(web_contents); |
| gfx::Image app_icon = favicon_driver->GetFavicon(); |
| if (!app_icon.IsEmpty()) |
| return *app_icon.ToImageSkia(); |
| } |
| return gfx::ImageSkia(); |
| } |
| |
| views::NonClientFrameView* ChromeNativeAppWindowViews::CreateNonClientFrameView( |
| views::Widget* widget) { |
| return (IsFrameless() || has_frame_color_) ? |
| CreateNonStandardAppFrame() : CreateStandardDesktopAppFrame(); |
| } |
| |
| bool ChromeNativeAppWindowViews::WidgetHasHitTestMask() const { |
| return shape_ != NULL; |
| } |
| |
| void ChromeNativeAppWindowViews::GetWidgetHitTestMask(gfx::Path* mask) const { |
| shape_->getBoundaryPath(mask); |
| } |
| |
| // views::View implementation. |
| |
| bool ChromeNativeAppWindowViews::AcceleratorPressed( |
| const ui::Accelerator& accelerator) { |
| const std::map<ui::Accelerator, int>& accelerator_table = |
| GetAcceleratorTable(); |
| auto iter = accelerator_table.find(accelerator); |
| DCHECK(iter != accelerator_table.end()); |
| int command_id = iter->second; |
| switch (command_id) { |
| case IDC_CLOSE_WINDOW: |
| Close(); |
| return true; |
| case IDC_ZOOM_MINUS: |
| zoom::PageZoom::Zoom(web_view()->GetWebContents(), |
| content::PAGE_ZOOM_OUT); |
| return true; |
| case IDC_ZOOM_NORMAL: |
| zoom::PageZoom::Zoom(web_view()->GetWebContents(), |
| content::PAGE_ZOOM_RESET); |
| return true; |
| case IDC_ZOOM_PLUS: |
| zoom::PageZoom::Zoom(web_view()->GetWebContents(), content::PAGE_ZOOM_IN); |
| return true; |
| default: |
| NOTREACHED() << "Unknown accelerator sent to app window."; |
| } |
| return NativeAppWindowViews::AcceleratorPressed(accelerator); |
| } |
| |
| // NativeAppWindow implementation. |
| |
| void ChromeNativeAppWindowViews::SetFullscreen(int fullscreen_types) { |
| widget()->SetFullscreen(fullscreen_types != AppWindow::FULLSCREEN_TYPE_NONE); |
| } |
| |
| bool ChromeNativeAppWindowViews::IsFullscreenOrPending() const { |
| return widget()->IsFullscreen(); |
| } |
| |
| void ChromeNativeAppWindowViews::UpdateShape( |
| std::unique_ptr<ShapeRects> rects) { |
| shape_rects_ = std::move(rects); |
| |
| // Build a region from the list of rects when it is supplied. |
| std::unique_ptr<SkRegion> region; |
| if (shape_rects_) { |
| region = std::make_unique<SkRegion>(); |
| for (const gfx::Rect& input_rect : *shape_rects_.get()) |
| region->op(gfx::RectToSkIRect(input_rect), SkRegion::kUnion_Op); |
| } |
| shape_ = std::move(region); |
| widget()->SetShape(shape() ? std::make_unique<ShapeRects>(*shape_rects_) |
| : nullptr); |
| widget()->OnSizeConstraintsChanged(); |
| } |
| |
| bool ChromeNativeAppWindowViews::HasFrameColor() const { |
| return has_frame_color_; |
| } |
| |
| SkColor ChromeNativeAppWindowViews::ActiveFrameColor() const { |
| return active_frame_color_; |
| } |
| |
| SkColor ChromeNativeAppWindowViews::InactiveFrameColor() const { |
| return inactive_frame_color_; |
| } |
| |
| // NativeAppWindowViews implementation. |
| |
| void ChromeNativeAppWindowViews::InitializeWindow( |
| AppWindow* app_window, |
| const AppWindow::CreateParams& create_params) { |
| DCHECK(widget()); |
| has_frame_color_ = create_params.has_frame_color; |
| active_frame_color_ = create_params.active_frame_color; |
| inactive_frame_color_ = create_params.inactive_frame_color; |
| InitializeDefaultWindow(create_params); |
| extension_keybinding_registry_.reset(new ExtensionKeybindingRegistryViews( |
| Profile::FromBrowserContext(app_window->browser_context()), |
| widget()->GetFocusManager(), |
| extensions::ExtensionKeybindingRegistry::PLATFORM_APPS_ONLY, |
| NULL)); |
| } |
| |
| void ChromeNativeAppWindowViews::EnsureAppIconCreated() { |
| if (app_icon_ && app_icon_->IsValid()) |
| return; |
| |
| // To avoid recursive call, reset the smart pointer. It will be checked in |
| // OnIconUpdated to determine if this is a real update or the initial callback |
| // on icon creation. |
| app_icon_.reset(); |
| app_icon_ = |
| extensions::ChromeAppIconService::Get(app_window()->browser_context()) |
| ->CreateIcon(this, app_window()->extension_id(), |
| app_window()->app_delegate()->PreferredIconSize()); |
| } |
| |
| void ChromeNativeAppWindowViews::OnIconUpdated( |
| extensions::ChromeAppIcon* icon) { |
| if (!app_icon_) |
| return; |
| DCHECK_EQ(app_icon_.get(), icon); |
| UpdateWindowIcon(); |
| } |