blob: d2e8a5393bef0fbc558ccc4e6cd93ac0861ff16d [file] [log] [blame]
// 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.
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
#import "base/apple/foundation_util.h"
#include "base/debug/crash_logging.h"
#include "base/debug/dump_without_crashing.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h" // IDC_BOOKMARK_MENU
#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_merged_surface_service_observer.h"
#include "chrome/browser/bookmarks/bookmark_parent_folder.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/bookmarks/bookmark_stats.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
#import "chrome/browser/ui/cocoa/l10n_util.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/profile_metrics/browser_profile_type.h"
#import "ui/base/cocoa/cocoa_base_utils.h"
#include "ui/base/window_open_disposition.h"
#import "ui/menus/cocoa/menu_controller.h"
using base::UserMetricsAction;
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
using content::OpenURLParams;
using content::Referrer;
namespace {
// Returns the NSMenuItem in |submenu|'s supermenu that holds |submenu|.
NSMenuItem* GetItemWithSubmenu(NSMenu* submenu) {
NSArray* parent_items = [[submenu supermenu] itemArray];
for (NSMenuItem* item in parent_items) {
if ([item submenu] == submenu) {
return item;
}
}
return nil;
}
const BookmarkNode* GetNodeByUuid(const BookmarkModel* model,
const base::Uuid& guid) {
CHECK(model);
const BookmarkNode* node = model->GetNodeByUuid(
guid, BookmarkModel::NodeTypeForUuidLookup::kAccountNodes);
if (!node) {
node = model->GetNodeByUuid(
guid, BookmarkModel::NodeTypeForUuidLookup::kLocalOrSyncableNodes);
}
return node;
}
void DoOpenBookmark(Profile* profile,
WindowOpenDisposition disposition,
const BookmarkNode* node) {
DCHECK(profile);
Browser* browser = chrome::FindTabbedBrowser(profile, true);
if (!browser) {
browser = Browser::Create(Browser::CreateParams(profile, true));
}
OpenURLParams params(node->url(), Referrer(), disposition,
ui::PAGE_TRANSITION_AUTO_BOOKMARK, false);
browser->OpenURL(params, /*navigation_handle_callback=*/{});
RecordBookmarkLaunch(BookmarkLaunchLocation::kTopMenu,
profile_metrics::GetBrowserProfileType(profile));
}
// Waits for the BookmarkMergedSurfaceServiceLoaded(), then calls
// DoOpenBookmark() on it.
//
// Owned by itself. Allocate with `new`.
class BookmarkRestorer : public BookmarkMergedSurfaceServiceObserver {
public:
BookmarkRestorer(Profile* profile,
WindowOpenDisposition disposition,
base::Uuid guid);
~BookmarkRestorer() override = default;
// BookmarkMergedSurfaceServiceObserver:
void BookmarkMergedSurfaceServiceLoaded() override;
void BookmarkMergedSurfaceServiceBeingDeleted() override;
void BookmarkNodeAdded(const BookmarkParentFolder& parent,
size_t index) override {}
void BookmarkNodesRemoved(
const BookmarkParentFolder& parent,
const base::flat_set<const bookmarks::BookmarkNode*>& nodes) override {}
void BookmarkNodeMoved(const BookmarkParentFolder& old_parent,
size_t old_index,
const BookmarkParentFolder& new_parent,
size_t new_index) override {}
void BookmarkNodeChanged(const bookmarks::BookmarkNode* node) override {}
void BookmarkNodeFaviconChanged(
const bookmarks::BookmarkNode* node) override {}
void BookmarkParentFolderChildrenReordered(
const BookmarkParentFolder& folder) override {}
void BookmarkAllUserNodesRemoved() override {}
private:
const raw_ptr<Profile> profile_;
const WindowOpenDisposition disposition_;
const base::Uuid guid_;
raw_ptr<BookmarkMergedSurfaceService> bookmark_service_;
base::ScopedObservation<BookmarkMergedSurfaceService,
BookmarkMergedSurfaceServiceObserver>
observation_{this};
};
BookmarkRestorer::BookmarkRestorer(Profile* profile,
WindowOpenDisposition disposition,
base::Uuid guid)
: profile_(profile), disposition_(disposition), guid_(guid) {
bookmark_service_ =
BookmarkMergedSurfaceServiceFactory::GetForProfile(profile);
CHECK(bookmark_service_);
observation_.Observe(bookmark_service_);
}
void BookmarkRestorer::BookmarkMergedSurfaceServiceLoaded() {
const BookmarkModel* model = bookmark_service_->bookmark_model();
if (const BookmarkNode* node = GetNodeByUuid(model, guid_)) {
DoOpenBookmark(profile_, disposition_, node);
}
delete this;
}
void BookmarkRestorer::BookmarkMergedSurfaceServiceBeingDeleted() {
delete this;
}
// Open the URL of the given BookmarkNode in the current tab. Waits for
// BookmarkModelLoaded() if needed (e.g. for a freshly-loaded profile).
void OpenBookmarkByGUID(WindowOpenDisposition disposition,
base::Uuid guid,
Profile* profile) {
if (!profile) {
// Failed to load profile, ignore.
return;
}
BookmarkMergedSurfaceService* bookmark_service =
BookmarkMergedSurfaceServiceFactory::GetForProfile(profile);
CHECK(bookmark_service);
if (!bookmark_service->loaded()) {
// Bookmark service hasn't loaded yet. Wait for
// BookmarkMergedSurfaceServiceLoaded(), and *then* open it.
std::ignore = new BookmarkRestorer(profile, disposition, std::move(guid));
return;
}
const BookmarkNode* node =
GetNodeByUuid(bookmark_service->bookmark_model(), guid);
if (!node) {
// Bookmark not known, ignore.
return;
}
// BookmarkModel already loaded and the bookmark is known. Open it
// immediately.
DoOpenBookmark(profile, disposition, node);
}
} // namespace
@implementation BookmarkMenuCocoaController {
raw_ptr<BookmarkMenuBridge, AcrossTasksDanglingUntriaged>
_bridge; // Weak. Owns |self|.
}
+ (NSString*)tooltipForNode:(const BookmarkNode*)node {
NSString* title = base::SysUTF16ToNSString(node->GetTitle());
if (node->is_folder()) {
return title;
}
std::string urlString = node->url().possibly_invalid_spec();
NSString* url = base::SysUTF8ToNSString(urlString);
return cocoa_l10n_util::TooltipForURLAndTitle(url, title);
}
- (instancetype)initWithBridge:(BookmarkMenuBridge*)bridge {
if ((self = [super init])) {
_bridge = bridge;
DCHECK(_bridge);
}
return self;
}
- (BOOL)validateMenuItem:(NSMenuItem*)menuItem {
return ![AppController.sharedController keyWindowIsModal];
}
// NSMenu delegate method: called just before menu is displayed.
- (void)menuNeedsUpdate:(NSMenu*)menu {
Profile* profile = _bridge->GetProfile();
if (!profile) {
// Unfortunately, we can't update a menu with a dead profile.
return;
}
// The root menu does not have a corresponding bookmark node.
if (_bridge->IsMenuRoot(menu)) {
_bridge->UpdateRootMenuIfInvalid();
return;
}
// Find the bookmark node corresponding to this menu.
const BookmarkModel* model =
BookmarkMergedSurfaceServiceFactory::GetForProfile(profile)
->bookmark_model();
NSMenuItem* item = GetItemWithSubmenu(menu);
base::Uuid guid = _bridge->TagToGUID([item tag]);
const BookmarkNode* node = GetNodeByUuid(model, guid);
if (!(node && node->is_folder())) {
// TODO(crbug.com/417269167): every non-root menu must correspond to a
// bookmark folder by construction.
SCOPED_CRASH_KEY_NUMBER("BookmarkMenuCocoaController", "NSMenuItem",
[item tag]);
SCOPED_CRASH_KEY_STRING32("BookmarkMenuCocoaController", "NSMenuItem",
base::SysNSStringToUTF8([item title]));
base::debug::DumpWithoutCrashing();
return;
}
_bridge->UpdateNonRootMenu(menu, BookmarkParentFolder::FromFolderNode(node));
}
- (BOOL)menuHasKeyEquivalent:(NSMenu*)menu
forEvent:(NSEvent*)event
target:(id*)target
action:(SEL*)action {
// Note it is OK to return NO if there's already an item in |menu| that
// handles |event|.
return NO;
}
// Open the URL of the given BookmarkNode in the current tab. If the Profile
// is not loaded in memory, load it first.
- (void)openURLForGUID:(base::Uuid)guid {
WindowOpenDisposition disposition =
ui::WindowOpenDispositionFromNSEvent([NSApp currentEvent]);
if (Profile* profile = _bridge->GetProfile()) {
OpenBookmarkByGUID(disposition, std::move(guid), profile);
} else {
// Both BookmarkMenuBridge and BookmarkMenuCocoaController could get
// destroyed before RunInSafeProfileHelper finishes. The callback needs to
// be self-contained.
app_controller_mac::RunInProfileSafely(
_bridge->GetProfileDir(),
base::BindOnce(&OpenBookmarkByGUID, disposition, std::move(guid)),
app_controller_mac::kIgnoreOnFailure);
}
}
- (IBAction)openBookmarkMenuItem:(id)sender {
NSInteger tag = [sender tag];
base::Uuid guid = _bridge->TagToGUID(tag);
[self openURLForGUID:std::move(guid)];
}
+ (void)openBookmarkByGUID:(base::Uuid)guid
inProfile:(Profile*)profile
withDisposition:(WindowOpenDisposition)disposition {
OpenBookmarkByGUID(disposition, guid, profile);
}
@end // BookmarkMenuCocoaController