From 1616ca1c34d50e0a3ba881faa58e02c6886e51a2 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 4 Jun 2021 11:53:26 -0700 Subject: [PATCH] [AltServer] Refactors common NSMenu logic into MenuController --- AltServer/AppDelegate.swift | 66 ++++++--------- AltServer/Base.lproj/Main.storyboard | 6 -- AltServer/MenuController.swift | 115 +++++++++++++++++++++++++++ AltStore.xcodeproj/project.pbxproj | 4 + 4 files changed, 143 insertions(+), 48 deletions(-) create mode 100644 AltServer/MenuController.swift diff --git a/AltServer/AppDelegate.swift b/AltServer/AppDelegate.swift index a171fdf1..b1f455e5 100644 --- a/AltServer/AppDelegate.swift +++ b/AltServer/AppDelegate.swift @@ -21,6 +21,8 @@ private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/alts private let altstoreAppURL = URL(string: "https://f000.backblazeb2.com/file/altstore/altstore.ipa")! #endif +extension ALTDevice: MenuDisplayable {} + @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { @@ -41,6 +43,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { private weak var authenticationAppleIDTextField: NSTextField? private weak var authenticationPasswordTextField: NSSecureTextField? + private var connectedDevicesMenuController: MenuController! + private var sideloadIPAConnectedDevicesMenuController: MenuController! + func applicationDidFinishLaunching(_ aNotification: Notification) { UserDefaults.standard.registerDefaults() @@ -56,8 +61,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.statusItem = item self.appMenu.delegate = self - self.connectedDevicesMenu.delegate = self - self.sideloadIPAConnectedDevicesMenu.delegate = self + + let placeholder = NSLocalizedString("No Connected Devices", comment: "") + + self.connectedDevicesMenuController = MenuController(menu: self.connectedDevicesMenu, items: []) + self.connectedDevicesMenuController.placeholder = placeholder + self.connectedDevicesMenuController.action = { [weak self] device in + self?.installAltStore(to: device) + } + + self.sideloadIPAConnectedDevicesMenuController = MenuController(menu: self.sideloadIPAConnectedDevicesMenu, items: []) + self.sideloadIPAConnectedDevicesMenuController.placeholder = placeholder + self.sideloadIPAConnectedDevicesMenuController.action = { [weak self] device in + self?.sideloadIPA(to: device) + } UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { (success, error) in guard success else { return } @@ -89,20 +106,13 @@ class AppDelegate: NSObject, NSApplicationDelegate { private extension AppDelegate { - @objc func installAltStore(_ item: NSMenuItem) + @objc func installAltStore(to device: ALTDevice) { - guard let index = item.menu?.index(of: item), index != -1 else { return } - - let device = self.connectedDevices[index] self.installApplication(at: altstoreAppURL, to: device) } - @objc func sideloadIPA(_ item: NSMenuItem) + @objc func sideloadIPA(to device: ALTDevice) { - guard let index = item.menu?.index(of: item), index != -1 else { return } - - let device = self.connectedDevices[index] - let openPanel = NSOpenPanel() openPanel.canChooseDirectories = false openPanel.allowsMultipleSelection = false @@ -303,6 +313,9 @@ extension AppDelegate: NSMenuDelegate guard menu == self.appMenu else { return } self.connectedDevices = ALTDeviceManager.shared.availableDevices + + self.connectedDevicesMenuController.items = self.connectedDevices + self.sideloadIPAConnectedDevicesMenuController.items = self.connectedDevices self.launchAtLoginMenuItem.target = self self.launchAtLoginMenuItem.action = #selector(AppDelegate.toggleLaunchAtLogin(_:)) @@ -323,37 +336,6 @@ extension AppDelegate: NSMenuDelegate self.installMailPluginMenuItem.target = self self.installMailPluginMenuItem.action = #selector(AppDelegate.handleInstallMailPluginMenuItem(_:)) } - - func numberOfItems(in menu: NSMenu) -> Int - { - guard menu == self.connectedDevicesMenu || menu == self.sideloadIPAConnectedDevicesMenu else { return -1 } - - return self.connectedDevices.isEmpty ? 1 : self.connectedDevices.count - } - - func menu(_ menu: NSMenu, update item: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool - { - guard menu == self.connectedDevicesMenu || menu == self.sideloadIPAConnectedDevicesMenu else { return false } - - if self.connectedDevices.isEmpty - { - item.title = NSLocalizedString("No Connected Devices", comment: "") - item.isEnabled = false - item.target = nil - item.action = nil - } - else - { - let device = self.connectedDevices[index] - item.title = device.name - item.isEnabled = true - item.target = self - item.action = (menu == self.connectedDevicesMenu) ? #selector(AppDelegate.installAltStore(_:)) : #selector(AppDelegate.sideloadIPA(_:)) - item.tag = index - } - - return true - } } extension AppDelegate: NSTextFieldDelegate diff --git a/AltServer/Base.lproj/Main.storyboard b/AltServer/Base.lproj/Main.storyboard index 9d6926ab..b37cbc67 100644 --- a/AltServer/Base.lproj/Main.storyboard +++ b/AltServer/Base.lproj/Main.storyboard @@ -93,9 +93,6 @@ - - - @@ -109,9 +106,6 @@ - - - diff --git a/AltServer/MenuController.swift b/AltServer/MenuController.swift new file mode 100644 index 00000000..67715344 --- /dev/null +++ b/AltServer/MenuController.swift @@ -0,0 +1,115 @@ +// +// MenuController.swift +// AltServer +// +// Created by Riley Testut on 3/3/21. +// Copyright © 2021 Riley Testut. All rights reserved. +// + +import Foundation +import AppKit + +protocol MenuDisplayable +{ + var name: String { get } +} + +class MenuController: NSObject, NSMenuDelegate +{ + let menu: NSMenu + + var items: [T] { + didSet { + self.submenus.removeAll() + self.updateMenu() + } + } + + var placeholder: String? { + didSet { + self.updateMenu() + } + } + + var action: ((T) -> Void)? + + var submenuHandler: ((T) -> NSMenu)? + private var submenus = [T: NSMenu]() + + init(menu: NSMenu, items: [T]) + { + self.menu = menu + self.items = items + + super.init() + + self.menu.delegate = self + } + + @objc + private func performAction(_ menuItem: NSMenuItem) + { + guard case let index = self.menu.index(of: menuItem), index != -1 else { return } + + let item = self.items[index] + self.action?(item) + } + + @objc + func numberOfItems(in menu: NSMenu) -> Int + { + let numberOfItems = (self.items.isEmpty && self.placeholder != nil) ? 1 : self.items.count + return numberOfItems + } + + @objc + func menu(_ menu: NSMenu, update menuItem: NSMenuItem, at index: Int, shouldCancel: Bool) -> Bool + { + if let text = self.placeholder, self.items.isEmpty + { + menuItem.title = text + menuItem.isEnabled = false + menuItem.target = nil + menuItem.action = nil + } + else + { + let item = self.items[index] + + menuItem.title = item.name + menuItem.isEnabled = true + menuItem.target = self + menuItem.action = #selector(MenuController.performAction(_:)) + menuItem.tag = index + + if let submenu = self.submenus[item] ?? self.submenuHandler?(item) + { + menuItem.submenu = submenu + + // Cache submenu to prevent duplicate calls to submenuHandler. + self.submenus[item] = submenu + } + } + + return true + } +} + +private extension MenuController +{ + func updateMenu() + { + self.menu.removeAllItems() + + let numberOfItems = self.numberOfItems(in: self.menu) + for index in 0 ..< numberOfItems + { + let menuItem = NSMenuItem(title: "", action: nil, keyEquivalent: "") + guard self.menu(self.menu, update: menuItem, at: index, shouldCancel: false) else { break } + + self.menu.addItem(menuItem) + } + + self.menu.update() + } +} diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 23838b95..103952d6 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -332,6 +332,7 @@ BFF00D302501BD7D00746320 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = BFF00D2F2501BD7D00746320 /* Intents.intentdefinition */; }; BFF00D322501BDA100746320 /* BackgroundRefreshAppsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */; }; BFF00D342501BDCF00746320 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF00D332501BDCF00746320 /* IntentHandler.swift */; }; + BFF0394B25F0551600BE607D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0394A25F0551600BE607D /* MenuController.swift */; }; BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; }; BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; }; BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */; }; @@ -775,6 +776,7 @@ BFF00D2F2501BD7D00746320 /* Intents.intentdefinition */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.intentdefinition; path = Intents.intentdefinition; sourceTree = ""; }; BFF00D312501BDA100746320 /* BackgroundRefreshAppsOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundRefreshAppsOperation.swift; sourceTree = ""; }; BFF00D332501BDCF00746320 /* IntentHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + BFF0394A25F0551600BE607D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = ""; }; BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = ""; }; BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = ""; }; BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = ""; }; @@ -975,6 +977,7 @@ BFE48974238007CE003239E0 /* AnisetteDataManager.swift */, BFC712BA2512B9CF00AB5EBE /* PluginManager.swift */, BFAD67A225E0854500D4C4D1 /* DeveloperDiskManager.swift */, + BFF0394A25F0551600BE607D /* MenuController.swift */, BF703195229F36FF006E110F /* Devices */, BFD52BDC22A0A659000B7ED1 /* Connections */, BF055B4A233B528B0086DEA9 /* Extensions */, @@ -2280,6 +2283,7 @@ buildActionMask = 2147483647; files = ( BFF767C82489A74E0097E58C /* WirelessConnectionHandler.swift in Sources */, + BFF0394B25F0551600BE607D /* MenuController.swift in Sources */, BFECAC8024FD950B0077C41F /* ConnectionManager.swift in Sources */, BFECAC8324FD950B0077C41F /* NetworkConnection.swift in Sources */, BF541C0B25E5A5FA00CD46B2 /* FileManager+URLs.swift in Sources */,