// Copyright 2013 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/ash/shelf/browser_shortcut_shelf_item_controller.h"
#include <limits>
#include <utility>
#include <vector>
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/wm/desks/desks_util.h"
#include "ash/wm/window_animations.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/app_restore/full_restore_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chrome/browser/ui/ash/shelf/shelf_context_menu.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/chrome_pages.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/app_constants/constants.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/extension_prefs.h"
#include "ui/aura/window.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/image/image.h"
namespace {
// The tab-index flag for browser window menu items that do not specify a tab.
constexpr int kNoTab = std::numeric_limits<int>::max();
// Gets a list of active browsers.
BrowserList::BrowserVector GetListOfActiveBrowsers(
const ash::ShelfModel* model) {
BrowserList::BrowserVector active_browsers;
for (Browser* browser : *BrowserList::GetInstance()) {
// Only include browsers for the active user.
if (!multi_user_util::IsProfileFromActiveUser(browser->profile()))
continue;
// Exclude invisible non-minimized browser windows on the active desk.
aura::Window* native_window = browser->window()->GetNativeWindow();
if (!browser->window()->IsVisible() && !browser->window()->IsMinimized() &&
ash::desks_util::BelongsToActiveDesk(native_window)) {
continue;
}
if (!IsBrowserRepresentedInBrowserList(browser, model) &&
!browser->is_type_normal()) {
continue;
}
active_browsers.push_back(browser);
}
return active_browsers;
}
bool ShouldRecordLaunchTime(Browser* browser, const ash::ShelfModel* model) {
return !browser->profile()->IsOffTheRecord() &&
IsBrowserRepresentedInBrowserList(browser, model);
}
} // namespace
BrowserShortcutShelfItemController::BrowserShortcutShelfItemController(
ash::ShelfModel* shelf_model)
: ash::ShelfItemDelegate(ash::ShelfID(app_constants::kChromeAppId)),
shelf_model_(shelf_model) {
BrowserList::AddObserver(this);
}
BrowserShortcutShelfItemController::~BrowserShortcutShelfItemController() {
BrowserList::RemoveObserver(this);
}
// This function is responsible for handling mouse and key events that are
// triggered when Ash is the Chrome browser and when the browser icon on the
// shelf is clicked, or when the Alt+N accelerator is triggered for the
// browser. For SWA and PWA please refer to AppShortcutShelfItemController.
// For Lacros please refer to BrowserAppShelfItemController.
void BrowserShortcutShelfItemController::ItemSelected(
std::unique_ptr<ui::Event> event,
int64_t display_id,
ash::ShelfLaunchSource source,
ItemSelectedCallback callback,
const ItemFilterPredicate& filter_predicate) {
Profile* profile = ChromeShelfController::instance()->profile();
ash::full_restore::FullRestoreService::MaybeCloseNotification(profile);
if (event && (event->flags() & ui::EF_CONTROL_DOWN)) {
ash::NewWindowDelegate::GetInstance()->NewWindow(
/*incognito=*/false,
/*should_trigger_session_restore=*/true);
std::move(callback).Run(ash::SHELF_ACTION_NEW_WINDOW_CREATED, {});
return;
}
auto items =
GetAppMenuItems(event ? event->flags() : ui::EF_NONE, filter_predicate);
// In case of a keyboard event, we were called by a hotkey. In that case we
// activate the next item in line if an item of our list is already active.
//
// Here we check the implicit assumption that the type of the event that gets
// passed in is never ui::EventType::kKeyPressed. One may find it strange as
// usually ui::EventType::kKeyReleased comes in pair with
// ui::EventType::kKeyPressed, i.e, if we need to handle
// ui::EventType::kKeyReleased, then we probably need to handle
// ui::EventType::kKeyPressed too. However this is not the case here. The
// ui::KeyEvent that gets passed in is manufactured as an
// ui::EventType::kKeyReleased typed KeyEvent right before being passed in.
// This is similar to the situations of AppShortcutShelfItemController and
// BrowserAppShelfItemController.
//
// One other thing regarding the KeyEvent here that one may find confusing is
// that even though the code here says EventType::kKeyReleased, one only needs
// to conduct a press action (e.g., pressing Alt+1 on a physical device
// without letting go) to trigger this ItemSelected() function call. The
// subsequent key release action is not required. This naming disparity comes
// from the fact that while the key accelerator is triggered and handled by
// ui::AcceleratorManager::Process() with a KeyEvent instance as one of its
// inputs, further down the callstack, the same KeyEvent instance is not
// passed over into ash::Shelf::ActivateShelfItemOnDisplay(). Instead, a new
// KeyEvent instance is fabricated inside
// ash::Shelf::ActivateShelfItemOnDisplay(), with its type being
// EventType::kKeyReleased, to represent the original KeyEvent, whose type is
// EventType::kKeyPressed.
//
// The fabrication of the release typed key event was first introduced in this
// CL in 2013.
// https://chromiumcodereview.appspot.com/14551002/patch/41001/42001
//
// That said, there also exist other UX where the original KeyEvent instance
// gets passed down intact. And in those UX, we should still expect a
// EventType::kKeyPressed type. This type of UX can happen when the user keeps
// pressing the Tab key to move to the next icon, and then presses the Enter
// key to launch the app. It can also happen in a ChromeVox session, in which
// the Space key can be used to activate the app. More can be found in this
// bug. http://b/315364997.
//
// A bug is filed to track future works for fixing this confusing naming
// disparity. https://crbug.com/1473895
if (event && event->type() == ui::EventType::kKeyReleased) {
std::move(callback).Run(ActivateOrAdvanceToNextBrowser(), std::move(items));
return;
}
Browser* last_browser = chrome::FindTabbedBrowser(profile, true);
if (last_browser && !filter_predicate.is_null() &&
!filter_predicate.Run(last_browser->window()->GetNativeWindow())) {
last_browser = nullptr;
}
if (!last_browser) {
ash::NewWindowDelegate::GetInstance()->NewWindow(
/*incognito=*/false,
/*should_trigger_session_restore=*/true);
std::move(callback).Run(ash::SHELF_ACTION_NEW_WINDOW_CREATED, {});
return;
}
ash::ShelfAction action;
if (items.size() == 1) {
// Single browser, activate or minimize if active.
action =
ChromeShelfController::instance()->ActivateWindowOrMinimizeIfActive(
last_browser->window(), true /* minimize allowed */);
} else if (source == ash::LAUNCH_FROM_SHELF) {
// Multiple targets, activating from shelf, a menu will be shown.
// No need to activate or minimize the recently active browser.
action = ash::SHELF_ACTION_NONE;
} else {
// Multiple targets, not activating from shelf, no menu will be shown.
// Activate the recently active browser, never minimize.
action =
ChromeShelfController::instance()->ActivateWindowOrMinimizeIfActive(
last_browser->window(), false /* minimize not allowed */);
}
std::move(callback).Run(action, std::move(items));
}
ash::ShelfItemDelegate::AppMenuItems
BrowserShortcutShelfItemController::GetAppMenuItems(
int event_flags,
const ItemFilterPredicate& filter_predicate) {
std::vector<std::pair<Browser*, size_t>> app_menu_items;
AppMenuItems items;
bool found_tabbed_browser = false;
ChromeShelfController* controller = ChromeShelfController::instance();
for (Browser* browser : GetListOfActiveBrowsers(shelf_model_)) {
if (!filter_predicate.is_null() &&
!filter_predicate.Run(browser->window()->GetNativeWindow())) {
continue;
}
TabStripModel* tab_strip = browser->tab_strip_model();
if (browser->is_type_normal())
found_tabbed_browser = true;
if (!(event_flags & ui::EF_SHIFT_DOWN)) {
base::RecordAction(base::UserMetricsAction(
"Shelf_BrowserShortcutShelfItem_ShowWindows"));
app_menu_items.emplace_back(browser, kNoTab);
auto* tab = tab_strip->GetActiveWebContents();
const gfx::Image& icon =
ui::ResourceBundle::GetSharedInstance().GetImageNamed(
(browser->profile() && browser->profile()->IsIncognitoProfile())
? IDR_ASH_SHELF_LIST_INCOGNITO_BROWSER
: IDR_ASH_SHELF_LIST_BROWSER);
// Set the title of the app menu item to the browser window title if the
// user set one on the window. Otherwise, use the title defined in
// ChromeShelfController.
std::string browser_title = browser->user_title();
std::u16string item_title = browser_title.empty()
? controller->GetAppMenuTitle(tab)
: base::UTF8ToUTF16(browser_title);
items.push_back({static_cast<int>(app_menu_items.size() - 1), item_title,
icon.AsImageSkia()});
} else {
base::RecordAction(
base::UserMetricsAction("Shelf_BrowserShortcutShelfItem_ShowTabs"));
for (int i = 0; i < tab_strip->count(); ++i) {
auto* tab = tab_strip->GetWebContentsAt(i);
app_menu_items.emplace_back(browser, i);
items.push_back({static_cast<int>(app_menu_items.size() - 1),
controller->GetAppMenuTitle(tab),
controller->GetAppMenuIcon(tab).AsImageSkia()});
}
}
}
// If only windowed applications are open, we return an empty list to
// enforce the creation of a new browser.
if (!found_tabbed_browser)
return AppMenuItems();
app_menu_items_ = std::move(app_menu_items);
return items;
}
void BrowserShortcutShelfItemController::GetContextMenu(
int64_t display_id,
GetContextMenuCallback callback) {
ChromeShelfController* controller = ChromeShelfController::instance();
const ash::ShelfItem* item = controller->GetItem(shelf_id());
context_menu_ = ShelfContextMenu::Create(controller, item, display_id);
context_menu_->GetMenuModel(std::move(callback));
}
void BrowserShortcutShelfItemController::ExecuteCommand(bool from_context_menu,
int64_t command_id,
int32_t event_flags,
int64_t display_id) {
DCHECK(!from_context_menu);
// Check that the index is valid and the browser has not been closed.
// It's unclear why, but the browser's window may be null: crbug.com/937088
if (command_id < static_cast<int64_t>(app_menu_items_.size()) &&
app_menu_items_[command_id].first &&
app_menu_items_[command_id].first->window()) {
Browser* browser = app_menu_items_[command_id].first;
TabStripModel* tab_strip = browser->tab_strip_model();
const int tab_index = app_menu_items_[command_id].second;
if (event_flags & (ui::EF_SHIFT_DOWN | ui::EF_MIDDLE_MOUSE_BUTTON)) {
if (tab_index == kNoTab) {
tab_strip->CloseAllTabs();
} else if (tab_strip->ContainsIndex(tab_index)) {
tab_strip->CloseWebContentsAt(tab_index,
TabCloseTypes::CLOSE_USER_GESTURE);
}
} else {
multi_user_util::MoveWindowToCurrentDesktop(
browser->window()->GetNativeWindow());
if (tab_index != kNoTab && tab_strip->ContainsIndex(tab_index))
tab_strip->ActivateTabAt(tab_index);
browser->window()->Show();
browser->window()->Activate();
}
}
app_menu_items_.clear();
}
void BrowserShortcutShelfItemController::Close() {
for (Browser* browser : GetListOfActiveBrowsers(shelf_model_)) {
browser->window()->Close();
}
}
// static
bool BrowserShortcutShelfItemController::IsListOfActiveBrowserEmpty(
const ash::ShelfModel* model) {
return GetListOfActiveBrowsers(model).empty();
}
ash::ShelfAction
BrowserShortcutShelfItemController::ActivateOrAdvanceToNextBrowser() {
// Create a list of all suitable running browsers.
std::vector<Browser*> items;
// We use the list in the order of how the browsers got created - not the LRU
// order.
const BrowserList* browser_list = BrowserList::GetInstance();
for (BrowserList::const_iterator it = browser_list->begin();
it != browser_list->end(); ++it) {
if (IsBrowserRepresentedInBrowserList(*it, shelf_model_))
items.push_back(*it);
}
// If there are no suitable browsers we create a new one.
if (items.empty()) {
ash::NewWindowDelegate::GetInstance()->NewWindow(
/*incognito=*/false,
/*should_trigger_session_restore=*/true);
return ash::SHELF_ACTION_NEW_WINDOW_CREATED;
}
Browser* browser = BrowserList::GetInstance()->GetLastActive();
if (items.size() == 1) {
// If there is only one suitable browser, we can either activate it, or
// bounce it (if it is already active).
if (items[0]->window()->IsActive()) {
ash::BounceWindow(items[0]->window()->GetNativeWindow());
return ash::SHELF_ACTION_NONE;
}
browser = items[0];
} else {
// If there is more than one suitable browser, we advance to the next if
// |browser| is already active - or - check the last used browser if it can
// be used.
std::vector<Browser*>::iterator i = base::ranges::find(items, browser);
if (i != items.end()) {
if (browser->window()->IsActive())
browser = (++i == items.end()) ? items[0] : *i;
} else {
browser = chrome::FindTabbedBrowser(
ChromeShelfController::instance()->profile(), true);
if (!browser || !IsBrowserRepresentedInBrowserList(browser, shelf_model_))
browser = items[0];
}
}
DCHECK(browser);
browser->window()->Show();
browser->window()->Activate();
return ash::SHELF_ACTION_WINDOW_ACTIVATED;
}
void BrowserShortcutShelfItemController::OnBrowserAdded(Browser* browser) {
if (!ShouldRecordLaunchTime(browser, shelf_model_))
return;
const BrowserList* browser_list = BrowserList::GetInstance();
for (BrowserList::const_iterator it = browser_list->begin();
it != browser_list->end(); ++it) {
if (*it == browser)
continue;
if (ShouldRecordLaunchTime(*it, shelf_model_))
return;
}
extensions::ExtensionPrefs::Get(browser->profile())
->SetLastLaunchTime(shelf_id().app_id, base::Time::Now());
}
void BrowserShortcutShelfItemController::OnBrowserClosing(Browser* browser) {
DCHECK(browser);
// Reset pointers to the closed browser, but leave menu indices intact.
for (auto& it : app_menu_items_) {
if (it.first == browser)
it.first = nullptr;
}
}