From 1671d0337ff1c6114a21a243bfedeb84abd9891b Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Wed, 11 Oct 2023 21:28:21 +0200 Subject: [PATCH 1/9] Refactoring Menu.populate() func * populate() is split into smaller functions to handle different parts of the logic: - filter function returns list of devices for particular platform - createMenuItem create new menu item for device --- MiniSim/Menu.swift | 80 +++++++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/MiniSim/Menu.swift b/MiniSim/Menu.swift index a8ba0c2..017f0bf 100644 --- a/MiniSim/Menu.swift +++ b/MiniSim/Menu.swift @@ -14,7 +14,7 @@ class Menu: NSMenu { var devices: [Device] = [] { didSet { - populateDevices(isFirst: oldValue.isEmpty) + populateDevices() assignKeyEquivalents() } willSet { @@ -52,8 +52,8 @@ class Menu: NSMenu { } private func removeMenuItems(removedDevices: Set) { - let itemsToRemove = self.items.filter({ removedDevices.contains($0.title) }) - itemsToRemove.forEach(safeRemoveItem) + self.items.filter({ removedDevices.contains($0.title) }) + .forEach(safeRemoveItem) } @objc private func androidSubMenuClick(_ sender: NSMenuItem) { @@ -121,36 +121,58 @@ class Menu: NSMenu { } } - - private func populateDevices(isFirst: Bool) { + // MARK: Populate sections + private func populateDevices() { let sortedDevices = devices.sorted(by: { $0.platform == .android && $1.platform == .ios }) - for (index, device) in sortedDevices.enumerated() { - let isAndroid = device.platform == .android - if let itemIndex = items.firstIndex(where: { $0.title == device.displayName }) { - let item = self.items.get(at: itemIndex) - item?.state = device.booted ? .on : .off - item?.submenu = isAndroid ? - self.populateAndroidSubMenu(booted: device.booted) : - self.populateIOSSubMenu(booted: device.booted) - continue + let platformSections: [MenuSections] = [.iOSHeader, .androidHeader] + for section in platformSections { + let devices = filter(devices: sortedDevices, for: section) + let menuItems = devices.map { createMenuItem(for: $0) } + self.updateSection(with: Array(menuItems), section: section) + } + } + + private func filter(devices: [Device], for section: MenuSections) -> [Device] { + let platform: Platform = section == .iOSHeader ? .ios : .android + return devices.filter { $0.platform == platform } + } + + private func updateSection(with items: [NSMenuItem], section: MenuSections) { + guard let header = self.items.first(where: { $0.tag == section.rawValue }), + let startIndex = self.items.firstIndex(of: header) else { + return + } + + var count = 0 + items.forEach { menuItem in + count += 1 + if let itemIndex = self.items.firstIndex(where: { $0.title == menuItem.title }) { + self.replaceMenuItem(at: itemIndex, with: menuItem) + return } - - let menuItem = NSMenuItem( - title: device.displayName, - action: #selector(deviceItemClick), - keyEquivalent: "", - type: isAndroid ? .launchAndroid : .launchIOS - ) - - menuItem.target = self - menuItem.keyEquivalentModifierMask = isAndroid ? [.option] : [.command] - menuItem.submenu = isAndroid ? populateAndroidSubMenu(booted: device.booted) : populateIOSSubMenu(booted: device.booted) - menuItem.state = device.booted ? .on : .off - - let iosDevicesCount = self.devices.filter({ $0.platform == .ios }).count - self.safeInsertItem(menuItem, at: isAndroid && UserDefaults.standard.enableiOSSimulators ? (isFirst ? index : iosDevicesCount) + 3 : 1) + self.safeInsertItem(menuItem, at: startIndex + count) } } + + private func createMenuItem(for device: Device) -> NSMenuItem { + let menuItem = NSMenuItem( + title: device.displayName, + action: #selector(deviceItemClick), + keyEquivalent: "", + type: device.platform == .ios ? .launchIOS : .launchAndroid + ) + + menuItem.target = self + menuItem.keyEquivalentModifierMask = [.command] + menuItem.submenu = device.platform == .ios ? populateIOSSubMenu(booted: device.booted) : populateAndroidSubMenu(booted: device.booted) + menuItem.state = device.booted ? .on : .off + return menuItem + } + + private func replaceMenuItem(at index: Int, with newItem: NSMenuItem) { + self.removeItem(at: index) + self.insertItem(newItem, at: index) + } private func populateAndroidSubMenu(booted: Bool) -> NSMenu { let subMenu = NSMenu() From decedff058dbabefb3504c41ac37ccc10570f83b Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Wed, 11 Oct 2023 23:01:31 +0200 Subject: [PATCH 2/9] Unifying Android and iOS sub menu building * SubMenuItem protocol is introduced to provide generic type for submenus * Android and iOS submenu types are updated to conform to new protocol * NSMenuItem extension is added to provide convinience init for sub menu items as well as custom command menu items * Menu is updated to use new types and better building flow --- MiniSim.xcodeproj/project.pbxproj | 8 ++ .../NSMenuItem+ConvenienceInit.swift | 44 +++++++ MiniSim/Menu.swift | 124 ++++++++++-------- MiniSim/MenuItems/AndroidSubMenuItem.swift | 15 +-- MiniSim/MenuItems/IOSSubMenuItem.swift | 17 ++- MiniSim/MenuItems/SubMenuItem.swift | 18 +++ 6 files changed, 151 insertions(+), 75 deletions(-) create mode 100644 MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift create mode 100644 MiniSim/MenuItems/SubMenuItem.swift diff --git a/MiniSim.xcodeproj/project.pbxproj b/MiniSim.xcodeproj/project.pbxproj index 36de1eb..418914e 100644 --- a/MiniSim.xcodeproj/project.pbxproj +++ b/MiniSim.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 52B363EA2AEC0D3D006F515C /* ParametersTableViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B363E92AEC0D3D006F515C /* ParametersTableViewModel.swift */; }; 52B363EC2AEC10A3006F515C /* ParametersTableForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B363EB2AEC10A3006F515C /* ParametersTableForm.swift */; }; 52B363EE2AEC10B3006F515C /* ParametersTableFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B363ED2AEC10B3006F515C /* ParametersTableFormViewModel.swift */; }; + 4AFACC742AD730BE00EC369F /* SubMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFACC732AD730BE00EC369F /* SubMenuItem.swift */; }; + 4AFACC762AD73D7900EC369F /* NSMenuItem+ConvenienceInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFACC752AD73D7900EC369F /* NSMenuItem+ConvenienceInit.swift */; }; 76059BF52AD4361C0008D38B /* SetupPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF42AD4361C0008D38B /* SetupPreferences.swift */; }; 76059BF72AD449DC0008D38B /* OnboardingHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF62AD449DC0008D38B /* OnboardingHeader.swift */; }; 76059BF92AD558C30008D38B /* SetupItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF82AD558C30008D38B /* SetupItemView.swift */; }; @@ -85,6 +87,8 @@ 52B363E92AEC0D3D006F515C /* ParametersTableViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParametersTableViewModel.swift; sourceTree = ""; }; 52B363EB2AEC10A3006F515C /* ParametersTableForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParametersTableForm.swift; sourceTree = ""; }; 52B363ED2AEC10B3006F515C /* ParametersTableFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParametersTableFormViewModel.swift; sourceTree = ""; }; + 4AFACC732AD730BE00EC369F /* SubMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubMenuItem.swift; sourceTree = ""; }; + 4AFACC752AD73D7900EC369F /* NSMenuItem+ConvenienceInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+ConvenienceInit.swift"; sourceTree = ""; }; 76059BF42AD4361C0008D38B /* SetupPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPreferences.swift; sourceTree = ""; }; 76059BF62AD449DC0008D38B /* OnboardingHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeader.swift; sourceTree = ""; }; 76059BF82AD558C30008D38B /* SetupItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupItemView.swift; sourceTree = ""; }; @@ -195,6 +199,7 @@ 76FCABAA29B390D5003BBF9A /* Collection+get.swift */, 76E4451329D4403F00039025 /* NSNotificationName.swift */, 767F713129D574EF004159A6 /* UNUserNotificationCenter+showNotification.swift */, + 4AFACC752AD73D7900EC369F /* NSMenuItem+ConvenienceInit.swift */, ); path = Extensions; sourceTree = ""; @@ -334,6 +339,7 @@ 7630B25D2984339100D8B57D /* MenuSections.swift */, 76D305712994155C005C373B /* IOSSubMenuItem.swift */, 76F2A91829924242002D4EF6 /* AndroidSubMenuItem.swift */, + 4AFACC732AD730BE00EC369F /* SubMenuItem.swift */, ); path = MenuItems; sourceTree = ""; @@ -494,6 +500,7 @@ 76630F0C29BDD0C000FB64F9 /* Devices.swift in Sources */, 767C761F29B26ED3009B9AEC /* AccessibilityElement.swift in Sources */, 7610992F2A3F95D90067885A /* NSScriptCommand+utils.swift in Sources */, + 4AFACC762AD73D7900EC369F /* NSMenuItem+ConvenienceInit.swift in Sources */, 76F269872A2A39D100424BDA /* Variables.swift in Sources */, 764BA3EB2A5AD43F003A78AF /* LaunchDeviceCommand.swift in Sources */, 7630B2732986C68000D8B57D /* PaneIdentifier.swift in Sources */, @@ -502,6 +509,7 @@ 76FCABAB29B390D5003BBF9A /* Collection+get.swift in Sources */, 76E4451229D4391000039025 /* Onboarding.swift in Sources */, 7630B2752986D52900D8B57D /* NSAlert+showError.swift in Sources */, + 4AFACC742AD730BE00EC369F /* SubMenuItem.swift in Sources */, 7630B25E2984339100D8B57D /* MenuSections.swift in Sources */, 76AC9AF62A0EA82C00864A8B /* CustomCommands.swift in Sources */, 76489D5C29BFCA330070EF03 /* OnboardingItem.swift in Sources */, diff --git a/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift b/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift new file mode 100644 index 0000000..8bda0c3 --- /dev/null +++ b/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift @@ -0,0 +1,44 @@ +// +// NSMenuItem+ConvenienceInit.swift +// MiniSim +// +// Created by Anton Kolchunov on 11.10.23. +// + +import Cocoa +import Foundation + +extension NSMenuItem { + convenience init(target: AnyObject, action: Selector) { + self.init() + self.target = target + self.action = action + } + + convenience init( + menuItem: SubMenuItem, + target: AnyObject, + action: Selector + ) { + self.init(target: target, action: action) + self.tag = menuItem.tag + self.image = menuItem.image + self.title = menuItem.title + self.toolTip = menuItem.title + self.target = target + self.action = action + } + + convenience init( + command: Command, + target: AnyObject, + action: Selector + ) { + self.init(target: target, action: action) + self.image = NSImage( + systemSymbolName: command.icon, + accessibilityDescription: command.name + ) + self.title = command.name + } +} diff --git a/MiniSim/Menu.swift b/MiniSim/Menu.swift index 017f0bf..96931f6 100644 --- a/MiniSim/Menu.swift +++ b/MiniSim/Menu.swift @@ -164,7 +164,7 @@ class Menu: NSMenu { menuItem.target = self menuItem.keyEquivalentModifierMask = [.command] - menuItem.submenu = device.platform == .ios ? populateIOSSubMenu(booted: device.booted) : populateAndroidSubMenu(booted: device.booted) + menuItem.submenu = buildSubMenu(for: device) menuItem.state = device.booted ? .on : .off return menuItem } @@ -174,72 +174,60 @@ class Menu: NSMenu { self.insertItem(newItem, at: index) } - private func populateAndroidSubMenu(booted: Bool) -> NSMenu { + func buildSubMenu(for device: Device) -> NSMenu { let subMenu = NSMenu() - for item in AndroidSubMenuItem.allCases { - if item == AndroidSubMenuItem.customCommand { - continue + let platform = device.platform + let callback = platform == .android ? #selector(androidSubMenuClick) : #selector(IOSSubMenuClick) + let actionsSubMenu = createActionsSubMenu( + for: platform.subMenuItems, + isDeviceBooted: device.booted, + callback: callback) + let customCommandSubMenu = createCustomCommandsMenu( + for: platform, + isDeviceBooted: device.booted, + callback: callback) + (actionsSubMenu + customCommandSubMenu).forEach { subMenu.addItem($0) } + return subMenu + } + + func createActionsSubMenu( + for subMenuItems: [SubMenuItem], + isDeviceBooted: Bool, + callback: Selector + ) -> [NSMenuItem] { + subMenuItems.filter { item in + if item.needBootedDevice && !isDeviceBooted { + return false } - let menuItem = item.menuItem - menuItem.target = self - menuItem.action = #selector(androidSubMenuClick) - if item.needBootedDevice && !booted { - continue - } - if item.bootsDevice && booted { - continue + if item.bootsDevice && isDeviceBooted { + return false } - subMenu.addItem(menuItem) - } - - for item in DeviceService.getCustomCommands(platform: .android) { - let menuItem = AndroidSubMenuItem.customCommand.menuItem - menuItem.target = self - menuItem.action = #selector(androidSubMenuClick) - if item.needBootedDevice && !booted { - continue - } - if item.bootsDevice ?? false && booted { - continue + + return true + }.map { item in + if item.isSeparator { + return NSMenuItem.separator() } - menuItem.image = NSImage(systemSymbolName: item.icon, accessibilityDescription: item.name) - menuItem.title = item.name - subMenu.addItem(menuItem) + return NSMenuItem(menuItem: item, target: self, action: callback) } - return subMenu } - private func populateIOSSubMenu(booted: Bool) -> NSMenu { - let subMenu = NSMenu() - for item in IOSSubMenuItem.allCases { - if item == IOSSubMenuItem.customCommand { - continue - } - let menuItem = item.menuItem - menuItem.target = self - menuItem.action = #selector(IOSSubMenuClick) - subMenu.addItem(menuItem) - } - - for item in DeviceService.getCustomCommands(platform: .ios) { - let menuItem = IOSSubMenuItem.customCommand.menuItem - menuItem.target = self - menuItem.action = #selector(IOSSubMenuClick) - if item.needBootedDevice && !booted { - continue - } - if item.bootsDevice ?? false && booted { - continue + func createCustomCommandsMenu(for platform: Platform, isDeviceBooted: Bool, callback: Selector) -> [NSMenuItem] { + DeviceService.getCustomCommands(platform: platform) + .filter { item in + if item.needBootedDevice && !isDeviceBooted { + return false + } + if item.bootsDevice ?? false && isDeviceBooted { + return false + } + return true } - menuItem.image = NSImage(systemSymbolName: item.icon, accessibilityDescription: item.name) - menuItem.title = item.name - subMenu.addItem(menuItem) - } - return subMenu + .map { NSMenuItem(command: $0, target: self, action: callback) } } - private func safeInsertItem(_ item: NSMenuItem, at index: Int) { + private func safeInsertItem(_ item: NSMenuItem, at index: Int) { guard !items.contains(where: {$0.title == item.title}), index <= items.count else { return } @@ -269,3 +257,27 @@ extension Menu: NSMenuDelegate { KeyboardShortcuts.enable(.toggleMiniSim) } } + +extension Platform { + var subMenuItems: [SubMenuItem] { + switch self { + case .android: + return [ + AndroidSubMenuItem.copyName, + AndroidSubMenuItem.copyAdbId, + AndroidSubMenuItem.separator, + AndroidSubMenuItem.coldBootAndroid, + AndroidSubMenuItem.androidNoAudio, + AndroidSubMenuItem.toggleA11yAndroid, + AndroidSubMenuItem.pasteToEmulator + ] + case .ios: + return [ + IOSSubMenuItem.copyName, + IOSSubMenuItem.copyUDID, + IOSSubMenuItem.separator, + IOSSubMenuItem.deleteSim + ] + } + } +} diff --git a/MiniSim/MenuItems/AndroidSubMenuItem.swift b/MiniSim/MenuItems/AndroidSubMenuItem.swift index 29ac6a9..6698a87 100644 --- a/MiniSim/MenuItems/AndroidSubMenuItem.swift +++ b/MiniSim/MenuItems/AndroidSubMenuItem.swift @@ -7,7 +7,7 @@ import Cocoa -enum AndroidSubMenuItem: Int, CaseIterable { +enum AndroidSubMenuItem: Int, CaseIterable, SubMenuItem { case copyName = 100 case copyAdbId = 101 @@ -36,15 +36,10 @@ enum AndroidSubMenuItem: Int, CaseIterable { } } - var menuItem: NSMenuItem { - let item = self == .separator ? NSMenuItem.separator() : NSMenuItem() - item.tag = rawValue - item.image = image - item.title = title - item.toolTip = title - return item - } - + var tag: Int { self.rawValue } + + var isSeparator: Bool { self == .separator } + var title: String { switch self { case .copyName: diff --git a/MiniSim/MenuItems/IOSSubMenuItem.swift b/MiniSim/MenuItems/IOSSubMenuItem.swift index d42d79c..b4f8f97 100644 --- a/MiniSim/MenuItems/IOSSubMenuItem.swift +++ b/MiniSim/MenuItems/IOSSubMenuItem.swift @@ -7,7 +7,7 @@ import Cocoa -enum IOSSubMenuItem: Int, CaseIterable { +enum IOSSubMenuItem: Int, CaseIterable, SubMenuItem { case copyName = 100 case copyUDID = 101 @@ -15,15 +15,14 @@ enum IOSSubMenuItem: Int, CaseIterable { case deleteSim = 103 case customCommand = 1000 - var menuItem: NSMenuItem { - let item = self == .separator ? .separator() : NSMenuItem() - item.tag = rawValue - item.image = image - item.title = title - item.toolTip = title - return item - } + var needBootedDevice: Bool { false } + + var bootsDevice: Bool { false } + var tag: Int { self.rawValue } + + var isSeparator: Bool { self == .separator } + var title: String { switch self { case .copyName: diff --git a/MiniSim/MenuItems/SubMenuItem.swift b/MiniSim/MenuItems/SubMenuItem.swift new file mode 100644 index 0000000..fb4e950 --- /dev/null +++ b/MiniSim/MenuItems/SubMenuItem.swift @@ -0,0 +1,18 @@ +// +// SubMenuItem.swift +// MiniSim +// +// Created by Anton Kolchunov on 11.10.23. +// + +import Cocoa +import Foundation + +protocol SubMenuItem { + var needBootedDevice: Bool { get } + var bootsDevice: Bool { get } + var title: String { get } + var image: NSImage? { get } + var tag: Int { get } + var isSeparator: Bool { get } +} From b577f03ecb207fccfc5f14da7d49e78a8a93bb52 Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Wed, 11 Oct 2023 23:48:07 +0200 Subject: [PATCH 3/9] Refactoring main elements of the menu * MenuSections is replaced with MainMenuActions and DeviceListSection types * MainMenuActions covers general actions like openings settings or quiting app * DeviceListSection is responsible for platform section headers --- MiniSim.xcodeproj/project.pbxproj | 12 ++- .../NSMenuItem+ConvenienceInit.swift | 8 ++ MiniSim/Menu.swift | 25 ++++-- MiniSim/MenuItems/DeviceListSection.swift | 22 +++++ MiniSim/MenuItems/MainMenuActions.swift | 36 +++++++++ MiniSim/MenuItems/MenuSections.swift | 80 ------------------- MiniSim/MiniSim.swift | 66 ++++++++++----- 7 files changed, 138 insertions(+), 111 deletions(-) create mode 100644 MiniSim/MenuItems/DeviceListSection.swift create mode 100644 MiniSim/MenuItems/MainMenuActions.swift delete mode 100644 MiniSim/MenuItems/MenuSections.swift diff --git a/MiniSim.xcodeproj/project.pbxproj b/MiniSim.xcodeproj/project.pbxproj index 418914e..b001526 100644 --- a/MiniSim.xcodeproj/project.pbxproj +++ b/MiniSim.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 52B363EE2AEC10B3006F515C /* ParametersTableFormViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B363ED2AEC10B3006F515C /* ParametersTableFormViewModel.swift */; }; 4AFACC742AD730BE00EC369F /* SubMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFACC732AD730BE00EC369F /* SubMenuItem.swift */; }; 4AFACC762AD73D7900EC369F /* NSMenuItem+ConvenienceInit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFACC752AD73D7900EC369F /* NSMenuItem+ConvenienceInit.swift */; }; + 4AFACC782AD74E9000EC369F /* DeviceListSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFACC772AD74E9000EC369F /* DeviceListSection.swift */; }; 76059BF52AD4361C0008D38B /* SetupPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF42AD4361C0008D38B /* SetupPreferences.swift */; }; 76059BF72AD449DC0008D38B /* OnboardingHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF62AD449DC0008D38B /* OnboardingHeader.swift */; }; 76059BF92AD558C30008D38B /* SetupItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76059BF82AD558C30008D38B /* SetupItemView.swift */; }; @@ -20,7 +21,7 @@ 7625140B2992B46D0060A225 /* Pasteboard+utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7625140A2992B46D0060A225 /* Pasteboard+utils.swift */; }; 762CF1E02981968F00099999 /* String+match.swift in Sources */ = {isa = PBXBuildFile; fileRef = 762CF1DF2981968F00099999 /* String+match.swift */; }; 762CF1E42981DE6100099999 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 762CF1E32981DE6100099999 /* Device.swift */; }; - 7630B25E2984339100D8B57D /* MenuSections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7630B25D2984339100D8B57D /* MenuSections.swift */; }; + 7630B25E2984339100D8B57D /* MainMenuActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7630B25D2984339100D8B57D /* MainMenuActions.swift */; }; 7630B26129853EF400D8B57D /* Preferences in Frameworks */ = {isa = PBXBuildFile; productRef = 7630B26029853EF400D8B57D /* Preferences */; }; 7630B2662985C44A00D8B57D /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7630B2652985C44A00D8B57D /* Preferences.swift */; }; 7630B2682985C4CF00D8B57D /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7630B2672985C4CF00D8B57D /* About.swift */; }; @@ -89,6 +90,7 @@ 52B363ED2AEC10B3006F515C /* ParametersTableFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParametersTableFormViewModel.swift; sourceTree = ""; }; 4AFACC732AD730BE00EC369F /* SubMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubMenuItem.swift; sourceTree = ""; }; 4AFACC752AD73D7900EC369F /* NSMenuItem+ConvenienceInit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+ConvenienceInit.swift"; sourceTree = ""; }; + 4AFACC772AD74E9000EC369F /* DeviceListSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceListSection.swift; sourceTree = ""; }; 76059BF42AD4361C0008D38B /* SetupPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupPreferences.swift; sourceTree = ""; }; 76059BF62AD449DC0008D38B /* OnboardingHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingHeader.swift; sourceTree = ""; }; 76059BF82AD558C30008D38B /* SetupItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupItemView.swift; sourceTree = ""; }; @@ -97,7 +99,7 @@ 7625140A2992B46D0060A225 /* Pasteboard+utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Pasteboard+utils.swift"; sourceTree = ""; }; 762CF1DF2981968F00099999 /* String+match.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+match.swift"; sourceTree = ""; }; 762CF1E32981DE6100099999 /* Device.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; }; - 7630B25D2984339100D8B57D /* MenuSections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuSections.swift; sourceTree = ""; }; + 7630B25D2984339100D8B57D /* MainMenuActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuActions.swift; sourceTree = ""; }; 7630B2652985C44A00D8B57D /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; 7630B2672985C4CF00D8B57D /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; 7630B26C2986B4FD00D8B57D /* KeyboardShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcuts.swift; sourceTree = ""; }; @@ -336,7 +338,8 @@ isa = PBXGroup; children = ( 76730BB8298C1DF80019C680 /* DeviceMenuItem.swift */, - 7630B25D2984339100D8B57D /* MenuSections.swift */, + 7630B25D2984339100D8B57D /* MainMenuActions.swift */, + 4AFACC772AD74E9000EC369F /* DeviceListSection.swift */, 76D305712994155C005C373B /* IOSSubMenuItem.swift */, 76F2A91829924242002D4EF6 /* AndroidSubMenuItem.swift */, 4AFACC732AD730BE00EC369F /* SubMenuItem.swift */, @@ -489,6 +492,7 @@ 7645D4C42982CB2B00019227 /* MiniSim.swift in Sources */, 76F2A912299033EA002D4EF6 /* DeviceError.swift in Sources */, 7631218E2A12B3BA00EE7F48 /* CustomCommandsViewModel.swift in Sources */, + 4AFACC782AD74E9000EC369F /* DeviceListSection.swift in Sources */, 7677999E29C26264009030F8 /* PermissionsView.swift in Sources */, 76630F2B29C718F500FB64F9 /* AndroidPathInput.swift in Sources */, 767F713229D574EF004159A6 /* UNUserNotificationCenter+showNotification.swift in Sources */, @@ -510,7 +514,7 @@ 76E4451229D4391000039025 /* Onboarding.swift in Sources */, 7630B2752986D52900D8B57D /* NSAlert+showError.swift in Sources */, 4AFACC742AD730BE00EC369F /* SubMenuItem.swift in Sources */, - 7630B25E2984339100D8B57D /* MenuSections.swift in Sources */, + 7630B25E2984339100D8B57D /* MainMenuActions.swift in Sources */, 76AC9AF62A0EA82C00864A8B /* CustomCommands.swift in Sources */, 76489D5C29BFCA330070EF03 /* OnboardingItem.swift in Sources */, 7645D5012982E6FA00019227 /* main.swift in Sources */, diff --git a/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift b/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift index 8bda0c3..1e6614e 100644 --- a/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift +++ b/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift @@ -41,4 +41,12 @@ extension NSMenuItem { ) self.title = command.name } + + convenience init(mainMenuItem: MainMenuActions, target: AnyObject, action: Selector) { + self.init(target: target, action: action) + self.tag = mainMenuItem.rawValue + self.keyEquivalent = mainMenuItem.keyEquivalent + self.title = mainMenuItem.title + self.toolTip = mainMenuItem.title + } } diff --git a/MiniSim/Menu.swift b/MiniSim/Menu.swift index 96931f6..7ef4347 100644 --- a/MiniSim/Menu.swift +++ b/MiniSim/Menu.swift @@ -33,7 +33,7 @@ class Menu: NSMenu { self.delegate = self } - func getDevices() { + func updateDevicesList() { let userDefaults = UserDefaults.standard DeviceService.getAllDevices( android: userDefaults.enableAndroidEmulators && userDefaults.androidHome != nil, @@ -90,7 +90,7 @@ class Menu: NSMenu { } private func assignKeyEquivalents() { - let sections = MenuSections.allCases.map {$0.title} + let sections = DeviceListSection.allCases.map {$0.title} let deviceItems = items.filter { !sections.contains($0.title) } let iosDeviceNames = devices.filter({ $0.platform == Platform.ios }).map { $0.displayName } let androidDeviceNames = devices.filter({ $0.platform == Platform.android }).map { $0.displayName } @@ -124,7 +124,7 @@ class Menu: NSMenu { // MARK: Populate sections private func populateDevices() { let sortedDevices = devices.sorted(by: { $0.platform == .android && $1.platform == .ios }) - let platformSections: [MenuSections] = [.iOSHeader, .androidHeader] + let platformSections: [DeviceListSection] = sections for section in platformSections { let devices = filter(devices: sortedDevices, for: section) let menuItems = devices.map { createMenuItem(for: $0) } @@ -132,12 +132,23 @@ class Menu: NSMenu { } } - private func filter(devices: [Device], for section: MenuSections) -> [Device] { - let platform: Platform = section == .iOSHeader ? .ios : .android + var sections: [DeviceListSection] { + var sections: [DeviceListSection] = [] + if UserDefaults.standard.enableAndroidEmulators { + sections.append(.android) + } + if UserDefaults.standard.enableiOSSimulators { + sections.append(.iOS) + } + return sections + } + + private func filter(devices: [Device], for section: DeviceListSection) -> [Device] { + let platform: Platform = section == .iOS ? .ios : .android return devices.filter { $0.platform == platform } } - private func updateSection(with items: [NSMenuItem], section: MenuSections) { + private func updateSection(with items: [NSMenuItem], section: DeviceListSection) { guard let header = self.items.first(where: { $0.tag == section.rawValue }), let startIndex = self.items.firstIndex(of: header) else { return @@ -248,7 +259,7 @@ class Menu: NSMenu { extension Menu: NSMenuDelegate { func menuWillOpen(_ menu: NSMenu) { NotificationCenter.default.post(name: .menuWillOpen, object: nil) - self.getDevices() + self.updateDevicesList() KeyboardShortcuts.disable(.toggleMiniSim) } diff --git a/MiniSim/MenuItems/DeviceListSection.swift b/MiniSim/MenuItems/DeviceListSection.swift new file mode 100644 index 0000000..86769c0 --- /dev/null +++ b/MiniSim/MenuItems/DeviceListSection.swift @@ -0,0 +1,22 @@ +// +// DeviceListSection.swift +// MiniSim +// +// Created by Anton Kolchunov on 11.10.23. +// + +import Foundation + +enum DeviceListSection: Int, CaseIterable { + case iOS = 2000 + case android + + var title: String { + switch self { + case .iOS: + return NSLocalizedString("iOS Simulator", comment: "") + case .android: + return NSLocalizedString("Android Simulator", comment: "") + } + } +} diff --git a/MiniSim/MenuItems/MainMenuActions.swift b/MiniSim/MenuItems/MainMenuActions.swift new file mode 100644 index 0000000..7a89d83 --- /dev/null +++ b/MiniSim/MenuItems/MainMenuActions.swift @@ -0,0 +1,36 @@ +// +// MainMenuActions.swift +// MiniSim +// +// Created by Oskar Kwaśniewski on 27/01/2023. +// + +import Foundation + +enum MainMenuActions: Int, CaseIterable { + case clearDerrivedData = 1000 + case preferences + case quit + + var keyEquivalent: String { + switch self { + case .quit: + return "q" + case .preferences: + return "," + default: + return "" + } + } + + var title: String { + switch self { + case .quit: + return NSLocalizedString("Quit", comment: "") + case .preferences: + return NSLocalizedString("Preferences", comment: "") + case .clearDerrivedData: + return NSLocalizedString("Clear Xcode Derived Data", comment: "") + } + } +} diff --git a/MiniSim/MenuItems/MenuSections.swift b/MiniSim/MenuItems/MenuSections.swift deleted file mode 100644 index ddc0781..0000000 --- a/MiniSim/MenuItems/MenuSections.swift +++ /dev/null @@ -1,80 +0,0 @@ -// -// MenuSections.swift -// MiniSim -// -// Created by Oskar Kwaśniewski on 27/01/2023. -// - -import Cocoa - -enum MenuSections: Int, CaseIterable { - case iOSHeader = 100 - case separator1 = 1 - case androidHeader = 101 - - case separator2 = 2 - case clearDerrivedData = 119 - case preferences = 120 - case quit = 121 - - var menuItem: NSMenuItem { - var item: NSMenuItem! - switch self { - case .separator1, .separator2: - item = NSMenuItem.separator() - case .iOSHeader, .androidHeader: - if #available(macOS 14.0, *) { - item = NSMenuItem.sectionHeader(title: "") - } else { - item = NSMenuItem() - } - default: - item = NSMenuItem() - } - - item.tag = rawValue - item.keyEquivalent = keyEquivalent - item.title = title - item.toolTip = title - return item - } - - var attachItem: Bool { - switch self { - case .iOSHeader, .separator1, .clearDerrivedData: - return UserDefaults.standard.enableiOSSimulators - case .androidHeader, .separator2: - return UserDefaults.standard.enableAndroidEmulators - default: - return true - } - } - - var keyEquivalent: String { - switch self { - case .quit: - return "q" - case .preferences: - return "," - default: - return "" - } - } - - var title: String { - switch self { - case .iOSHeader: - return NSLocalizedString("iOS Simulator", comment: "") - case .androidHeader: - return NSLocalizedString("Android Simulator", comment: "") - case .quit: - return NSLocalizedString("Quit", comment: "") - case .preferences: - return NSLocalizedString("Preferences", comment: "") - case .clearDerrivedData: - return NSLocalizedString("Clear Xcode Derived Data", comment: "") - default: - return "" - } - } -} diff --git a/MiniSim/MiniSim.swift b/MiniSim/MiniSim.swift index ccd8f04..a5daa81 100644 --- a/MiniSim/MiniSim.swift +++ b/MiniSim/MiniSim.swift @@ -84,9 +84,14 @@ class MiniSim: NSObject { menu = Menu() statusItem.menu = menu setMenuImage() - populateSections() - menu.getDevices() + guard menu.items.isEmpty else { + menu.updateDevicesList() + return + } + + (devicesListHeaders + mainMenu).forEach { menu.addItem($0) } + menu.updateDevicesList() } private func initObservers() { @@ -133,11 +138,11 @@ class MiniSim: NSObject { } @objc private func handleDeviceDeleted() { - menu.getDevices() + menu.updateDevicesList() } @objc func menuItemAction(_ sender: NSMenuItem) { - if let tag = MenuSections(rawValue: sender.tag) { + if let tag = MainMenuActions(rawValue: sender.tag) { switch tag { case .preferences: settingsController.show() @@ -157,29 +162,50 @@ class MiniSim: NSObject { UNUserNotificationCenter.showNotification(title: "Derived data has been cleared!", body: "Removed \(amountCleared) of data") NotificationCenter.default.post(name: .commandDidSucceed, object: nil) } - default: - break } } } - private func populateSections() { - if !menu.items.isEmpty { - return + private var devicesListHeaders: [NSMenuItem] { + var sections: [DeviceListSection] = [] + if UserDefaults.standard.enableiOSSimulators { + sections.append(.iOS) } - MenuSections.allCases.forEach { item in - if (!item.attachItem) { - return - } - - let menuItem = item.menuItem - if menuItem.tag >= MenuSections.clearDerrivedData.rawValue { - menuItem.action = #selector(menuItemAction) - menuItem.target = self + + if UserDefaults.standard.enableAndroidEmulators { + sections.append(.android) + } + + guard sections.count > 1 else { + return [] + } + + var menuItems: [NSMenuItem] = [] + + sections.forEach { section in + var menuItem: NSMenuItem + if #available(macOS 14.0, *) { + menuItem = NSMenuItem.sectionHeader(title: "") } else { - menuItem.isEnabled = false + menuItem = NSMenuItem() } - menu.addItem(menuItem) + menuItem.tag = section.rawValue + menuItem.title = section.title + menuItem.toolTip = section.title + + menuItems.append(menuItem) + menuItems.append(NSMenuItem.separator()) + } + return menuItems + } + + private var mainMenu: [NSMenuItem] { + MainMenuActions.allCases.map { item in + NSMenuItem( + mainMenuItem: item, + target: self, + action: #selector(menuItemAction) + ) } } } From 94dbf5b64c233f4ff2fd045e19250f65eb87a913 Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Mon, 30 Oct 2023 22:56:51 +0100 Subject: [PATCH 4/9] Adjusting tags --- MiniSim/MenuItems/AndroidSubMenuItem.swift | 14 +++++++------- MiniSim/MenuItems/DeviceListSection.swift | 2 +- MiniSim/MenuItems/IOSSubMenuItem.swift | 8 ++++---- MiniSim/MenuItems/MainMenuActions.swift | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/MiniSim/MenuItems/AndroidSubMenuItem.swift b/MiniSim/MenuItems/AndroidSubMenuItem.swift index 6698a87..209f995 100644 --- a/MiniSim/MenuItems/AndroidSubMenuItem.swift +++ b/MiniSim/MenuItems/AndroidSubMenuItem.swift @@ -10,13 +10,13 @@ import Cocoa enum AndroidSubMenuItem: Int, CaseIterable, SubMenuItem { case copyName = 100 - case copyAdbId = 101 - case separator = 102 - case coldBootAndroid = 103 - case androidNoAudio = 104 - case toggleA11yAndroid = 105 - case pasteToEmulator = 106 - case customCommand = 1000 + case copyAdbId + case separator + case coldBootAndroid + case androidNoAudio + case toggleA11yAndroid + case pasteToEmulator + case customCommand = 200 var needBootedDevice: Bool { switch self { diff --git a/MiniSim/MenuItems/DeviceListSection.swift b/MiniSim/MenuItems/DeviceListSection.swift index 86769c0..8e74ca6 100644 --- a/MiniSim/MenuItems/DeviceListSection.swift +++ b/MiniSim/MenuItems/DeviceListSection.swift @@ -8,7 +8,7 @@ import Foundation enum DeviceListSection: Int, CaseIterable { - case iOS = 2000 + case iOS = 100 case android var title: String { diff --git a/MiniSim/MenuItems/IOSSubMenuItem.swift b/MiniSim/MenuItems/IOSSubMenuItem.swift index b4f8f97..4e72156 100644 --- a/MiniSim/MenuItems/IOSSubMenuItem.swift +++ b/MiniSim/MenuItems/IOSSubMenuItem.swift @@ -10,10 +10,10 @@ import Cocoa enum IOSSubMenuItem: Int, CaseIterable, SubMenuItem { case copyName = 100 - case copyUDID = 101 - case separator = 102 - case deleteSim = 103 - case customCommand = 1000 + case copyUDID + case separator + case deleteSim + case customCommand = 200 var needBootedDevice: Bool { false } diff --git a/MiniSim/MenuItems/MainMenuActions.swift b/MiniSim/MenuItems/MainMenuActions.swift index 7a89d83..404aa6c 100644 --- a/MiniSim/MenuItems/MainMenuActions.swift +++ b/MiniSim/MenuItems/MainMenuActions.swift @@ -8,7 +8,7 @@ import Foundation enum MainMenuActions: Int, CaseIterable { - case clearDerrivedData = 1000 + case clearDerrivedData = 200 case preferences case quit From 5ebe102411cd68c3974dc0bd4e777c5edd3e71bf Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Sun, 29 Oct 2023 22:07:02 +0100 Subject: [PATCH 5/9] Refactoring submenu types * Android and iOS specific sub menu types are removed * New system with structs instead of enum values is added --- MiniSim.xcodeproj/project.pbxproj | 8 -- .../AppleScript Commands/ExecuteCommand.swift | 26 ++-- .../AppleScript Commands/GetCommands.swift | 58 +++++--- .../NSMenuItem+ConvenienceInit.swift | 2 +- MiniSim/Menu.swift | 48 +++---- MiniSim/MenuItems/AndroidSubMenuItem.swift | 87 ----------- MiniSim/MenuItems/IOSSubMenuItem.swift | 61 -------- MiniSim/MenuItems/SubMenuItem.swift | 135 +++++++++++++++++- MiniSim/Service/DeviceService.swift | 24 ++-- 9 files changed, 209 insertions(+), 240 deletions(-) delete mode 100644 MiniSim/MenuItems/AndroidSubMenuItem.swift delete mode 100644 MiniSim/MenuItems/IOSSubMenuItem.swift diff --git a/MiniSim.xcodeproj/project.pbxproj b/MiniSim.xcodeproj/project.pbxproj index b001526..b6b248b 100644 --- a/MiniSim.xcodeproj/project.pbxproj +++ b/MiniSim.xcodeproj/project.pbxproj @@ -69,7 +69,6 @@ 768F8EC829954C8A00DFBCDB /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 768F8EC729954C8A00DFBCDB /* Sparkle */; }; 76AC9AF62A0EA82C00864A8B /* CustomCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76AC9AF52A0EA82C00864A8B /* CustomCommands.swift */; }; 76AC9AF92A0EB50800864A8B /* SymbolPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 76AC9AF82A0EB50800864A8B /* SymbolPicker */; }; - 76D305722994155C005C373B /* IOSSubMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76D305712994155C005C373B /* IOSSubMenuItem.swift */; }; 76E4451229D4391000039025 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E4451129D4391000039025 /* Onboarding.swift */; }; 76E4451429D4403F00039025 /* NSNotificationName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76E4451329D4403F00039025 /* NSNotificationName.swift */; }; 76F04A11298A5AE000BF9CA3 /* ADB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F04A10298A5AE000BF9CA3 /* ADB.swift */; }; @@ -80,7 +79,6 @@ 76F2A912299033EA002D4EF6 /* DeviceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F2A911299033EA002D4EF6 /* DeviceError.swift */; }; 76F2A914299050F9002D4EF6 /* UserDefaults+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F2A913299050F9002D4EF6 /* UserDefaults+Configuration.swift */; }; 76F2A9172991B7B6002D4EF6 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F2A9162991B7B6002D4EF6 /* ViewModifiers.swift */; }; - 76F2A91929924242002D4EF6 /* AndroidSubMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76F2A91829924242002D4EF6 /* AndroidSubMenuItem.swift */; }; 76FCABAB29B390D5003BBF9A /* Collection+get.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76FCABAA29B390D5003BBF9A /* Collection+get.swift */; }; /* End PBXBuildFile section */ @@ -144,7 +142,6 @@ 7684FAAE29D202F500230BB0 /* AndroidHomeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AndroidHomeError.swift; sourceTree = ""; }; 768F8ECC2995575B00DFBCDB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 76AC9AF52A0EA82C00864A8B /* CustomCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCommands.swift; sourceTree = ""; }; - 76D305712994155C005C373B /* IOSSubMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSSubMenuItem.swift; sourceTree = ""; }; 76E4451129D4391000039025 /* Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = ""; }; 76E4451329D4403F00039025 /* NSNotificationName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSNotificationName.swift; sourceTree = ""; }; 76F04A10298A5AE000BF9CA3 /* ADB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ADB.swift; sourceTree = ""; }; @@ -154,7 +151,6 @@ 76F2A911299033EA002D4EF6 /* DeviceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceError.swift; sourceTree = ""; }; 76F2A913299050F9002D4EF6 /* UserDefaults+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Configuration.swift"; sourceTree = ""; }; 76F2A9162991B7B6002D4EF6 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = ""; }; - 76F2A91829924242002D4EF6 /* AndroidSubMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AndroidSubMenuItem.swift; sourceTree = ""; }; 76FCABAA29B390D5003BBF9A /* Collection+get.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+get.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -340,8 +336,6 @@ 76730BB8298C1DF80019C680 /* DeviceMenuItem.swift */, 7630B25D2984339100D8B57D /* MainMenuActions.swift */, 4AFACC772AD74E9000EC369F /* DeviceListSection.swift */, - 76D305712994155C005C373B /* IOSSubMenuItem.swift */, - 76F2A91829924242002D4EF6 /* AndroidSubMenuItem.swift */, 4AFACC732AD730BE00EC369F /* SubMenuItem.swift */, ); path = MenuItems; @@ -487,7 +481,6 @@ 767A9CC929C320ED00554193 /* OnboardingButton.swift in Sources */, 7677999C29C26240009030F8 /* SetupView.swift in Sources */, 76F269852A2A376A00424BDA /* CustomCommandError.swift in Sources */, - 76D305722994155C005C373B /* IOSSubMenuItem.swift in Sources */, 767799A029C30BF5009030F8 /* BlurredView.swift in Sources */, 7645D4C42982CB2B00019227 /* MiniSim.swift in Sources */, 76F2A912299033EA002D4EF6 /* DeviceError.swift in Sources */, @@ -535,7 +528,6 @@ 763121872A12AF3400EE7F48 /* CustomCommandForm.swift in Sources */, 762CF1E42981DE6100099999 /* Device.swift in Sources */, 7645D5032983186100019227 /* NSMenuItem+ImageInit.swift in Sources */, - 76F2A91929924242002D4EF6 /* AndroidSubMenuItem.swift in Sources */, 764BA3ED2A5AD478003A78AF /* GetCommands.swift in Sources */, 7630B26D2986B4FD00D8B57D /* KeyboardShortcuts.swift in Sources */, 76059BF52AD4361C0008D38B /* SetupPreferences.swift in Sources */, diff --git a/MiniSim/AppleScript Commands/ExecuteCommand.swift b/MiniSim/AppleScript Commands/ExecuteCommand.swift index c549abb..d3f0e55 100644 --- a/MiniSim/AppleScript Commands/ExecuteCommand.swift +++ b/MiniSim/AppleScript Commands/ExecuteCommand.swift @@ -25,20 +25,22 @@ class ExecuteCommand: NSScriptCommand { let device = Device(name: deviceName, ID: deviceId, platform: platform) let rawTag = Int(tag) ?? 0 - if platform == .android { - if let menuItem = AndroidSubMenuItem(rawValue: rawTag) { - DeviceService.handleAndroidAction(device: device, commandTag: menuItem, itemName: commandName) - } - - return nil; - } - - - if let menuItem = IOSSubMenuItem(rawValue: rawTag) { - DeviceService.handleiOSAction(device: device, commandTag: menuItem, itemName: commandName) + guard let menuItem = SubMenuItems.Tags(rawValue: rawTag) else { + return nil } + platform == .android ? + DeviceService.handleAndroidAction( + device: device, + commandTag: menuItem, + itemName: commandName + ) : + DeviceService.handleiOSAction( + device: device, + commandTag: menuItem, + itemName: commandName + ) - return nil; + return Never.self; } } diff --git a/MiniSim/AppleScript Commands/GetCommands.swift b/MiniSim/AppleScript Commands/GetCommands.swift index 194dcb6..151cb9b 100644 --- a/MiniSim/AppleScript Commands/GetCommands.swift +++ b/MiniSim/AppleScript Commands/GetCommands.swift @@ -14,34 +14,46 @@ class GetCommands: NSScriptCommand { let argument = self.property(forKey: "platform") as? String, let platform = Platform(rawValue: argument) else { - scriptErrorNumber = NSRequiredArgumentsMissingScriptError; - return nil; + scriptErrorNumber = NSRequiredArgumentsMissingScriptError + return nil } - do { - var commands: [Command] = [] - - switch platform { - case .android: - commands = AndroidSubMenuItem.allCases.compactMap { $0.CommandItem } - case .ios: - commands = IOSSubMenuItem.allCases.compactMap { $0.CommandItem } + let commands = platform.subMenuItems + .compactMap { $0 as? SubMenuActionItem } + .map { $0.commandItem } + let customCommands = DeviceService.getCustomCommands(platform: platform) + .map { command in + Command( + id: command.id, + name: command.name, + command: command.command, + icon: command.icon, + platform: command.platform, + needBootedDevice: command.needBootedDevice, + bootsDevice: command.bootsDevice, + tag: SubMenuItems.Tags.customCommand.rawValue + ) } - - let customCommandTag = platform == .android ? AndroidSubMenuItem.customCommand.rawValue : IOSSubMenuItem.customCommand.rawValue - - let customCommands = DeviceService.getCustomCommands(platform: platform).map({ - Command(id: $0.id, name: $0.name, command: $0.command, icon: $0.icon, platform: $0.platform, needBootedDevice: $0.needBootedDevice, bootsDevice: $0.bootsDevice, tag: customCommandTag) - }) - - commands.append(contentsOf: customCommands) - - return try self.encode(commands) - + + do { + return try self.encode(commands + customCommands) } catch { - scriptErrorNumber = NSInternalScriptError; + scriptErrorNumber = NSInternalScriptError return nil } - + } +} + +extension SubMenuActionItem { + var commandItem: Command { + Command( + name: self.title, + command: "", + icon: "", + platform: Platform.android, + needBootedDevice: needBootedDevice, + bootsDevice: self.bootsDevice, + tag: self.tag + ) } } diff --git a/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift b/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift index 1e6614e..3f2f5f5 100644 --- a/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift +++ b/MiniSim/Extensions/NSMenuItem+ConvenienceInit.swift @@ -16,7 +16,7 @@ extension NSMenuItem { } convenience init( - menuItem: SubMenuItem, + menuItem: SubMenuActionItem, target: AnyObject, action: Selector ) { diff --git a/MiniSim/Menu.swift b/MiniSim/Menu.swift index 7ef4347..190350e 100644 --- a/MiniSim/Menu.swift +++ b/MiniSim/Menu.swift @@ -57,14 +57,14 @@ class Menu: NSMenu { } @objc private func androidSubMenuClick(_ sender: NSMenuItem) { - guard let tag = AndroidSubMenuItem(rawValue: sender.tag) else { return } + guard let tag = SubMenuItems.Tags(rawValue: sender.tag) else { return } guard let device = getDeviceByName(name: sender.parent?.title ?? "") else { return } DeviceService.handleAndroidAction(device: device, commandTag: tag, itemName: sender.title) } @objc private func IOSSubMenuClick(_ sender: NSMenuItem) { - guard let tag = IOSSubMenuItem(rawValue: sender.tag) else { return } + guard let tag = SubMenuItems.Tags(rawValue: sender.tag) else { return } guard let device = getDeviceByName(name: sender.parent?.title ?? "") else { return } DeviceService.handleiOSAction(device: device, commandTag: tag, itemName: sender.title) @@ -206,21 +206,22 @@ class Menu: NSMenu { isDeviceBooted: Bool, callback: Selector ) -> [NSMenuItem] { - subMenuItems.filter { item in - if item.needBootedDevice && !isDeviceBooted { - return false - } - - if item.bootsDevice && isDeviceBooted { - return false - } - - return true - }.map { item in - if item.isSeparator { + subMenuItems.compactMap { item in + if item is SubMenuItems.Separator { return NSMenuItem.separator() } - return NSMenuItem(menuItem: item, target: self, action: callback) + else if let item = item as? SubMenuActionItem { + if item.needBootedDevice && !isDeviceBooted { + return nil + } + + if item.bootsDevice && isDeviceBooted { + return nil + } + + return NSMenuItem(menuItem: item, target: self, action: callback) + } + return nil } } @@ -273,22 +274,9 @@ extension Platform { var subMenuItems: [SubMenuItem] { switch self { case .android: - return [ - AndroidSubMenuItem.copyName, - AndroidSubMenuItem.copyAdbId, - AndroidSubMenuItem.separator, - AndroidSubMenuItem.coldBootAndroid, - AndroidSubMenuItem.androidNoAudio, - AndroidSubMenuItem.toggleA11yAndroid, - AndroidSubMenuItem.pasteToEmulator - ] + return SubMenuItems.android case .ios: - return [ - IOSSubMenuItem.copyName, - IOSSubMenuItem.copyUDID, - IOSSubMenuItem.separator, - IOSSubMenuItem.deleteSim - ] + return SubMenuItems.ios } } } diff --git a/MiniSim/MenuItems/AndroidSubMenuItem.swift b/MiniSim/MenuItems/AndroidSubMenuItem.swift deleted file mode 100644 index 209f995..0000000 --- a/MiniSim/MenuItems/AndroidSubMenuItem.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// AndroidSubMenuItem.swift -// MiniSim -// -// Created by Oskar Kwaśniewski on 07/02/2023. -// - -import Cocoa - -enum AndroidSubMenuItem: Int, CaseIterable, SubMenuItem { - - case copyName = 100 - case copyAdbId - case separator - case coldBootAndroid - case androidNoAudio - case toggleA11yAndroid - case pasteToEmulator - case customCommand = 200 - - var needBootedDevice: Bool { - switch self { - case .copyAdbId, .toggleA11yAndroid, .pasteToEmulator: - return true - default: - return false - } - } - - var bootsDevice: Bool { - switch self { - case .androidNoAudio, .coldBootAndroid: - return true - default: - return false - } - } - - var tag: Int { self.rawValue } - - var isSeparator: Bool { self == .separator } - - var title: String { - switch self { - case .copyName: - return NSLocalizedString("Copy name", comment: "") - case .copyAdbId: - return NSLocalizedString("Copy ID", comment: "") - case .androidNoAudio: - return NSLocalizedString("Run without audio", comment: "") - case .coldBootAndroid: - return NSLocalizedString("Cold boot", comment: "") - case .toggleA11yAndroid: - return NSLocalizedString("Toggle accessibility", comment: "") - case .pasteToEmulator: - return NSLocalizedString("Paste clipboard to emulator", comment: "") - default: - return "" - } - } - - var image: NSImage? { - switch self { - case .copyName: - return NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: "Copy name") - case .copyAdbId: - return NSImage(systemSymbolName: "doc.on.doc", accessibilityDescription: "Copy ID") - case .androidNoAudio: - return NSImage(systemSymbolName: "speaker.slash.fill", accessibilityDescription: "Run without audio") - case .coldBootAndroid: - return NSImage(systemSymbolName: "sunrise.fill", accessibilityDescription: "Cold boot") - case .toggleA11yAndroid: - return NSImage(systemSymbolName: "figure.walk.circle.fill", accessibilityDescription: "Toggle accessibility") - case .pasteToEmulator: - return NSImage(systemSymbolName: "keyboard", accessibilityDescription: "Keyboard") - default: - return NSImage() - } - } - - var CommandItem: Command? { - if self == .separator || self == .customCommand { - return nil - } - return Command(name: self.title, command: "", icon: "", platform: Platform.android, needBootedDevice: needBootedDevice, bootsDevice: self.bootsDevice, tag: self.rawValue) - } -} diff --git a/MiniSim/MenuItems/IOSSubMenuItem.swift b/MiniSim/MenuItems/IOSSubMenuItem.swift deleted file mode 100644 index 4e72156..0000000 --- a/MiniSim/MenuItems/IOSSubMenuItem.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// iOSSubMenuItem.swift -// MiniSim -// -// Created by Oskar Kwaśniewski on 08/02/2023. -// - -import Cocoa - -enum IOSSubMenuItem: Int, CaseIterable, SubMenuItem { - - case copyName = 100 - case copyUDID - case separator - case deleteSim - case customCommand = 200 - - var needBootedDevice: Bool { false } - - var bootsDevice: Bool { false } - - var tag: Int { self.rawValue } - - var isSeparator: Bool { self == .separator } - - var title: String { - switch self { - case .copyName: - return NSLocalizedString("Copy name", comment: "") - case .copyUDID: - return NSLocalizedString("Copy UDID", comment: "") - case .deleteSim: - return NSLocalizedString("Delete simulator", comment: "") - default: - return "" - } - } - - var image: NSImage? { - switch self { - case .copyName: - return NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: "Copy name") - case .copyUDID: - return NSImage(systemSymbolName: "doc.on.doc", accessibilityDescription: "Copy UDID") - case .deleteSim: - return NSImage(systemSymbolName: "trash", accessibilityDescription: "Delete simulator") - default: - return nil - } - } - - var CommandItem: Command? { - if self == .separator || self == .customCommand { - return nil - } - - // `needBootedDevice` is not supported on iOS. - return Command(name: self.title, command: "", icon: "", platform: Platform.ios, needBootedDevice: false, tag: self.rawValue) - } - -} diff --git a/MiniSim/MenuItems/SubMenuItem.swift b/MiniSim/MenuItems/SubMenuItem.swift index fb4e950..b5114bc 100644 --- a/MiniSim/MenuItems/SubMenuItem.swift +++ b/MiniSim/MenuItems/SubMenuItem.swift @@ -8,11 +8,138 @@ import Cocoa import Foundation -protocol SubMenuItem { +protocol SubMenuItem { } + +protocol SubMenuActionItem: SubMenuItem { + var title: String { get } + var tag: Int { get } var needBootedDevice: Bool { get } var bootsDevice: Bool { get } - var title: String { get } var image: NSImage? { get } - var tag: Int { get } - var isSeparator: Bool { get } +} + +enum SubMenuItems { + enum Tags: Int { + case copyName = 100 + case copyID + case coldBoot + case noAudio + case toggleA11y + case paste + case delete + case customCommand = 200 + } + + struct Separator: SubMenuItem { } + + struct CopyName: SubMenuActionItem { + let title: String = NSLocalizedString("Copy name", comment: "") + let tag: Int = Tags.copyName.rawValue + let bootsDevice: Bool = false + let needBootedDevice: Bool = false + let image = NSImage( + systemSymbolName: "square.and.arrow.up", + accessibilityDescription: "Copy name" + ) + } + + struct CopyID: SubMenuActionItem { + let title: String = NSLocalizedString("Copy ID", comment: "") + let tag: Int = Tags.copyID.rawValue + let bootsDevice: Bool = false + let needBootedDevice: Bool = true + let image = NSImage( + systemSymbolName: "doc.on.doc", + accessibilityDescription: "Copy ID" + ) + } + + struct CopyUDID: SubMenuActionItem { + let title: String = NSLocalizedString("Copy UDID", comment: "") + let tag: Int = Tags.copyID.rawValue + let bootsDevice: Bool = false + let needBootedDevice: Bool = false + let image = NSImage( + systemSymbolName: "doc.on.doc", + accessibilityDescription: "Copy UDID" + ) + } + + struct ColdBoot: SubMenuActionItem { + let title: String = NSLocalizedString("Cold boot", comment: "") + let tag: Int = Tags.coldBoot.rawValue + let bootsDevice: Bool = true + let needBootedDevice: Bool = false + let image = NSImage( + systemSymbolName: "sunrise.fill", + accessibilityDescription: "Cold boot" + ) + } + + struct NoAudio: SubMenuActionItem { + let title: String = NSLocalizedString("Run without audio", comment: "") + let tag: Int = Tags.noAudio.rawValue + let bootsDevice: Bool = true + let needBootedDevice: Bool = false + let image = NSImage( + systemSymbolName: "speaker.slash.fill", + accessibilityDescription: "Run without audio" + ) + } + + struct ToggleA11y: SubMenuActionItem { + let title: String = NSLocalizedString("Toggle accessibility", comment: "") + let tag: Int = Tags.toggleA11y.rawValue + let bootsDevice: Bool = false + let needBootedDevice: Bool = true + let image = NSImage( + systemSymbolName: "figure.walk.circle.fill", + accessibilityDescription: "Toggle accessibility" + ) + } + + struct Paste: SubMenuActionItem { + let title: String = NSLocalizedString("Paste clipboard to device", comment: "") + let tag: Int = Tags.paste.rawValue + let bootsDevice: Bool = false + let needBootedDevice: Bool = true + let image = NSImage( + systemSymbolName: "keyboard", + accessibilityDescription: "Keyboard" + ) + } + + struct Delete: SubMenuActionItem { + let title: String = NSLocalizedString("Delete simulator", comment: "") + let tag: Int = Tags.paste.rawValue + let bootsDevice: Bool = false + let needBootedDevice: Bool = false + let image = NSImage( + systemSymbolName: "trash", + accessibilityDescription: "Delete simulator" + ) + } +} + +extension SubMenuItems { + static var android: [SubMenuItem] = [ + CopyName(), + CopyID(), + + Separator(), + + ColdBoot(), + NoAudio(), + ToggleA11y(), + Paste() + ] + + static var ios: [SubMenuItem] = [ + CopyName(), + CopyUDID(), + + Separator(), + + Delete() + ] } diff --git a/MiniSim/Service/DeviceService.swift b/MiniSim/Service/DeviceService.swift index fb36a4c..6058c87 100644 --- a/MiniSim/Service/DeviceService.swift +++ b/MiniSim/Service/DeviceService.swift @@ -14,13 +14,11 @@ protocol DeviceServiceProtocol { static func getIOSDevices() throws -> [Device] static func checkXcodeSetup() -> Bool static func deleteSimulator(uuid: String) throws - static func handleiOSAction(device: Device, commandTag: IOSSubMenuItem, itemName: String) static func toggleA11y(device: Device) throws static func getAndroidDevices() throws -> [Device] static func sendText(device: Device, text: String) throws static func checkAndroidSetup() throws -> String - static func handleAndroidAction(device: Device, commandTag: AndroidSubMenuItem, itemName: String) static func focusDevice(_ device: Device) static func runCustomCommand(_ device: Device, command: Command) throws @@ -287,18 +285,17 @@ extension DeviceService { try shellOut(to: ProcessPaths.xcrun.rawValue, arguments: ["simctl", "delete", uuid]) } - static func handleiOSAction(device: Device, commandTag: IOSSubMenuItem, itemName: String) { - + static func handleiOSAction(device: Device, commandTag: SubMenuItems.Tags, itemName: String) { switch commandTag { case .copyName: NSPasteboard.general.copyToPasteboard(text: device.name) DeviceService.showSuccessMessage(title: "Device name copied to clipboard!", message: device.name) - case .copyUDID: + case .copyID: if let deviceID = device.ID { NSPasteboard.general.copyToPasteboard(text: deviceID) DeviceService.showSuccessMessage(title: "Device ID copied to clipboard!", message: deviceID) } - case .deleteSim: + case .delete: guard let deviceID = device.ID else { return } if !NSAlert.showQuestionDialog(title: "Are you sure?", message: "Are you sure you want to delete this Simulator?") { return @@ -400,20 +397,20 @@ extension DeviceService { try shellOut(to: "\(adbPath) -s \(deviceId) shell input text \"\(formattedText)\"") } - static func handleAndroidAction(device: Device, commandTag: AndroidSubMenuItem, itemName: String) { - DispatchQueue.global(qos: .userInitiated).async { + static func handleAndroidAction(device: Device, commandTag: SubMenuItems.Tags, itemName: String) { + queue.async { do { switch commandTag { - case .coldBootAndroid: + case .coldBoot: try DeviceService.launchDevice(name: device.name, additionalArguments:["-no-snapshot"]) - case .androidNoAudio: + case .noAudio: try DeviceService.launchDevice(name: device.name, additionalArguments:["-no-audio"]) - case .toggleA11yAndroid: + case .toggleA11y: try DeviceService.toggleA11y(device: device) - case .copyAdbId: + case .copyID: if let deviceId = device.ID { NSPasteboard.general.copyToPasteboard(text: deviceId) DeviceService.showSuccessMessage(title: "Device ID copied to clipboard!", message: deviceId) @@ -423,7 +420,7 @@ extension DeviceService { NSPasteboard.general.copyToPasteboard(text: device.name) DeviceService.showSuccessMessage(title: "Device name copied to clipboard!", message: device.name) - case .pasteToEmulator: + case .paste: guard let clipboard = NSPasteboard.general.pasteboardItems?.first?.string(forType: .string) else { break } try DeviceService.sendText(device: device, text: clipboard) @@ -431,7 +428,6 @@ extension DeviceService { if let command = DeviceService.getCustomCommand(platform: .android, commandName: itemName) { try DeviceService.runCustomCommand(device, command: command) } - default: break } From 1e871784fb1aa2b15bc923b07677c7aeeee3692a Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Tue, 31 Oct 2023 21:55:20 +0100 Subject: [PATCH 6/9] Addressing PR comments --- .../AppleScript Commands/ExecuteCommand.swift | 32 +++++----- MiniSim/Menu.swift | 50 ++++++++++++--- MiniSim/MenuItems/SubMenuItem.swift | 64 +++++++++---------- MiniSim/MiniSim.swift | 41 +----------- 4 files changed, 94 insertions(+), 93 deletions(-) diff --git a/MiniSim/AppleScript Commands/ExecuteCommand.swift b/MiniSim/AppleScript Commands/ExecuteCommand.swift index d3f0e55..e1864e9 100644 --- a/MiniSim/AppleScript Commands/ExecuteCommand.swift +++ b/MiniSim/AppleScript Commands/ExecuteCommand.swift @@ -19,28 +19,30 @@ class ExecuteCommand: NSScriptCommand { let deviceId = self.property(forKey: "deviceId") as? String else { scriptErrorNumber = NSRequiredArgumentsMissingScriptError; - return nil; + return nil } let device = Device(name: deviceName, ID: deviceId, platform: platform) let rawTag = Int(tag) ?? 0 - guard let menuItem = SubMenuItems.Tags(rawValue: rawTag) else { + guard let menuItem = SubMenuItems.Tags(rawValue: rawTag) else { return nil } - platform == .android ? - DeviceService.handleAndroidAction( - device: device, - commandTag: menuItem, - itemName: commandName - ) : - DeviceService.handleiOSAction( - device: device, - commandTag: menuItem, - itemName: commandName - ) - - return Never.self; + switch platform { + case .android: + DeviceService.handleAndroidAction( + device: device, + commandTag: menuItem, + itemName: commandName + ) + case .ios: + DeviceService.handleiOSAction( + device: device, + commandTag: menuItem, + itemName: commandName + ) + } + return nil } } diff --git a/MiniSim/Menu.swift b/MiniSim/Menu.swift index 190350e..6a66aa3 100644 --- a/MiniSim/Menu.swift +++ b/MiniSim/Menu.swift @@ -33,6 +33,39 @@ class Menu: NSMenu { self.delegate = self } + func populateDefaultMenu() { + var sections: [DeviceListSection] = [] + if UserDefaults.standard.enableiOSSimulators { + sections.append(.iOS) + } + + if UserDefaults.standard.enableAndroidEmulators { + sections.append(.android) + } + + guard !sections.isEmpty else { + return + } + + var menuItems: [NSMenuItem] = [] + + sections.forEach { section in + var menuItem: NSMenuItem + if #available(macOS 14.0, *) { + menuItem = NSMenuItem.sectionHeader(title: "") + } else { + menuItem = NSMenuItem() + } + menuItem.tag = section.rawValue + menuItem.title = section.title + menuItem.toolTip = section.title + + menuItems.append(menuItem) + menuItems.append(NSMenuItem.separator()) + } + self.items = menuItems + } + func updateDevicesList() { let userDefaults = UserDefaults.standard DeviceService.getAllDevices( @@ -52,7 +85,8 @@ class Menu: NSMenu { } private func removeMenuItems(removedDevices: Set) { - self.items.filter({ removedDevices.contains($0.title) }) + self.items + .filter({ removedDevices.contains($0.title) }) .forEach(safeRemoveItem) } @@ -154,14 +188,12 @@ class Menu: NSMenu { return } - var count = 0 - items.forEach { menuItem in - count += 1 + for (index, menuItem) in items.enumerated() { if let itemIndex = self.items.firstIndex(where: { $0.title == menuItem.title }) { self.replaceMenuItem(at: itemIndex, with: menuItem) - return + continue } - self.safeInsertItem(menuItem, at: startIndex + count) + self.safeInsertItem(menuItem, at: startIndex + index + 1) } } @@ -174,7 +206,7 @@ class Menu: NSMenu { ) menuItem.target = self - menuItem.keyEquivalentModifierMask = [.command] + menuItem.keyEquivalentModifierMask = device.platform == .android ? [.option] : [.command] menuItem.submenu = buildSubMenu(for: device) menuItem.state = device.booted ? .on : .off return menuItem @@ -210,7 +242,8 @@ class Menu: NSMenu { if item is SubMenuItems.Separator { return NSMenuItem.separator() } - else if let item = item as? SubMenuActionItem { + + if let item = item as? SubMenuActionItem { if item.needBootedDevice && !isDeviceBooted { return nil } @@ -221,6 +254,7 @@ class Menu: NSMenu { return NSMenuItem(menuItem: item, target: self, action: callback) } + return nil } } diff --git a/MiniSim/MenuItems/SubMenuItem.swift b/MiniSim/MenuItems/SubMenuItem.swift index b5114bc..090e3ac 100644 --- a/MiniSim/MenuItems/SubMenuItem.swift +++ b/MiniSim/MenuItems/SubMenuItem.swift @@ -33,10 +33,10 @@ enum SubMenuItems { struct Separator: SubMenuItem { } struct CopyName: SubMenuActionItem { - let title: String = NSLocalizedString("Copy name", comment: "") - let tag: Int = Tags.copyName.rawValue - let bootsDevice: Bool = false - let needBootedDevice: Bool = false + let title = NSLocalizedString("Copy name", comment: "") + let tag = Tags.copyName.rawValue + let bootsDevice = false + let needBootedDevice = false let image = NSImage( systemSymbolName: "square.and.arrow.up", accessibilityDescription: "Copy name" @@ -44,10 +44,10 @@ enum SubMenuItems { } struct CopyID: SubMenuActionItem { - let title: String = NSLocalizedString("Copy ID", comment: "") - let tag: Int = Tags.copyID.rawValue - let bootsDevice: Bool = false - let needBootedDevice: Bool = true + let title = NSLocalizedString("Copy ID", comment: "") + let tag = Tags.copyID.rawValue + let bootsDevice = false + let needBootedDevice = true let image = NSImage( systemSymbolName: "doc.on.doc", accessibilityDescription: "Copy ID" @@ -55,10 +55,10 @@ enum SubMenuItems { } struct CopyUDID: SubMenuActionItem { - let title: String = NSLocalizedString("Copy UDID", comment: "") - let tag: Int = Tags.copyID.rawValue - let bootsDevice: Bool = false - let needBootedDevice: Bool = false + let title = NSLocalizedString("Copy UDID", comment: "") + let tag = Tags.copyID.rawValue + let bootsDevice = false + let needBootedDevice = false let image = NSImage( systemSymbolName: "doc.on.doc", accessibilityDescription: "Copy UDID" @@ -66,10 +66,10 @@ enum SubMenuItems { } struct ColdBoot: SubMenuActionItem { - let title: String = NSLocalizedString("Cold boot", comment: "") - let tag: Int = Tags.coldBoot.rawValue - let bootsDevice: Bool = true - let needBootedDevice: Bool = false + let title = NSLocalizedString("Cold boot", comment: "") + let tag = Tags.coldBoot.rawValue + let bootsDevice = true + let needBootedDevice = false let image = NSImage( systemSymbolName: "sunrise.fill", accessibilityDescription: "Cold boot" @@ -77,10 +77,10 @@ enum SubMenuItems { } struct NoAudio: SubMenuActionItem { - let title: String = NSLocalizedString("Run without audio", comment: "") - let tag: Int = Tags.noAudio.rawValue - let bootsDevice: Bool = true - let needBootedDevice: Bool = false + let title = NSLocalizedString("Run without audio", comment: "") + let tag = Tags.noAudio.rawValue + let bootsDevice = true + let needBootedDevice = false let image = NSImage( systemSymbolName: "speaker.slash.fill", accessibilityDescription: "Run without audio" @@ -88,10 +88,10 @@ enum SubMenuItems { } struct ToggleA11y: SubMenuActionItem { - let title: String = NSLocalizedString("Toggle accessibility", comment: "") - let tag: Int = Tags.toggleA11y.rawValue - let bootsDevice: Bool = false - let needBootedDevice: Bool = true + let title = NSLocalizedString("Toggle accessibility", comment: "") + let tag = Tags.toggleA11y.rawValue + let bootsDevice = false + let needBootedDevice = true let image = NSImage( systemSymbolName: "figure.walk.circle.fill", accessibilityDescription: "Toggle accessibility" @@ -99,10 +99,10 @@ enum SubMenuItems { } struct Paste: SubMenuActionItem { - let title: String = NSLocalizedString("Paste clipboard to device", comment: "") - let tag: Int = Tags.paste.rawValue - let bootsDevice: Bool = false - let needBootedDevice: Bool = true + let title = NSLocalizedString("Paste clipboard to device", comment: "") + let tag = Tags.paste.rawValue + let bootsDevice = false + let needBootedDevice = true let image = NSImage( systemSymbolName: "keyboard", accessibilityDescription: "Keyboard" @@ -110,10 +110,10 @@ enum SubMenuItems { } struct Delete: SubMenuActionItem { - let title: String = NSLocalizedString("Delete simulator", comment: "") - let tag: Int = Tags.paste.rawValue - let bootsDevice: Bool = false - let needBootedDevice: Bool = false + let title = NSLocalizedString("Delete simulator", comment: "") + let tag = Tags.paste.rawValue + let bootsDevice = false + let needBootedDevice = false let image = NSImage( systemSymbolName: "trash", accessibilityDescription: "Delete simulator" diff --git a/MiniSim/MiniSim.swift b/MiniSim/MiniSim.swift index a5daa81..d4dfb2e 100644 --- a/MiniSim/MiniSim.swift +++ b/MiniSim/MiniSim.swift @@ -85,12 +85,10 @@ class MiniSim: NSObject { statusItem.menu = menu setMenuImage() - guard menu.items.isEmpty else { - menu.updateDevicesList() - return + if menu.items.isEmpty { + menu.populateDefaultMenu() + menu.items += mainMenu } - - (devicesListHeaders + mainMenu).forEach { menu.addItem($0) } menu.updateDevicesList() } @@ -166,39 +164,6 @@ class MiniSim: NSObject { } } - private var devicesListHeaders: [NSMenuItem] { - var sections: [DeviceListSection] = [] - if UserDefaults.standard.enableiOSSimulators { - sections.append(.iOS) - } - - if UserDefaults.standard.enableAndroidEmulators { - sections.append(.android) - } - - guard sections.count > 1 else { - return [] - } - - var menuItems: [NSMenuItem] = [] - - sections.forEach { section in - var menuItem: NSMenuItem - if #available(macOS 14.0, *) { - menuItem = NSMenuItem.sectionHeader(title: "") - } else { - menuItem = NSMenuItem() - } - menuItem.tag = section.rawValue - menuItem.title = section.title - menuItem.toolTip = section.title - - menuItems.append(menuItem) - menuItems.append(NSMenuItem.separator()) - } - return menuItems - } - private var mainMenu: [NSMenuItem] { MainMenuActions.allCases.map { item in NSMenuItem( From add262d0ecc325810351876f53aa758af52df6b6 Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Thu, 2 Nov 2023 15:29:50 +0100 Subject: [PATCH 7/9] Addressing code review comments --- MiniSim/Menu.swift | 2 +- MiniSim/MenuItems/SubMenuItem.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MiniSim/Menu.swift b/MiniSim/Menu.swift index 6a66aa3..0009f54 100644 --- a/MiniSim/Menu.swift +++ b/MiniSim/Menu.swift @@ -162,7 +162,7 @@ class Menu: NSMenu { for section in platformSections { let devices = filter(devices: sortedDevices, for: section) let menuItems = devices.map { createMenuItem(for: $0) } - self.updateSection(with: Array(menuItems), section: section) + self.updateSection(with: menuItems, section: section) } } diff --git a/MiniSim/MenuItems/SubMenuItem.swift b/MiniSim/MenuItems/SubMenuItem.swift index 090e3ac..c8264a2 100644 --- a/MiniSim/MenuItems/SubMenuItem.swift +++ b/MiniSim/MenuItems/SubMenuItem.swift @@ -111,7 +111,7 @@ enum SubMenuItems { struct Delete: SubMenuActionItem { let title = NSLocalizedString("Delete simulator", comment: "") - let tag = Tags.paste.rawValue + let tag = Tags.delete.rawValue let bootsDevice = false let needBootedDevice = false let image = NSImage( From 3556fec60146743e0264485c526f0987f078aae9 Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Thu, 2 Nov 2023 15:37:41 +0100 Subject: [PATCH 8/9] Replacing guard with if to improve readability --- MiniSim/Menu.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MiniSim/Menu.swift b/MiniSim/Menu.swift index 0009f54..2a58f64 100644 --- a/MiniSim/Menu.swift +++ b/MiniSim/Menu.swift @@ -43,7 +43,7 @@ class Menu: NSMenu { sections.append(.android) } - guard !sections.isEmpty else { + if sections.isEmpty { return } From fb04d3a34ee2c97befe7e7d6fbf737a08e79c7e7 Mon Sep 17 00:00:00 2001 From: Anton Kolchunov Date: Mon, 6 Nov 2023 10:02:28 +0100 Subject: [PATCH 9/9] Improving code how items are added to menu --- MiniSim/Menu.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/MiniSim/Menu.swift b/MiniSim/Menu.swift index 2a58f64..f3a3d12 100644 --- a/MiniSim/Menu.swift +++ b/MiniSim/Menu.swift @@ -14,7 +14,7 @@ class Menu: NSMenu { var devices: [Device] = [] { didSet { - populateDevices() + populateDevicesMenu(devices) assignKeyEquivalents() } willSet { @@ -156,12 +156,12 @@ class Menu: NSMenu { } // MARK: Populate sections - private func populateDevices() { - let sortedDevices = devices.sorted(by: { $0.platform == .android && $1.platform == .ios }) + private func populateDevicesMenu(_ devices: [Device]) { let platformSections: [DeviceListSection] = sections for section in platformSections { - let devices = filter(devices: sortedDevices, for: section) - let menuItems = devices.map { createMenuItem(for: $0) } + let sectionDevices = filter(devices: devices, for: section) + .sorted(by: { $0.name < $1.name }) + let menuItems = sectionDevices.map { createMenuItem(for: $0) } self.updateSection(with: menuItems, section: section) } } @@ -188,12 +188,12 @@ class Menu: NSMenu { return } - for (index, menuItem) in items.enumerated() { + for menuItem in items.reversed() { if let itemIndex = self.items.firstIndex(where: { $0.title == menuItem.title }) { self.replaceMenuItem(at: itemIndex, with: menuItem) continue } - self.safeInsertItem(menuItem, at: startIndex + index + 1) + self.safeInsertItem(menuItem, at: startIndex + 1) } }