1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
|
// Copyright 2019 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/cocoa/tab_menu_bridge.h"
#import <Cocoa/Cocoa.h>
#include "base/functional/callback.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/ui/recently_audible_helper.h"
#include "chrome/browser/ui/tab_ui_helper.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_user_gesture_details.h"
#include "chrome/grit/generated_resources.h"
#include "components/tabs/public/tab_interface.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/image/image_skia_util_mac.h"
using MenuItemCallback = base::RepeatingCallback<void(NSMenuItem*)>;
namespace {
void UpdateItemForWebContents(NSMenuItem* item,
content::WebContents* web_contents) {
tabs::TabInterface* const tab_interface =
tabs::TabInterface::GetFromContents(web_contents);
TabUIHelper* const tab_ui_helper =
tab_interface->GetTabFeatures()->tab_ui_helper();
auto* audio_helper = RecentlyAudibleHelper::FromWebContents(web_contents);
if (audio_helper && audio_helper->WasRecentlyAudible()) {
// If this webcontents is or was recently playing audio, append either a
// speaker-playing-sound icon or a muted-speaker icon to its title to make
// it easy to find the tabs playing sound in the Tab menu.
int title_id;
std::u16string emoji;
if (web_contents->IsAudioMuted()) {
title_id = IDS_WINDOW_AUDIO_MUTING_MAC;
emoji = u"\U0001F507";
} else {
title_id = IDS_WINDOW_AUDIO_PLAYING_MAC;
emoji = u"\U0001F50A";
}
item.title =
l10n_util::GetNSStringF(title_id, tab_ui_helper->GetTitle(), emoji);
} else {
item.title = base::SysUTF16ToNSString(tab_ui_helper->GetTitle());
}
item.image = NSImageFromImageSkia(
tab_ui_helper->GetFavicon().Rasterize(&web_contents->GetColorProvider()));
}
void RemoveMenuItems(NSArray* menu_items) {
NSMenu* tab_menu = [[menu_items firstObject] menu];
for (NSMenuItem* item in menu_items) {
[tab_menu removeItem:item];
}
}
} // namespace
@interface TabMenuListener : NSObject
- (instancetype)initWithCallback:(MenuItemCallback)callback;
- (void)activateTab:(id)sender;
@end
@implementation TabMenuListener {
MenuItemCallback _callback;
}
- (instancetype)initWithCallback:(MenuItemCallback)callback {
if ((self = [super init])) {
_callback = callback;
}
return self;
}
- (IBAction)activateTab:(id)sender {
_callback.Run(sender);
}
@end
TabMenuBridge::TabMenuBridge(TabStripModel* model, NSMenuItem* menu_item)
: model_(model), menu_item_(menu_item) {
menu_listener_ = [[TabMenuListener alloc]
initWithCallback:base::BindRepeating(
&TabMenuBridge::OnDynamicItemChosen,
// Unretained is safe here: this class owns
// MenuListener, which holds the callback
// being constructed here, so the callback
// will be destructed before this class.
base::Unretained(this))];
model_->AddObserver(this);
}
TabMenuBridge::~TabMenuBridge() {
if (model_) {
model_->RemoveObserver(this);
}
RemoveMenuItems(DynamicMenuItems());
}
void TabMenuBridge::BuildMenu() {
DCHECK(model_);
AddDynamicItemsFromModel();
}
NSMutableArray* TabMenuBridge::DynamicMenuItems() {
NSMenu* tabMenu = menu_item_.submenu;
NSMutableArray* array =
[[NSMutableArray alloc] initWithCapacity:[tabMenu numberOfItems]];
for (NSMenuItem* item in menu_item_.submenu.itemArray) {
if (item.target == menu_listener_) {
[array addObject:item];
}
}
return array;
}
void TabMenuBridge::AddDynamicItemsFromModel() {
NSMutableArray* recyclable_items = DynamicMenuItems();
NSMenu* tabMenu = menu_item_.submenu;
dynamic_items_start_ = tabMenu.numberOfItems - recyclable_items.count;
for (int i = 0; i < model_->count(); ++i) {
NSMenuItem* item;
if (recyclable_items.count) {
item = [recyclable_items firstObject];
[recyclable_items removeObjectAtIndex:0];
item.state = NSControlStateValueOff;
} else {
item = [[NSMenuItem alloc] initWithTitle:@""
action:@selector(activateTab:)
keyEquivalent:@""];
[item setTarget:menu_listener_];
}
if (model_->active_index() == i) {
[item setState:NSControlStateValueOn];
}
UpdateItemForWebContents(item, model_->GetWebContentsAt(i));
if ([item menu] == nil) {
[tabMenu addItem:item];
}
}
RemoveMenuItems(recyclable_items);
}
void TabMenuBridge::OnDynamicItemChosen(NSMenuItem* item) {
if (!model_) {
return;
}
DCHECK_EQ(item.target, menu_listener_);
int index = [menu_item_.submenu indexOfItem:item] - dynamic_items_start_;
model_->ActivateTabAt(index,
TabStripUserGestureDetails(
TabStripUserGestureDetails::GestureType::kTabMenu));
}
void TabMenuBridge::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
DCHECK(tab_strip_model);
DCHECK_EQ(tab_strip_model, model_);
// If a single WebContents is being replaced, just regenerate that one menu
// item.
if (change.type() == TabStripModelChange::kReplaced) {
const TabStripModelChange::Replace* replace = change.GetReplace();
int menu_index = replace->index + dynamic_items_start_;
UpdateItemForWebContents([menu_item_.submenu itemAtIndex:menu_index],
replace -> new_contents);
return;
}
AddDynamicItemsFromModel();
}
void TabMenuBridge::TabChangedAt(content::WebContents* contents,
int index,
TabChangeType change_type) {
DCHECK(model_);
// Ignore loading state changes - they happen very often during page load and
// are used to drive the load spinner, which is not interesting to this menu.
if (change_type == TabChangeType::kLoadingOnly) {
return;
}
int menu_index = index + dynamic_items_start_;
// It might seem like this can't happen but actually it can:
// 1) Someone calls TabMenuModel::AddWebContents
// 2) Some other observer (not this) is notified of the add
// 3) That observer responds by doing something that eventually leads into
// UpdateWebContentsStateAt, while this class still hasn't observed the
// OnTabStripModelChanged (but the method that will notify us is on the
// stack)
// 4) That UpdateWebContentsStateAt causes this object to observe a
// TabChangedAt for an index it hasn't yet been informed exists
// As such, this code early-outs instead of DCHECKing. The newly-added
// WebContents will be picked up later anyway when this object does get
// notified of the addition.
if (menu_index < 0 || menu_index >= menu_item_.submenu.numberOfItems) {
return;
}
NSMenuItem* item = [menu_item_.submenu itemAtIndex:menu_index];
UpdateItemForWebContents(item, contents);
}
void TabMenuBridge::OnTabStripModelDestroyed(TabStripModel* model) {
model_->RemoveObserver(this);
model_ = nullptr;
}
|