From 58d3ec43e2e36fc788d174af40987f7989a33fce Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Tue, 11 May 2021 11:30:48 +0700 Subject: [PATCH] Zoom menu items; Menu Items validation (#105) * Zoom menu items; Menu Items validation in MainMenuActions * Update navigational menu items on WebView state change --- DuckDuckGo/BrowserTab/Model/Tab.swift | 1 + DuckDuckGo/BrowserTab/View/WebView.swift | 26 +++++ DuckDuckGo/Common/Localizables/UserText.swift | 4 +- DuckDuckGo/Main/MainViewController.swift | 95 ++++++++----------- DuckDuckGo/Main/View/Launch.storyboard | 26 ++++- .../Main/View/MainWindowController.swift | 8 -- DuckDuckGo/Menus/MainMenu.swift | 16 ++-- DuckDuckGo/Menus/MainMenuActions.swift | 69 +++++++++++++- .../View/OptionsButtonMenu.swift | 40 +++++++- 9 files changed, 208 insertions(+), 77 deletions(-) diff --git a/DuckDuckGo/BrowserTab/Model/Tab.swift b/DuckDuckGo/BrowserTab/Model/Tab.swift index d3ad1210d4..5c5f6028f2 100644 --- a/DuckDuckGo/BrowserTab/Model/Tab.swift +++ b/DuckDuckGo/BrowserTab/Model/Tab.swift @@ -247,6 +247,7 @@ final class Tab: NSObject { private func setupWebView(shouldLoadInBackground: Bool) { webView.navigationDelegate = self webView.allowsBackForwardNavigationGestures = true + webView.allowsMagnification = true subscribeToUserScripts() subscribeToOpenExternalUrlEvents() diff --git a/DuckDuckGo/BrowserTab/View/WebView.swift b/DuckDuckGo/BrowserTab/View/WebView.swift index 374020f830..b68726e0f6 100644 --- a/DuckDuckGo/BrowserTab/View/WebView.swift +++ b/DuckDuckGo/BrowserTab/View/WebView.swift @@ -40,6 +40,32 @@ final class WebView: WKWebView { ] + static private let maxMagnification: CGFloat = 3.0 + static private let minMagnification: CGFloat = 0.5 + static private let magnificationStep: CGFloat = 0.1 + + var canZoomToActualSize: Bool { + self.window != nil && self.magnification != 1.0 + } + + var canZoomIn: Bool { + self.window != nil && self.magnification < Self.maxMagnification + } + + var canZoomOut: Bool { + self.window != nil && self.magnification > Self.minMagnification + } + + func zoomIn() { + guard canZoomIn else { return } + self.magnification = min(self.magnification + Self.magnificationStep, Self.maxMagnification) + } + + func zoomOut() { + guard canZoomOut else { return } + self.magnification = max(self.magnification - Self.magnificationStep, Self.minMagnification) + } + deinit { self.configuration.userContentController.removeAllUserScripts() } diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 8bcead54e3..da586b47f5 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -93,7 +93,9 @@ struct UserText { static let editFavorite = NSLocalizedString("edit.favorite", value: "Edit Favorite", comment: "Header of the view that edits a favorite bookmark") static let removeFromFavorites = NSLocalizedString("remove.from.favorites", value: "Remove from Favorites", comment: "Button for removing bookmarks from favorites") static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page...", comment: "Menu item for bookmarking current page") - + + static let zoom = NSLocalizedString("zoom", value: "Zoom", comment: "Menu with Zooming commands") + static let emailOptionsMenuItem = NSLocalizedString("email.optionsMenu", value: "Email Protection", comment: "Menu item email feature") static let emailOptionsMenuCreateAddressSubItem = NSLocalizedString("email.optionsMenu.createAddress", value: "Create a Duck Address", comment: "Create an email alias sub menu item") static let emailOptionsMenuViewDashboardSubItem = NSLocalizedString("email.optionsMenu.viewDashboard", value: "View Dashboard", comment: "View email dashboard sub menu item") diff --git a/DuckDuckGo/Main/MainViewController.swift b/DuckDuckGo/Main/MainViewController.swift index f4667ec467..fb494f5c35 100644 --- a/DuckDuckGo/Main/MainViewController.swift +++ b/DuckDuckGo/Main/MainViewController.swift @@ -36,8 +36,7 @@ final class MainViewController: NSViewController { let tabCollectionViewModel: TabCollectionViewModel private var selectedTabViewModelCancellable: AnyCancellable? - private var canGoForwardCancellable: AnyCancellable? - private var canGoBackCancellable: AnyCancellable? + private var navigationalCancellables = Set() private var canBookmarkCancellable: AnyCancellable? private var canInsertLastRemovedTabCancellable: AnyCancellable? private var findInPageCancellable: AnyCancellable? @@ -58,7 +57,6 @@ final class MainViewController: NSViewController { listenToKeyDownEvents() subscribeToSelectedTabViewModel() - subscribeToCanInsertLastRemovedTab() findInPageContainerView.applyDropShadow() } @@ -70,18 +68,6 @@ final class MainViewController: NSViewController { fatalError("Default AppKit State Restoration should not be used") } - func windowDidBecomeMain() { - NSApplication.shared.mainMenuTyped.setWindowRelatedMenuItems(enabled: true) - - updateBackMenuItem() - updateForwardMenuItem() - updateReopenLastClosedTabMenuItem() - } - - func windowDidResignMain() { - NSApplication.shared.mainMenuTyped.setWindowRelatedMenuItems(enabled: false) - } - func windowWillClose() { if let monitor = keyDownMonitor { NSEvent.removeMonitor(monitor) @@ -132,43 +118,32 @@ final class MainViewController: NSViewController { private func subscribeToSelectedTabViewModel() { selectedTabViewModelCancellable = tabCollectionViewModel.$selectedTabViewModel.receive(on: DispatchQueue.main).sink { [weak self] _ in + self?.navigationalCancellables = [] self?.subscribeToCanGoBackForward() self?.subscribeToFindInPage() - self?.subscribeToCanBookmark() } } private func subscribeToFindInPage() { - findInPageCancellable?.cancel() let model = tabCollectionViewModel.selectedTabViewModel?.findInPage - findInPageCancellable = model?.$visible.receive(on: DispatchQueue.main).sink { [weak self] _ in + model?.$visible.receive(on: DispatchQueue.main).sink { [weak self] _ in self?.updateFindInPage() - } + }.store(in: &self.navigationalCancellables) } private func subscribeToCanGoBackForward() { - canGoBackCancellable?.cancel() - canGoBackCancellable = tabCollectionViewModel.selectedTabViewModel?.$canGoBack.receive(on: DispatchQueue.main).sink { [weak self] _ in + tabCollectionViewModel.selectedTabViewModel?.$canGoBack.receive(on: DispatchQueue.main).sink { [weak self] _ in self?.updateBackMenuItem() - } - canGoForwardCancellable?.cancel() - canGoForwardCancellable = tabCollectionViewModel.selectedTabViewModel?.$canGoForward.receive(on: DispatchQueue.main).sink { [weak self] _ in + }.store(in: &self.navigationalCancellables) + tabCollectionViewModel.selectedTabViewModel?.$canGoForward.receive(on: DispatchQueue.main).sink { [weak self] _ in self?.updateForwardMenuItem() - } - } - - private func subscribeToCanBookmark() { - canBookmarkCancellable?.cancel() - canBookmarkCancellable = tabCollectionViewModel.selectedTabViewModel?.$canBeBookmarked.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.updateBookmarksMenu() - } - } - - private func subscribeToCanInsertLastRemovedTab() { - canInsertLastRemovedTabCancellable?.cancel() - canInsertLastRemovedTabCancellable = tabCollectionViewModel.$canInsertLastRemovedTab.receive(on: DispatchQueue.main).sink { [weak self] _ in - self?.updateReopenLastClosedTabMenuItem() - } + }.store(in: &self.navigationalCancellables) + tabCollectionViewModel.selectedTabViewModel?.$canReload.receive(on: DispatchQueue.main).sink { [weak self] _ in + self?.updateReloadMenuItem() + }.store(in: &self.navigationalCancellables) + tabCollectionViewModel.selectedTabViewModel?.$isLoading.receive(on: DispatchQueue.main).sink { [weak self] _ in + self?.updateStopMenuItem() + }.store(in: &self.navigationalCancellables) } private func updateFindInPage() { @@ -191,53 +166,63 @@ final class MainViewController: NSViewController { } private func updateBackMenuItem() { - guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { + guard self.view.window?.isMainWindow == true, + let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel + else { os_log("MainViewController: No tab view model selected", type: .error) return } guard let backMenuItem = NSApplication.shared.mainMenuTyped.backMenuItem else { - os_log("MainViewController: Failed to get reference to back menu item", type: .error) + assertionFailure("MainViewController: Failed to get reference to back menu item") return } backMenuItem.isEnabled = selectedTabViewModel.canGoBack } - func updateForwardMenuItem() { - guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { + private func updateForwardMenuItem() { + guard self.view.window?.isMainWindow == true, + let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel + else { os_log("MainViewController: No tab view model selected", type: .error) return } guard let forwardMenuItem = NSApplication.shared.mainMenuTyped.forwardMenuItem else { - os_log("MainViewController: Failed to get reference to back menu item", type: .error) + assertionFailure("MainViewController: Failed to get reference to Forward menu item") return } forwardMenuItem.isEnabled = selectedTabViewModel.canGoForward } - private func updateBookmarksMenu() { - guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { + private func updateReloadMenuItem() { + guard self.view.window?.isMainWindow == true, + let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel + else { os_log("MainViewController: No tab view model selected", type: .error) return } - guard let bookmarkThisPageMenuItem = NSApplication.shared.mainMenuTyped.bookmarkThisPageMenuItem, - let favoriteThisPageMenuItem = NSApplication.shared.mainMenuTyped.favoriteThisPageMenuItem else { - os_log("MainViewController: Failed to get reference to bookmarks menu items", type: .error) + guard let reloadMenuItem = NSApplication.shared.mainMenuTyped.reloadMenuItem else { + assertionFailure("MainViewController: Failed to get reference to Reload menu item") return } - bookmarkThisPageMenuItem.isEnabled = selectedTabViewModel.canBeBookmarked - favoriteThisPageMenuItem.isEnabled = selectedTabViewModel.canBeBookmarked + reloadMenuItem.isEnabled = selectedTabViewModel.canReload } - func updateReopenLastClosedTabMenuItem() { - guard let reopenLastClosedTabMenuItem = NSApplication.shared.mainMenuTyped.reopenLastClosedTabMenuItem else { - os_log("MainViewController: Failed to get reference to back menu item", type: .error) + private func updateStopMenuItem() { + guard self.view.window?.isMainWindow == true, + let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel + else { + os_log("MainViewController: No tab view model selected", type: .error) + return + } + guard let stopMenuItem = NSApplication.shared.mainMenuTyped.stopMenuItem else { + assertionFailure("MainViewController: Failed to get reference to Stop menu item") return } - reopenLastClosedTabMenuItem.isEnabled = tabCollectionViewModel.canInsertLastRemovedTab + stopMenuItem.isEnabled = selectedTabViewModel.isLoading } } diff --git a/DuckDuckGo/Main/View/Launch.storyboard b/DuckDuckGo/Main/View/Launch.storyboard index 3ffd69145b..ff27a6d9a5 100644 --- a/DuckDuckGo/Main/View/Launch.storyboard +++ b/DuckDuckGo/Main/View/Launch.storyboard @@ -322,7 +322,7 @@ - + @@ -337,12 +337,28 @@ + + + + + + + + + + + + + + + + - + @@ -553,6 +569,7 @@ CQ + @@ -567,8 +584,13 @@ CQ + + + + + diff --git a/DuckDuckGo/Main/View/MainWindowController.swift b/DuckDuckGo/Main/View/MainWindowController.swift index d909d46491..6a10685bdb 100644 --- a/DuckDuckGo/Main/View/MainWindowController.swift +++ b/DuckDuckGo/Main/View/MainWindowController.swift @@ -96,10 +96,6 @@ extension MainWindowController: NSWindowDelegate { return [.fullScreen, .autoHideMenuBar] } - func windowDidBecomeMain(_ notification: Notification) { - mainViewController.windowDidBecomeMain() - } - func windowDidBecomeKey(_ notification: Notification) { WindowControllersManager.shared.lastKeyMainWindowController = self } @@ -112,10 +108,6 @@ extension MainWindowController: NSWindowDelegate { mainViewController.tabBarViewController.draggingSpace.isHidden = false } - func windowDidResignMain(_ notification: Notification) { - mainViewController.windowDidResignMain() - } - func windowWillClose(_ notification: Notification) { mainViewController.windowWillClose() diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 734e7bbeda..b7756d74bb 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -30,6 +30,8 @@ final class MainMenu: NSMenu { @IBOutlet weak var backMenuItem: NSMenuItem? @IBOutlet weak var forwardMenuItem: NSMenuItem? + @IBOutlet weak var reloadMenuItem: NSMenuItem? + @IBOutlet weak var stopMenuItem: NSMenuItem? @IBOutlet weak var homeMenuItem: NSMenuItem? @IBOutlet weak var reopenLastClosedTabMenuItem: NSMenuItem? @@ -44,6 +46,11 @@ final class MainMenu: NSMenu { @IBOutlet weak var helpSeparatorMenuItem: NSMenuItem? @IBOutlet weak var sendFeedbackMenuItem: NSMenuItem? + @IBOutlet weak var toggleFullscreenMenuItem: NSMenuItem? + @IBOutlet weak var zoomInMenuItem: NSMenuItem? + @IBOutlet weak var zoomOutMenuItem: NSMenuItem? + @IBOutlet weak var actualSizeMenuItem: NSMenuItem? + required init(coder: NSCoder) { super.init(coder: coder) @@ -61,15 +68,6 @@ final class MainMenu: NSMenu { } } - func setWindowRelatedMenuItems(enabled: Bool) { - backMenuItem?.isEnabled = enabled - forwardMenuItem?.isEnabled = enabled - homeMenuItem?.isEnabled = enabled - reopenLastClosedTabMenuItem?.isEnabled = enabled - bookmarkThisPageMenuItem?.isEnabled = enabled - favoriteThisPageMenuItem?.isEnabled = enabled - } - private func setup() { #if !FEEDBACK diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index c903ff312d..9536d45296 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -121,7 +121,7 @@ extension MainViewController { selectedTabViewModel.tab.reload() } - @IBAction func stopLoading(_ sender: Any) { + @IBAction func stopLoadingPage(_ sender: Any) { guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { os_log("MainViewController: No tab view model selected", type: .error) return @@ -130,6 +130,33 @@ extension MainViewController { selectedTabViewModel.tab.stopLoading() } + @IBAction func zoomIn(_ sender: Any) { + guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { + os_log("MainViewController: No tab view model selected", type: .error) + return + } + + selectedTabViewModel.tab.webView.zoomIn() + } + + @IBAction func zoomOut(_ sender: Any) { + guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { + os_log("MainViewController: No tab view model selected", type: .error) + return + } + + selectedTabViewModel.tab.webView.zoomOut() + } + + @IBAction func actualSize(_ sender: Any) { + guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else { + os_log("MainViewController: No tab view model selected", type: .error) + return + } + + selectedTabViewModel.tab.webView.magnification = 1.0 + } + // MARK: - History @IBAction func back(_ sender: Any?) { @@ -286,6 +313,46 @@ extension MainViewController { } +extension MainViewController: NSMenuItemValidation { + + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + switch menuItem.action { + // Back/Forward + case #selector(MainViewController.back(_:)): + return tabCollectionViewModel.selectedTabViewModel?.canGoBack == true + case #selector(MainViewController.forward(_:)): + return tabCollectionViewModel.selectedTabViewModel?.canGoForward == true + + case #selector(MainViewController.stopLoadingPage(_:)): + return tabCollectionViewModel.selectedTabViewModel?.isLoading == true + + case #selector(MainViewController.reloadPage(_:)): + return tabCollectionViewModel.selectedTabViewModel?.canReload == true + + // Zoom + case #selector(MainViewController.zoomIn(_:)): + return tabCollectionViewModel.selectedTabViewModel?.tab.webView.canZoomIn == true + case #selector(MainViewController.zoomOut(_:)): + return tabCollectionViewModel.selectedTabViewModel?.tab.webView.canZoomOut == true + case #selector(MainViewController.actualSize(_:)): + return tabCollectionViewModel.selectedTabViewModel?.tab.webView.canZoomToActualSize == true + + // Bookmarks + case #selector(MainViewController.bookmarkThisPage(_:)), + #selector(MainViewController.favoriteThisPage(_:)): + return tabCollectionViewModel.selectedTabViewModel?.canBeBookmarked == true + + // Reopen Last Removed Tab + case #selector(MainViewController.reopenLastClosedTab(_:)): + return tabCollectionViewModel.canInsertLastRemovedTab == true + + default: + return menuItem.isEnabled + } + } + +} + extension MainViewController: FindInPageDelegate { func findInPageNext(_ controller: FindInPageViewController) { diff --git a/DuckDuckGo/NavigationBar/View/OptionsButtonMenu.swift b/DuckDuckGo/NavigationBar/View/OptionsButtonMenu.swift index ca383b647f..e17684e05a 100644 --- a/DuckDuckGo/NavigationBar/View/OptionsButtonMenu.swift +++ b/DuckDuckGo/NavigationBar/View/OptionsButtonMenu.swift @@ -57,6 +57,7 @@ final class OptionsButtonMenu: NSMenu { } let bookmarksMenuItem = NSMenuItem(title: UserText.bookmarks, action: nil, keyEquivalent: "") + let zoomMenuItem = NSMenuItem(title: UserText.zoom, action: nil, keyEquivalent: "") override func update() { self.result = nil @@ -92,7 +93,12 @@ final class OptionsButtonMenu: NSMenu { addItem(emailItem) addItem(NSMenuItem.separator()) - + + zoomMenuItem.submenu = ZoomSubMenu(tabCollectionViewModel: tabCollectionViewModel) + addItem(zoomMenuItem) + + addItem(NSMenuItem.separator()) + bookmarksMenuItem.image = NSImage(named: "Bookmark") addItem(bookmarksMenuItem) @@ -340,3 +346,35 @@ final class EmailOptionsButtonSubMenu: NSMenu { updateMenuItems() } } + +final class ZoomSubMenu: NSMenu { + + init(tabCollectionViewModel: TabCollectionViewModel) { + super.init(title: UserText.zoom) + + updateMenuItems(with: tabCollectionViewModel) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func updateMenuItems(with tabCollectionViewModel: TabCollectionViewModel) { + removeAllItems() + + let fullScreenItem = (NSApplication.shared.mainMenuTyped.toggleFullscreenMenuItem?.copy() as? NSMenuItem)! + addItem(fullScreenItem) + + addItem(.separator()) + + let zoomInItem = (NSApplication.shared.mainMenuTyped.zoomInMenuItem?.copy() as? NSMenuItem)! + addItem(zoomInItem) + + let zoomOutItem = (NSApplication.shared.mainMenuTyped.zoomOutMenuItem?.copy() as? NSMenuItem)! + addItem(zoomOutItem) + + let actualSizeItem = (NSApplication.shared.mainMenuTyped.actualSizeMenuItem?.copy() as? NSMenuItem)! + addItem(actualSizeItem) + } + +}