/* Copyright (c) 2023 The Brave Authors. All rights reserved. * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at https://mozilla.org/MPL/2.0/. */ #import "brave/browser/brave_app_controller_mac.h" #import #import #include "base/check.h" #include "brave/app/brave_command_ids.h" #include "brave/browser/brave_browser_features.h" #include "brave/browser/ui/browser_commands.h" #include "brave/components/brave_origin/buildflags/buildflags.h" #include "brave/components/tor/buildflags/buildflags.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_commands.h" #include "chrome/browser/ui/browser_finder.h" #include "chrome/grit/generated_resources.h" #include "components/policy/core/common/policy_pref_names.h" #include "components/prefs/pref_member.h" #include "ui/base/l10n/l10n_util_mac.h" #if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) #include "brave/browser/ui/views/brave_origin/brave_origin_startup_view.h" #endif #if BUILDFLAG(ENABLE_TOR) #include "brave/browser/tor/tor_profile_service_factory.h" #include "brave/components/tor/pref_names.h" #endif #if !defined(__has_feature) || !__has_feature(objc_arc) #error "This file requires ARC support." #endif namespace { #if BUILDFLAG(ENABLE_TOR) class TorPrefObserver : public BooleanPrefMember { public: TorPrefObserver(BraveAppController* controller, NSMenu* menu) : controller_(controller), menu_(menu) { Profile* profile = [controller_ lastProfileIfLoaded]; CHECK(profile); // The incognito profile will be deleted before app termination. So we // should original profile in order to avoid crash. // https://github.com/brave/brave-core/pull/21892#issuecomment-1928944343 profile = profile->GetOriginalProfile(); incognito_pref_observer_.Init( policy::policy_prefs::kIncognitoModeAvailability, profile->GetPrefs(), base::BindRepeating(&TorPrefObserver::UpdateMenu, base::Unretained(this))); CHECK(g_browser_process); CHECK(g_browser_process->local_state()); tor_disabled_pref_observer_.Init( tor::prefs::kTorDisabled, g_browser_process->local_state(), base::BindRepeating(&TorPrefObserver::UpdateMenu, base::Unretained(this))); } private: void UpdateMenu() { [controller_ menuNeedsUpdate:menu_]; } BraveAppController* controller_; // Owner of this NSMenu* menu_; IntegerPrefMember incognito_pref_observer_; BooleanPrefMember tor_disabled_pref_observer_; }; #endif // BUILDFLAG(ENABLE_TOR) } // namespace @interface AppController (Brave) // Expose methods in chrome/..app_controller_mac.mm - (BOOL)canOpenNewBrowser; - (void)commandDispatchUsingKeyModifiers:(id)sender; @end @interface BraveAppController () { NSMenuItem* _copyMenuItem; NSMenuItem* _copyCleanLinkMenuItem; #if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) NSMenuItem* _profileMenuItem; // Stashed while startup dialog is showing NSInteger _profileMenuIndex; // Original position in the menu bar #endif #if BUILDFLAG(ENABLE_TOR) NSMenuItem* _torMenuItem; // For dock menu NSMenuItem* _torMainMenuItem; // For main menu std::unique_ptr tor_pref_observer_; std::unique_ptr tor_main_pref_observer_; #endif // BUILDFLAG(ENABLE_TOR) } @end #if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) void BraveRestoreProfileMenu() { auto* controller = static_cast(AppController.sharedController); [controller restoreProfileMenu]; } #endif @implementation BraveAppController #if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) - (BOOL)canOpenNewBrowser { if (BraveOriginStartupView::IsShowing()) { return NO; } return [super canOpenNewBrowser]; } - (void)commandDispatch:(id)sender { if (BraveOriginStartupView::IsShowing()) { // Allow Quit through so the user can exit from the startup dialog. if ([sender tag] == IDC_EXIT) { BraveOriginStartupView::Hide(); } return; } [super commandDispatch:sender]; } - (void)commandDispatchUsingKeyModifiers:(id)sender { if (BraveOriginStartupView::IsShowing()) { return; } [super commandDispatchUsingKeyModifiers:sender]; } - (BOOL)applicationShouldHandleReopen:(NSApplication*)theApplication hasVisibleWindows:(BOOL)hasVisibleWindows { if (BraveOriginStartupView::IsShowing()) { return NO; } return [super applicationShouldHandleReopen:theApplication hasVisibleWindows:hasVisibleWindows]; } - (void)application:(NSApplication*)sender openURLs:(NSArray*)urls { if (BraveOriginStartupView::IsShowing()) { return; } [super application:sender openURLs:urls]; } - (void)restoreProfileMenu { if (_profileMenuItem) { [[NSApp mainMenu] insertItem:_profileMenuItem atIndex:_profileMenuIndex]; _profileMenuItem = nil; } } #endif // BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) - (void)mainMenuCreated { [super mainMenuCreated]; #if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) // Stash and remove the Profiles top-level menu. Profile switching, "Manage", // and "Add Profile" all open browser windows that bypass the startup dialog. // restoreProfileMenu re-inserts it. It is called from the startup completion // callback, or immediately if no dialog is needed. { NSMenu* mainMenu = [NSApp mainMenu]; NSMenuItem* profileMenu = [mainMenu itemWithTag:IDC_PROFILE_MAIN_MENU]; if (profileMenu) { _profileMenuIndex = [mainMenu indexOfItem:profileMenu]; _profileMenuItem = profileMenu; [mainMenu removeItem:profileMenu]; } } #endif // BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) NSMenu* editMenu = [[[NSApp mainMenu] itemWithTag:IDC_EDIT_MENU] submenu]; _copyMenuItem = [editMenu itemWithTag:IDC_CONTENT_CONTEXT_COPY]; DCHECK(_copyMenuItem); [[_copyMenuItem menu] setDelegate:self]; _copyCleanLinkMenuItem = [editMenu itemWithTag:IDC_COPY_CLEAN_LINK]; DCHECK(_copyCleanLinkMenuItem); [[_copyCleanLinkMenuItem menu] setDelegate:self]; #if BUILDFLAG(ENABLE_TOR) // Find and set up the main menu Tor item NSMenu* fileMenu = [[[NSApp mainMenu] itemWithTag:IDC_FILE_MENU] submenu]; _torMainMenuItem = [fileMenu itemWithTag:IDC_NEW_OFFTHERECORD_WINDOW_TOR]; CHECK(_torMainMenuItem); [[_torMainMenuItem menu] setDelegate:self]; // Don't create observer yet - profile may not be loaded during startup // Observer will be created when first needed in torMainMenuNeedsUpdate [self torMainMenuNeedsUpdate]; // Initial state update #endif // BUILDFLAG(ENABLE_TOR) } - (void)dealloc { [[_copyMenuItem menu] setDelegate:nil]; [[_copyCleanLinkMenuItem menu] setDelegate:nil]; #if BUILDFLAG(ENABLE_TOR) CHECK(_torMainMenuItem); [[_torMainMenuItem menu] setDelegate:nil]; #endif // BUILDFLAG(ENABLE_TOR) } - (void)applicationWillTerminate:(NSNotification*)notification { #if BUILDFLAG(ENABLE_TOR) tor_pref_observer_.reset(); tor_main_pref_observer_.reset(); _torMenuItem = nil; _torMainMenuItem = nil; #endif [super applicationWillTerminate:notification]; } - (Browser*)getBrowser { return chrome::FindBrowserWithProfile([self lastProfileIfLoaded]); } - (BOOL)shouldShowCleanLinkItem { return brave::HasSelectedURL([self getBrowser]); } - (void)setKeyEquivalentToItem:(NSMenuItem*)item { auto* hotkeyItem = item == _copyMenuItem ? _copyMenuItem : _copyCleanLinkMenuItem; auto* noHotkeyItem = item == _copyMenuItem ? _copyCleanLinkMenuItem : _copyMenuItem; [hotkeyItem setKeyEquivalent:@"c"]; [hotkeyItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand]; [noHotkeyItem setKeyEquivalent:@""]; [noHotkeyItem setKeyEquivalentModifierMask:0]; } - (void)menuNeedsUpdate:(NSMenu*)menu { if (menu == [_copyMenuItem menu] || menu == [_copyCleanLinkMenuItem menu]) { [self copyLinkMenuNeedsUpdate]; return; } #if BUILDFLAG(ENABLE_TOR) if (menu == [_torMenuItem menu]) { [self torMenuNeedsUpdate]; return; } if (menu == [_torMainMenuItem menu]) { [self torMainMenuNeedsUpdate]; return; } #endif // BUILDFLAG(ENABLE_TOR) [super menuNeedsUpdate:menu]; } - (void)copyLinkMenuNeedsUpdate { if ([self shouldShowCleanLinkItem]) { [_copyCleanLinkMenuItem setHidden:NO]; if (base::FeatureList::IsEnabled(features::kBraveCopyCleanLinkByDefault)) { [self setKeyEquivalentToItem:_copyCleanLinkMenuItem]; } else { [self setKeyEquivalentToItem:_copyMenuItem]; } } else { [_copyCleanLinkMenuItem setHidden:YES]; [self setKeyEquivalentToItem:_copyMenuItem]; } } #if BUILDFLAG(ENABLE_TOR) - (void)torMenuNeedsUpdate { _torMenuItem.enabled = [self validateUserInterfaceItem:_torMenuItem]; _torMenuItem.hidden = !_torMenuItem.enabled; } - (void)torMainMenuNeedsUpdate { if (_torMainMenuItem) { auto* profile = [self lastProfileIfLoaded]; if (profile) { // Create observer lazily once profile is loaded if (!tor_main_pref_observer_) { tor_main_pref_observer_ = std::make_unique(self, [_torMainMenuItem menu]); } bool tor_enabled = !TorProfileServiceFactory::IsTorDisabled(profile); _torMainMenuItem.enabled = tor_enabled; _torMainMenuItem.hidden = !tor_enabled; } else { // Profile not loaded yet, hide the item _torMainMenuItem.enabled = NO; _torMainMenuItem.hidden = YES; } } } #endif // BUILDFLAG(ENABLE_TOR) - (BOOL)validateUserInterfaceItem:(id)item { NSInteger tag = [item tag]; if (tag == IDC_COPY_CLEAN_LINK) { return [self shouldShowCleanLinkItem]; } #if BUILDFLAG(ENABLE_TOR) SEL action = [item action]; if (action == @selector(commandFromDock:) && tag == IDC_NEW_OFFTHERECORD_WINDOW_TOR) { auto* profile = [self lastProfileIfLoaded]; return profile && !TorProfileServiceFactory::IsTorDisabled(profile) && [self canOpenNewBrowser]; } #endif // BUILDFLAG(ENABLE_TOR) return [super validateUserInterfaceItem:item]; } - (void)executeCommand:(id)sender withProfile:(Profile*)profile { if (!profile) { // Couldn't load the Profile. RunInSafeProfileHelper will show the // ProfilePicker instead. return; } NSInteger tag = [sender tag]; if (tag == IDC_COPY_CLEAN_LINK) { brave::CleanAndCopySelectedURL([self getBrowser]); return; } #if BUILDFLAG(ENABLE_TOR) if (tag == IDC_NEW_OFFTHERECORD_WINDOW_TOR) { brave::NewOffTheRecordWindowTor(profile); return; } #endif // BUILDFLAG(ENABLE_TOR) [super executeCommand:sender withProfile:profile]; } - (NSMenu*)applicationDockMenu:(NSApplication*)sender { #if BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) if (BraveOriginStartupView::IsShowing()) { // Return an empty dock menu while the startup dialog is showing. // The system always adds "Quit" automatically. return [[NSMenu alloc] initWithTitle:@""]; } #endif // BUILDFLAG(IS_BRAVE_ORIGIN_BRANDED) auto* menu = [super applicationDockMenu:sender]; #if BUILDFLAG(ENABLE_TOR) // Add "New Private Window with Tor" only if the "New Private Window" item // exists. "New Private Window" could be missing when policy is set to // disable it. auto* open_new_private_window = [menu itemWithTag:IDC_NEW_INCOGNITO_WINDOW]; if (open_new_private_window) { auto index = [menu indexOfItem:open_new_private_window]; _torMenuItem = [[NSMenuItem alloc] initWithTitle:l10n_util::GetNSStringWithFixup( IDS_NEW_OFFTHERECORD_WINDOW_TOR) action:@selector(commandFromDock:) keyEquivalent:@""]; [menu insertItem:_torMenuItem atIndex:index + 1]; _torMenuItem.target = self; _torMenuItem.tag = IDC_NEW_OFFTHERECORD_WINDOW_TOR; [self torMenuNeedsUpdate]; tor_pref_observer_ = std::make_unique(self, [_torMenuItem menu]); } #endif return menu; } @end // @implementation BraveAppController