File: bookmark_menu_bridge.mm

package info (click to toggle)
chromium 139.0.7258.127-1
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 6,122,068 kB
  • sloc: cpp: 35,100,771; ansic: 7,163,530; javascript: 4,103,002; python: 1,436,920; asm: 946,517; xml: 746,709; pascal: 187,653; perl: 88,691; sh: 88,436; objc: 79,953; sql: 51,488; cs: 44,583; fortran: 24,137; makefile: 22,147; tcl: 15,277; php: 13,980; yacc: 8,984; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (402 lines) | stat: -rw-r--r-- 13,010 bytes parent folder | download | duplicates (5)
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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
// Copyright 2012 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/bookmarks/bookmark_menu_bridge.h"

#include "base/strings/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/app_controller_mac.h"
#include "chrome/browser/bookmarks/bookmark_merged_surface_service.h"
#include "chrome/browser/bookmarks/bookmark_merged_surface_service_factory.h"
#include "chrome/browser/bookmarks/bookmark_parent_folder.h"
#include "chrome/browser/bookmarks/bookmark_parent_folder_children.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/prefs/incognito_mode_prefs.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#include "chrome/browser/ui/browser_list.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/managed/managed_bookmark_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"
#include "ui/resources/grit/ui_resources.h"

using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;

namespace {

// Recursively clear any delegates from |menu| and its unbuilt submenus.
void ClearDelegatesFromSubmenu(NSMenu* menu) {
  CHECK(menu);
  // Either the delegate has been cleared, or items were never added.
  CHECK(![menu delegate] || [menu numberOfItems] == 0);
  [menu setDelegate:nil];
  NSArray* items = [menu itemArray];
  for (NSMenuItem* item in items) {
    if ([item hasSubmenu]) {
      ClearDelegatesFromSubmenu([item submenu]);
    }
  }
}

NSString* MenuTitleForNode(const BookmarkNode* node) {
  return base::SysUTF16ToNSString(node->GetTitle());
}

}  // namespace

BookmarkMenuBridge::BookmarkMenuBridge(Profile* profile, NSMenu* menu_root)
    : profile_(profile),
      controller_([[BookmarkMenuCocoaController alloc] initWithBridge:this]),
      menu_root_(menu_root) {
  CHECK(profile_);
  profile_dir_ = profile->GetPath();
  CHECK(menu_root_);
  CHECK(![menu_root_ delegate]);
  [menu_root_ setDelegate:controller_];

  bookmark_service_ =
      BookmarkMergedSurfaceServiceFactory::GetForProfile(profile_);

  // The bookmark service is only availble for Regular and Guest profiles.
  if (!bookmark_service_) {
    return;
  }

  bookmark_service_observation_.Observe(bookmark_service_);
  if (bookmark_service_->loaded()) {
    BookmarkMergedSurfaceServiceLoaded();
  }
}

BookmarkMenuBridge::~BookmarkMenuBridge() {
  ClearBookmarkMenu();
  [menu_root_ setDelegate:nil];
}

void BookmarkMenuBridge::BookmarkMergedSurfaceServiceLoaded() {
  InvalidateMenu();
}

bool BookmarkMenuBridge::IsMenuRoot(NSMenu* menu) {
  CHECK(menu);
  return menu == menu_root_;
}

void BookmarkMenuBridge::UpdateRootMenuIfInvalid() {
  CHECK(menu_root_);
  if (!IsMenuValid()) {
    BuildRootMenu(/*recurse=*/false);
  }
}

void BookmarkMenuBridge::UpdateNonRootMenu(NSMenu* menu,
                                           const BookmarkParentFolder& folder) {
  CHECK(menu);
  CHECK(!IsMenuRoot(menu));
  CHECK(controller_);
  CHECK_EQ([menu delegate], controller_);

  AddChildrenToMenu(folder, menu, /*recurse=*/false);

  // Clear the delegate to prevent further refreshes.
  [menu setDelegate:nil];
}

bool BookmarkMenuBridge::HasContent(const BookmarkParentFolder& folder) {
  // TODO(crbug.com/390398329): Verify if this should be replaced with
  // checking visibility of underlying nodes.
  return bookmark_service_->GetChildrenCount(folder) > 0;
}

void BookmarkMenuBridge::BuildRootMenu(bool recurse) {
  if (!bookmark_service_ || !bookmark_service_->loaded()) {
    return;
  }

  if (!folder_image_) {
    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    folder_image_ = rb.GetNativeImageNamed(IDR_FOLDER_CLOSED).ToNSImage();
    [folder_image_ setTemplate:YES];
  }

  ClearBookmarkMenu();

  BookmarkParentFolder bookmark_bar_folder =
      BookmarkParentFolder::BookmarkBarFolder();
  BookmarkParentFolder managed_folder = BookmarkParentFolder::ManagedFolder();

  // Add at most one separator for the bookmark bar and the managed bookmarks
  // folder.
  if (HasContent(bookmark_bar_folder) || HasContent(managed_folder)) {
    [menu_root_ addItem:[NSMenuItem separatorItem]];
  }

  if (HasContent(managed_folder)) {
    // Most users never see this node, so the image is only loaded if needed.
    ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
    NSImage* image =
        rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER_MANAGED).ToNSImage();
    AddSubmenu(menu_root_, managed_folder, image, recurse);
  }

  // Add entries from the bookmark bar to the root menu.
  for (const BookmarkNode* node :
       bookmark_service_->GetChildren(bookmark_bar_folder)) {
    AddNodeToMenu(node, menu_root_, recurse);
  }

  BookmarkParentFolder other_folder = BookmarkParentFolder::OtherFolder();
  BookmarkParentFolder mobile_folder = BookmarkParentFolder::MobileFolder();

  // Add at most one separator for the "Other Bookmarks" and "Mobile Bookmarks"
  // folders.
  if (HasContent(other_folder) || HasContent(mobile_folder)) {
    [menu_root_ addItem:[NSMenuItem separatorItem]];
  }

  if (HasContent(other_folder)) {
    AddSubmenu(menu_root_, other_folder, folder_image_, recurse);
  }
  if (HasContent(mobile_folder)) {
    AddSubmenu(menu_root_, mobile_folder, folder_image_, recurse);
  }

  is_menu_valid_ = true;
}

void BookmarkMenuBridge::BookmarkMergedSurfaceServiceBeingDeleted() {}

void BookmarkMenuBridge::BookmarkNodeAdded(const BookmarkParentFolder& parent,
                                           size_t index) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodesRemoved(
    const BookmarkParentFolder& parent,
    const base::flat_set<const bookmarks::BookmarkNode*>& nodes) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodeMoved(
    const BookmarkParentFolder& old_parent,
    size_t old_index,
    const BookmarkParentFolder& new_parent,
    size_t new_index) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkNodeChanged(const BookmarkNode* node) {
  NSMenuItem* item = MenuItemForNode(node);
  if (item) {
    ConfigureMenuItem(node, item);
  }
}

void BookmarkMenuBridge::BookmarkNodeFaviconChanged(const BookmarkNode* node) {
  NSMenuItem* item = MenuItemForNode(node);
  if (item) {
    ConfigureMenuItem(node, item);
  }
}

void BookmarkMenuBridge::BookmarkParentFolderChildrenReordered(
    const BookmarkParentFolder& folder) {
  InvalidateMenu();
}

void BookmarkMenuBridge::BookmarkAllUserNodesRemoved() {
  InvalidateMenu();
}

BookmarkModel* BookmarkMenuBridge::GetBookmarkModelForTesting() {
  CHECK(bookmark_service_);
  return bookmark_service_->bookmark_model();
}

Profile* BookmarkMenuBridge::GetProfile() {
  return profile_;
}

const base::FilePath& BookmarkMenuBridge::GetProfileDir() const {
  return profile_dir_;
}

NSMenu* BookmarkMenuBridge::BookmarkMenu() {
  return menu_root_;
}

void BookmarkMenuBridge::ClearBookmarkMenu() {
  InvalidateMenu();
  bookmark_nodes_.clear();
  tag_to_guid_.clear();

  // Recursively delete all menus that look like a bookmark. Also delete all
  // separator items since we explicitly add them back in. This deletes
  // everything except the first item ("Add Bookmark...").
  NSArray* items = [menu_root_ itemArray];
  for (NSMenuItem* item in items) {
    // If there's a submenu, it may have a reference to |controller_|. Ensure
    // that gets nerfed recursively.
    if ([item hasSubmenu]) {
      ClearDelegatesFromSubmenu([item submenu]);
    }

    // Convention: items in the bookmark list which are bookmarks have
    // an action of openBookmarkMenuItem:.  Also, assume all items
    // with submenus are submenus of bookmarks.
    if (([item action] == @selector(openBookmarkMenuItem:)) ||
        [item hasSubmenu] || [item isSeparatorItem]) {
      // This will eventually [obj release] all its kids, if it has any.
      [menu_root_ removeItem:item];
    } else {
      // Leave it alone.
    }
  }
}

void BookmarkMenuBridge::AddSubmenu(NSMenu* menu,
                                    const BookmarkParentFolder& folder,
                                    NSImage* image,
                                    bool recurse) {
  // For permanent folders containing multiple nodes, use the first node's title
  // as the menu title.
  auto nodes = bookmark_service_->GetUnderlyingNodes(folder);
  CHECK(!nodes.empty());
  const BookmarkNode* node = nodes[0];
  NSString* title = MenuTitleForNode(node);
  NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title
                                                action:nil
                                         keyEquivalent:@""];
  [item setImage:image];
  ConfigureMenuItem(node, item);
  bookmark_nodes_[node] = item;

  NSMenu* submenu = [[NSMenu alloc] initWithTitle:title];
  [menu setSubmenu:submenu forItem:item];

  // Set a delegate and a tag on the item so that the submenu can be populated
  // when (and if) Cocoa asks for it.
  if (!recurse) {
    [submenu setDelegate:controller_];
  }

  [menu addItem:item];

  if (recurse) {
    AddChildrenToMenu(folder, submenu, recurse);
  }
}

void BookmarkMenuBridge::AddChildrenToMenu(const BookmarkParentFolder& folder,
                                           NSMenu* menu,
                                           bool recurse) {
  BookmarkParentFolderChildren children =
      bookmark_service_->GetChildren(folder);
  if (!children.size()) {
    // Permanent folders with no children are not visible.
    CHECK(folder.HoldsNonPermanentFolder());
    // For empty non-permanent folder, show an unclickable entry with the text
    // "(empty)".
    NSString* empty_string = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU);
    NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:empty_string
                                                  action:nil
                                           keyEquivalent:@""];
    [menu addItem:item];
    return;
  }
  for (const BookmarkNode* child : children) {
    AddNodeToMenu(child, menu, recurse);
  }
}

void BookmarkMenuBridge::AddNodeToMenu(const BookmarkNode* node,
                                       NSMenu* menu,
                                       bool recurse) {
  if (node->is_folder()) {
    AddSubmenu(menu, BookmarkParentFolder::FromFolderNode(node), folder_image_,
               recurse);
  } else {
    CHECK(node->is_url());
    NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:MenuTitleForNode(node)
                                                  action:nil
                                           keyEquivalent:@""];
    bookmark_nodes_[node] = item;
    ConfigureMenuItem(node, item);
    [menu addItem:item];
  }
}

void BookmarkMenuBridge::ConfigureMenuItem(const BookmarkNode* node,
                                           NSMenuItem* item) {
  [item setTitle:MenuTitleForNode(node)];
  [item setTag:node->id()];
  tag_to_guid_[node->id()] = node->uuid();

  // The following settings only apply to URL items.
  if (node->is_folder()) {
    return;
  }
  CHECK(node->is_url());

  [item setTarget:controller_];
  [item setAction:@selector(openBookmarkMenuItem:)];
  [item setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];

  // Check to see if we have a favicon.
  NSImage* favicon = nil;
  BookmarkModel* model = bookmark_service_->bookmark_model();
  if (model) {
    const gfx::Image& image = model->GetFavicon(node);
    if (!image.IsEmpty()) {
      favicon = image.ToNSImage();
    }
  }
  // If we do not have a loaded favicon, use the default site image instead.
  if (!favicon) {
    favicon = favicon::GetDefaultFavicon().ToNSImage();
    [favicon setTemplate:YES];
  }
  [item setImage:favicon];
}

NSMenuItem* BookmarkMenuBridge::MenuItemForNode(const BookmarkNode* node) {
  if (!node) {
    return nil;
  }
  auto it = bookmark_nodes_.find(node);
  if (it == bookmark_nodes_.end()) {
    return nil;
  }
  return it->second;
}

NSMenuItem* BookmarkMenuBridge::MenuItemForNodeForTest(
    const bookmarks::BookmarkNode* node) {
  return MenuItemForNode(node);
}

void BookmarkMenuBridge::OnProfileWillBeDestroyed() {
  // Recursively populate the menu before the bookmark service is destroyed.
  BuildRootMenu(/*recurse=*/true);

  bookmark_service_observation_.Reset();
  bookmark_service_ = nullptr;
  profile_ = nullptr;

  // |bookmark_nodes_| stores the nodes by pointer, so it would be unsafe to
  // keep them.
  bookmark_nodes_.clear();
}

base::Uuid BookmarkMenuBridge::TagToGUID(int64_t tag) const {
  const auto& it = tag_to_guid_.find(tag);
  return it == tag_to_guid_.end() ? base::Uuid() : it->second;
}