├── .gitignore ├── .swiftlint.yml ├── App Delegate and Main Window ├── NSToolbarItemIdentifier+SearchItem.swift ├── SUGCustomMenusAppDelegate.swift ├── SUGMainWindowContentViewController.swift ├── SUGMainWindowController+NSSearchFieldDelegate.swift ├── SUGMainWindowController+NSToolbarDelegate.swift ├── SUGMainWindowController.storyboard └── SUGMainWindowController.swift ├── Assets.xcassets ├── Contents.json ├── circle.imageset │ ├── Contents.json │ └── clrcle.pdf └── square.imageset │ ├── Contents.json │ └── square.pdf ├── Base.lproj └── MainMenu.xib ├── CustomMenus-Info.plist ├── CustomMenus.entitlements ├── CustomMenus.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── doug.xcuserdatad │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (1)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (10)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (11)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (12)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (13)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (14)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (15)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (16)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (17)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (18)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (19)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (2)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (20)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (21)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (22)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (23)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (3)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (4)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (5)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (6)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (7)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (8)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26 (9)).xcuserstate │ │ ├── UserInterfaceState (alpaca's conflicted copy 2018-03-26).xcuserstate │ │ ├── UserInterfaceState (leopard's conflicted copy 2018-03-26 (1)).xcuserstate │ │ ├── UserInterfaceState (leopard's conflicted copy 2018-03-26 (2)).xcuserstate │ │ └── UserInterfaceState (leopard's conflicted copy 2018-03-26).xcuserstate └── xcuserdata │ └── doug.xcuserdatad │ ├── xcdebugger │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ ├── CustomMenus.xcscheme │ └── xcschememanagement.plist ├── README.md ├── Search Field ├── NSSearchField+cellClass.swift └── SUGSearchFieldCell.swift ├── Suggestion List Window ├── NSImage+SUGMask.swift ├── SUGSuggestionListContentView.swift ├── SUGSuggestionListWindow.swift ├── SUGSuggestionListWindowController.swift ├── SUGSuggestionView.swift └── SUGSuggestionViewController.swift ├── Suggestions ├── SUGSuggestion.swift └── SUGSuggestionGenerator.swift ├── en.lproj └── InfoPlist.strings └── readme_images ├── sample_search_suggestion_menu.png └── screenshot_app.png /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - function_body_length 3 | - line_length 4 | -------------------------------------------------------------------------------- /App Delegate and Main Window/NSToolbarItemIdentifier+SearchItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSToolbarItemIdentifier+SearchItem.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 9/5/23. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSToolbarItem.Identifier { 11 | 12 | static let SUG_search = NSToolbarItem.Identifier("SUG_search") 13 | 14 | } 15 | 16 | -------------------------------------------------------------------------------- /App Delegate and Main Window/SUGCustomMenusAppDelegate.swift: -------------------------------------------------------------------------------- 1 | // Converted to Swift 4 by Swiftify v4.1.6654 - https://objectivec2swift.com/ 2 | /* 3 | File: CustomMenusAppDelegate.m 4 | Abstract: This class is responsible for two major activities. It sets up the images in the popup menu (via a custom view) and responds to the menu actions. Also, it supplies the suggestions for the search text field and responds to suggestion selection changes and text field editing. 5 | Version: 1.4 6 | Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple 7 | Inc. ("Apple") in consideration of your agreement to the following 8 | terms, and your use, installation, modification or redistribution of 9 | this Apple software constitutes acceptance of these terms. If you do 10 | not agree with these terms, please do not use, install, modify or 11 | redistribute this Apple software. 12 | In consideration of your agreement to abide by the following terms, and 13 | subject to these terms, Apple grants you a personal, non-exclusive 14 | license, under Apple's copyrights in this original Apple software (the 15 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 16 | Software, with or without modifications, in source and/or binary forms; 17 | provided that if you redistribute the Apple Software in its entirety and 18 | without modifications, you must retain this notice and the following 19 | text and disclaimers in all such redistributions of the Apple Software. 20 | Neither the name, trademarks, service marks or logos of Apple Inc. may 21 | be used to endorse or promote products derived from the Apple Software 22 | without specific prior written permission from Apple. Except as 23 | expressly stated in this notice, no other rights or licenses, express or 24 | implied, are granted by Apple herein, including but not limited to any 25 | patent rights that may be infringed by your derivative works or by other 26 | works in which the Apple Software may be incorporated. 27 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 28 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 29 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 30 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 31 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 32 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 33 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 34 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 35 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 36 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 37 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 38 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 39 | POSSIBILITY OF SUCH DAMAGE. 40 | Copyright (C) 2012 Apple Inc. All Rights Reserved. 41 | */ 42 | import Cocoa 43 | 44 | @NSApplicationMain 45 | class SUGCustomMenusAppDelegate: NSObject, NSApplicationDelegate { 46 | 47 | private var mainWindowController: SUGMainWindowController? 48 | 49 | func applicationDidFinishLaunching(_ aNotification: Notification) { 50 | self.mainWindowController = NSStoryboard(name: "SUGMainWindowController", bundle: nil).instantiateController(withIdentifier: "SUGMainWindowController") as? SUGMainWindowController 51 | self.mainWindowController?.showWindow(nil) 52 | self.mainWindowController!.window!.center() 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /App Delegate and Main Window/SUGMainWindowContentViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUGMainWindowContentViewController.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 8/31/23. 6 | // 7 | 8 | import AppKit 9 | 10 | class SUGMainWindowContentViewController : NSViewController { 11 | 12 | @IBOutlet var imageView: NSImageView! 13 | 14 | func setImageUrl( imageUrl: URL? ) { 15 | var image: NSImage? = nil 16 | if let imageUrl { 17 | image = NSImage(contentsOf: imageUrl) 18 | } 19 | imageView.image = image 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /App Delegate and Main Window/SUGMainWindowController+NSSearchFieldDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUGMainWindowController+NSSearchFieldDelegate.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 9/5/23. 6 | // 7 | 8 | import AppKit 9 | 10 | extension SUGMainWindowController : NSSearchFieldDelegate { 11 | 12 | // When the user starts editing the text field, this method is called. This is an opportune time to 13 | // display the initial suggestions. 14 | 15 | func controlTextDidBeginEditing(_ notification: Notification) { 16 | // We keep the suggestionsController around, but lazely allocate it the first time it is needed. 17 | if suggestionsWindowController == nil { 18 | suggestionsWindowController = SUGSuggestionListWindowController(automaticallySelectFirstSuggestion: self.searchSuggestionGenerator.automaticallySelectFirstSuggestion) 19 | suggestionsWindowController?.target = self 20 | suggestionsWindowController?.action = #selector(SUGMainWindowController.update(withSelectedSuggestion:)) 21 | } 22 | updateSuggestions(from: notification.object as? NSControl) 23 | } 24 | 25 | // The field editor's text may have changed for a number of reasons. Generally, we should update the 26 | // suggestions window with the new suggestions. 27 | 28 | func controlTextDidChange(_ notification: Notification) { 29 | updateSuggestions(from: notification.object as? NSControl) 30 | } 31 | 32 | // The field editor has ended editing the text. This is not the same as the action from the NSTextField. 33 | // In the MainMenu.xib, the search text field is setup to only send its action on return / enter. If 34 | // the user tabs to or clicks on another control, text editing will end and this method is called. We 35 | // don't consider this committal of the action. Instead, we realy on the text field's action (see 36 | // -takeImageFromSuggestedURL: above) to commit the suggestion. However, since the action may not 37 | // occur, we need to cancel the suggestions window here. 38 | 39 | func controlTextDidEndEditing(_ obj: Notification) { 40 | /* If the suggestionController is already in a cancelled state, this call does nothing and is therefore always safe to call. 41 | */ 42 | if obj.userInfo?["NSTextMovement"] as? Int != 16 { 43 | suggestionsWindowController?.cancelSuggestions() 44 | } 45 | } 46 | 47 | // As the delegate for the NSTextField, this class is given a chance to respond to the key binding commands 48 | // interpreted by the input manager when the field editor calls -interpretKeyEvents:. This is where we 49 | // forward some of the keyboard commands to the suggestion window to facilitate keyboard navigation. 50 | // Also, this is where we can determine when the user deletes and where we can prevent AppKit's auto completion. 51 | 52 | func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { 53 | if commandSelector == #selector(NSResponder.moveUp(_:)) { 54 | // Move up in the suggested selections list 55 | suggestionsWindowController?.moveUp(textView) 56 | return true 57 | } 58 | if commandSelector == #selector(NSResponder.moveDown(_:)) { 59 | // Move down in the suggested selections list 60 | suggestionsWindowController?.moveDown(textView) 61 | return true 62 | } 63 | 64 | // This is "autocomplete" functionality, invoked when the user presses option-escaped. 65 | // By overriding this command we prevent AppKit's auto completion and can respond to 66 | // the user's intention by showing or cancelling our custom suggestions window. 67 | if commandSelector == #selector(NSResponder.complete(_:)) { 68 | if suggestionsWindowController != nil && suggestionsWindowController!.window != nil && suggestionsWindowController!.window!.isVisible { 69 | suggestionsWindowController?.cancelSuggestions() 70 | } else { 71 | updateSuggestions(from: control) 72 | } 73 | return true 74 | } 75 | // This is a command that we don't specifically handle, let the field editor do the appropriate thing. 76 | return false 77 | } 78 | 79 | } 80 | 81 | -------------------------------------------------------------------------------- /App Delegate and Main Window/SUGMainWindowController+NSToolbarDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUGMainWindowController+NSToolbarDelegate.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 9/5/23. 6 | // 7 | 8 | import AppKit 9 | 10 | extension SUGMainWindowController : NSToolbarDelegate { 11 | 12 | public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 13 | return [NSToolbarItem.Identifier.flexibleSpace, NSToolbarItem.Identifier.SUG_search] 14 | } 15 | 16 | public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 17 | return self.toolbarDefaultItemIdentifiers(toolbar) 18 | } 19 | 20 | public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { 21 | if itemIdentifier == NSToolbarItem.Identifier.SUG_search { 22 | let searchItem = NSSearchToolbarItem(itemIdentifier: itemIdentifier) 23 | self.searchField = searchItem.searchField 24 | searchItem.searchField.sendsWholeSearchString = true 25 | searchItem.searchField.delegate = self 26 | searchItem.searchField.target = self 27 | searchItem.searchField.action = #selector(SUGMainWindowController.takeImage(fromSuggestedURL:)) 28 | return searchItem 29 | } 30 | return nil 31 | } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /App Delegate and Main Window/SUGMainWindowController.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /App Delegate and Main Window/SUGMainWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUGMainWindowController.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 8/31/23. 6 | // 7 | 8 | import AppKit 9 | 10 | class SUGMainWindowController : NSWindowController { 11 | 12 | let searchSuggestionGenerator = SUGSuggestionGenerator() 13 | 14 | var searchField: NSTextField? 15 | 16 | var suggestionsWindowController: SUGSuggestionListWindowController? 17 | 18 | override func windowDidLoad() { 19 | let toolbar = NSToolbar(identifier: "SUGMainWindowController.toolbar") 20 | toolbar.displayMode = .iconOnly 21 | toolbar.delegate = self 22 | self.window?.toolbar = toolbar 23 | } 24 | 25 | // This is the action method for when the user changes the suggestion selection. Note, this 26 | // action is called continuously as the suggestion selection changes while being tracked 27 | // and does not denote user committal of the suggestion. For suggestion committal, the text 28 | // field's action method is used (see above). This method is wired up programatically in 29 | // the -controlTextDidBeginEditing: method below. 30 | 31 | @IBAction func update(withSelectedSuggestion sender: Any) { 32 | if let entry = (sender as? SUGSuggestionListWindowController)?.selectedSuggestion() { 33 | if let fieldEditor = self.window?.fieldEditor(false, for: searchField) { 34 | updateFieldEditor(fieldEditor, withSuggestion: entry.name) 35 | } 36 | } 37 | } 38 | 39 | // This method is invoked when the user presses return (or enter) on the search text field. 40 | // We don’t want to use the text from the search field as it is just the image filename 41 | // without a path. Also, it may not be valid. Instead, use this user action to trigger 42 | // setting the large image view in the main window to the currently suggested URL, if 43 | // there is one. 44 | 45 | @IBAction func takeImage(fromSuggestedURL sender: Any) { 46 | if let suggestionsWindowController = self.suggestionsWindowController, self.suggestionsWindowController?.window?.isVisible == true { 47 | let suggestion = suggestionsWindowController.selectedSuggestion() 48 | (self.contentViewController as? SUGMainWindowContentViewController)?.setImageUrl(imageUrl: suggestion?.url) 49 | } else { 50 | (self.contentViewController as? SUGMainWindowContentViewController)?.setImageUrl(imageUrl: nil) 51 | } 52 | self.suggestionsWindowController?.cancelSuggestions() 53 | } 54 | 55 | // Update the field editor with a suggested string. The additional suggested characters are auto selected. 56 | 57 | private func updateFieldEditor(_ fieldEditor: NSText?, withSuggestion suggestion: String?) { 58 | let selection = NSRange(location: fieldEditor?.selectedRange.location ?? 0, length: suggestion?.count ?? 0) 59 | fieldEditor?.string = suggestion ?? "" 60 | fieldEditor?.selectedRange = selection 61 | } 62 | 63 | // Determines the current list of suggestions, display the suggestions and update the field editor. 64 | 65 | func updateSuggestions(from control: NSControl?) { 66 | if let fieldEditor = self.window?.fieldEditor(false, for: control) { 67 | // Only use the text up to the caret position 68 | let selection: NSRange? = fieldEditor.selectedRange 69 | let searchString = (selection != nil) ? (fieldEditor.string as NSString?)?.substring(to: selection!.location) : nil 70 | var suggestions: [SUGSuggestion]? = nil 71 | if let searchString, !searchString.isEmpty { 72 | suggestions = self.searchSuggestionGenerator.suggestions(forSearchString: searchString) 73 | } 74 | if let suggestions, !suggestions.isEmpty { 75 | // We have at least 1 suggestion. Update the field editor to the first suggestion and show the suggestions window. 76 | 77 | suggestionsWindowController?.setSuggestions(suggestions) 78 | if !(suggestionsWindowController?.window?.isVisible ?? false) { 79 | suggestionsWindowController?.begin(for: (control as? NSTextField)) 80 | } 81 | if self.searchSuggestionGenerator.automaticallySelectFirstSuggestion { 82 | let suggestion = suggestions[0] 83 | updateFieldEditor(fieldEditor, withSuggestion: suggestion.name) 84 | } 85 | } else { 86 | // No suggestions. Cancel the suggestion window and set the _suggestedURL to nil. 87 | suggestionsWindowController?.cancelSuggestions() 88 | } 89 | } 90 | } 91 | 92 | } 93 | 94 | 95 | -------------------------------------------------------------------------------- /Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Assets.xcassets/circle.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "clrcle.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Assets.xcassets/circle.imageset/clrcle.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/Assets.xcassets/circle.imageset/clrcle.pdf -------------------------------------------------------------------------------- /Assets.xcassets/square.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "square.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Assets.xcassets/square.imageset/square.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/Assets.xcassets/square.imageset/square.pdf -------------------------------------------------------------------------------- /Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | -------------------------------------------------------------------------------- /CustomMenus-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.3 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1.4 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /CustomMenus.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1DDD58160DA1D0A300B32029 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1DDD58140DA1D0A300B32029 /* MainMenu.xib */; }; 11 | 2108362B2AA3E774006B0411 /* SUGSuggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210836292AA3E774006B0411 /* SUGSuggestion.swift */; }; 12 | 2108362C2AA3E774006B0411 /* SUGSuggestionGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2108362A2AA3E774006B0411 /* SUGSuggestionGenerator.swift */; }; 13 | 210836322AA3E791006B0411 /* SUGMainWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2108362E2AA3E791006B0411 /* SUGMainWindowController.swift */; }; 14 | 210836332AA3E791006B0411 /* SUGCustomMenusAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2108362F2AA3E791006B0411 /* SUGCustomMenusAppDelegate.swift */; }; 15 | 210836342AA3E791006B0411 /* SUGMainWindowController.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 210836302AA3E791006B0411 /* SUGMainWindowController.storyboard */; }; 16 | 210836352AA3E791006B0411 /* SUGMainWindowContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210836312AA3E791006B0411 /* SUGMainWindowContentViewController.swift */; }; 17 | 210836392AA3E7AE006B0411 /* SUGSearchFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210836372AA3E7AE006B0411 /* SUGSearchFieldCell.swift */; }; 18 | 2108363A2AA3E7AE006B0411 /* NSSearchField+cellClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210836382AA3E7AE006B0411 /* NSSearchField+cellClass.swift */; }; 19 | 210836412AA3E7D7006B0411 /* SUGSuggestionListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2108363C2AA3E7D7006B0411 /* SUGSuggestionListContentView.swift */; }; 20 | 210836422AA3E7D7006B0411 /* SUGSuggestionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2108363D2AA3E7D7006B0411 /* SUGSuggestionViewController.swift */; }; 21 | 210836432AA3E7D7006B0411 /* SUGSuggestionListWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2108363E2AA3E7D7006B0411 /* SUGSuggestionListWindowController.swift */; }; 22 | 210836442AA3E7D7006B0411 /* SUGSuggestionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2108363F2AA3E7D7006B0411 /* SUGSuggestionView.swift */; }; 23 | 210836452AA3E7D7006B0411 /* SUGSuggestionListWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210836402AA3E7D7006B0411 /* SUGSuggestionListWindow.swift */; }; 24 | 210836472AA3E83C006B0411 /* NSImage+SUGMask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 210836462AA3E83C006B0411 /* NSImage+SUGMask.swift */; }; 25 | 214C5DA72AA8075900620EDD /* SUGMainWindowController+NSSearchFieldDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214C5DA62AA8075900620EDD /* SUGMainWindowController+NSSearchFieldDelegate.swift */; }; 26 | 214C5DA92AA8079A00620EDD /* SUGMainWindowController+NSToolbarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214C5DA82AA8079A00620EDD /* SUGMainWindowController+NSToolbarDelegate.swift */; }; 27 | 214C5DAB2AA807C600620EDD /* NSToolbarItemIdentifier+SearchItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 214C5DAA2AA807C600620EDD /* NSToolbarItemIdentifier+SearchItem.swift */; }; 28 | 21E7837F2AA17C0F0027D003 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 21E7837E2AA17C0F0027D003 /* Assets.xcassets */; }; 29 | 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 30 | 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; 31 | /* End PBXBuildFile section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; 35 | 210836292AA3E774006B0411 /* SUGSuggestion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGSuggestion.swift; sourceTree = ""; }; 36 | 2108362A2AA3E774006B0411 /* SUGSuggestionGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGSuggestionGenerator.swift; sourceTree = ""; }; 37 | 2108362E2AA3E791006B0411 /* SUGMainWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGMainWindowController.swift; sourceTree = ""; }; 38 | 2108362F2AA3E791006B0411 /* SUGCustomMenusAppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGCustomMenusAppDelegate.swift; sourceTree = ""; }; 39 | 210836302AA3E791006B0411 /* SUGMainWindowController.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SUGMainWindowController.storyboard; sourceTree = ""; }; 40 | 210836312AA3E791006B0411 /* SUGMainWindowContentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGMainWindowContentViewController.swift; sourceTree = ""; }; 41 | 210836372AA3E7AE006B0411 /* SUGSearchFieldCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGSearchFieldCell.swift; sourceTree = ""; }; 42 | 210836382AA3E7AE006B0411 /* NSSearchField+cellClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSSearchField+cellClass.swift"; sourceTree = ""; }; 43 | 2108363C2AA3E7D7006B0411 /* SUGSuggestionListContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGSuggestionListContentView.swift; sourceTree = ""; }; 44 | 2108363D2AA3E7D7006B0411 /* SUGSuggestionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGSuggestionViewController.swift; sourceTree = ""; }; 45 | 2108363E2AA3E7D7006B0411 /* SUGSuggestionListWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGSuggestionListWindowController.swift; sourceTree = ""; }; 46 | 2108363F2AA3E7D7006B0411 /* SUGSuggestionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGSuggestionView.swift; sourceTree = ""; }; 47 | 210836402AA3E7D7006B0411 /* SUGSuggestionListWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SUGSuggestionListWindow.swift; sourceTree = ""; }; 48 | 210836462AA3E83C006B0411 /* NSImage+SUGMask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSImage+SUGMask.swift"; sourceTree = ""; }; 49 | 214C5DA62AA8075900620EDD /* SUGMainWindowController+NSSearchFieldDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SUGMainWindowController+NSSearchFieldDelegate.swift"; sourceTree = ""; }; 50 | 214C5DA82AA8079A00620EDD /* SUGMainWindowController+NSToolbarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SUGMainWindowController+NSToolbarDelegate.swift"; sourceTree = ""; }; 51 | 214C5DAA2AA807C600620EDD /* NSToolbarItemIdentifier+SearchItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSToolbarItemIdentifier+SearchItem.swift"; sourceTree = ""; }; 52 | 2184044E2A9FD3D400A7CBF8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 53 | 218404512A9FD3D700A7CBF8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 54 | 21E7837E2AA17C0F0027D003 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 55 | 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 56 | 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; 57 | 532273A015F92B3100F89E73 /* CustomMenus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = CustomMenus.entitlements; sourceTree = ""; }; 58 | 8D1107310486CEB800E47090 /* CustomMenus-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "CustomMenus-Info.plist"; sourceTree = ""; }; 59 | 8D1107320486CEB800E47090 /* CustomMenus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CustomMenus.app; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | /* End PBXFileReference section */ 61 | 62 | /* Begin PBXFrameworksBuildPhase section */ 63 | 8D11072E0486CEB800E47090 /* Frameworks */ = { 64 | isa = PBXFrameworksBuildPhase; 65 | buildActionMask = 2147483647; 66 | files = ( 67 | 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, 68 | ); 69 | runOnlyForDeploymentPostprocessing = 0; 70 | }; 71 | /* End PBXFrameworksBuildPhase section */ 72 | 73 | /* Begin PBXGroup section */ 74 | 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, 78 | ); 79 | name = "Linked Frameworks"; 80 | sourceTree = ""; 81 | }; 82 | 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 29B97324FDCFA39411CA2CEA /* AppKit.framework */, 86 | 29B97325FDCFA39411CA2CEA /* Foundation.framework */, 87 | ); 88 | name = "Other Frameworks"; 89 | sourceTree = ""; 90 | }; 91 | 19C28FACFE9D520D11CA2CBB /* Products */ = { 92 | isa = PBXGroup; 93 | children = ( 94 | 8D1107320486CEB800E47090 /* CustomMenus.app */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | 210836282AA3E774006B0411 /* Suggestions */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 210836292AA3E774006B0411 /* SUGSuggestion.swift */, 103 | 2108362A2AA3E774006B0411 /* SUGSuggestionGenerator.swift */, 104 | ); 105 | path = Suggestions; 106 | sourceTree = ""; 107 | }; 108 | 2108362D2AA3E791006B0411 /* App Delegate and Main Window */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 214C5DAA2AA807C600620EDD /* NSToolbarItemIdentifier+SearchItem.swift */, 112 | 2108362F2AA3E791006B0411 /* SUGCustomMenusAppDelegate.swift */, 113 | 210836312AA3E791006B0411 /* SUGMainWindowContentViewController.swift */, 114 | 210836302AA3E791006B0411 /* SUGMainWindowController.storyboard */, 115 | 2108362E2AA3E791006B0411 /* SUGMainWindowController.swift */, 116 | 214C5DA62AA8075900620EDD /* SUGMainWindowController+NSSearchFieldDelegate.swift */, 117 | 214C5DA82AA8079A00620EDD /* SUGMainWindowController+NSToolbarDelegate.swift */, 118 | ); 119 | path = "App Delegate and Main Window"; 120 | sourceTree = ""; 121 | }; 122 | 210836362AA3E7AE006B0411 /* Search Field */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 210836372AA3E7AE006B0411 /* SUGSearchFieldCell.swift */, 126 | 210836382AA3E7AE006B0411 /* NSSearchField+cellClass.swift */, 127 | ); 128 | path = "Search Field"; 129 | sourceTree = ""; 130 | }; 131 | 2108363B2AA3E7D7006B0411 /* Suggestion List Window */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | 210836462AA3E83C006B0411 /* NSImage+SUGMask.swift */, 135 | 2108363C2AA3E7D7006B0411 /* SUGSuggestionListContentView.swift */, 136 | 2108363D2AA3E7D7006B0411 /* SUGSuggestionViewController.swift */, 137 | 2108363E2AA3E7D7006B0411 /* SUGSuggestionListWindowController.swift */, 138 | 2108363F2AA3E7D7006B0411 /* SUGSuggestionView.swift */, 139 | 210836402AA3E7D7006B0411 /* SUGSuggestionListWindow.swift */, 140 | ); 141 | path = "Suggestion List Window"; 142 | sourceTree = ""; 143 | }; 144 | 29B97314FDCFA39411CA2CEA /* CustomMenus */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 532273A015F92B3100F89E73 /* CustomMenus.entitlements */, 148 | 21E7837E2AA17C0F0027D003 /* Assets.xcassets */, 149 | 210836282AA3E774006B0411 /* Suggestions */, 150 | 2108362D2AA3E791006B0411 /* App Delegate and Main Window */, 151 | 2108363B2AA3E7D7006B0411 /* Suggestion List Window */, 152 | 210836362AA3E7AE006B0411 /* Search Field */, 153 | 29B97317FDCFA39411CA2CEA /* Resources */, 154 | 29B97323FDCFA39411CA2CEA /* Frameworks */, 155 | 19C28FACFE9D520D11CA2CBB /* Products */, 156 | ); 157 | name = CustomMenus; 158 | sourceTree = ""; 159 | }; 160 | 29B97317FDCFA39411CA2CEA /* Resources */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 8D1107310486CEB800E47090 /* CustomMenus-Info.plist */, 164 | 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, 165 | 1DDD58140DA1D0A300B32029 /* MainMenu.xib */, 166 | ); 167 | name = Resources; 168 | sourceTree = ""; 169 | }; 170 | 29B97323FDCFA39411CA2CEA /* Frameworks */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, 174 | 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, 175 | ); 176 | name = Frameworks; 177 | sourceTree = ""; 178 | }; 179 | /* End PBXGroup section */ 180 | 181 | /* Begin PBXNativeTarget section */ 182 | 8D1107260486CEB800E47090 /* CustomMenus */ = { 183 | isa = PBXNativeTarget; 184 | buildConfigurationList = C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "CustomMenus" */; 185 | buildPhases = ( 186 | 8D1107290486CEB800E47090 /* Resources */, 187 | 8D11072C0486CEB800E47090 /* Sources */, 188 | 8D11072E0486CEB800E47090 /* Frameworks */, 189 | ); 190 | buildRules = ( 191 | ); 192 | dependencies = ( 193 | ); 194 | name = CustomMenus; 195 | productInstallPath = "$(HOME)/Applications"; 196 | productName = CustomMenus; 197 | productReference = 8D1107320486CEB800E47090 /* CustomMenus.app */; 198 | productType = "com.apple.product-type.application"; 199 | }; 200 | /* End PBXNativeTarget section */ 201 | 202 | /* Begin PBXProject section */ 203 | 29B97313FDCFA39411CA2CEA /* Project object */ = { 204 | isa = PBXProject; 205 | attributes = { 206 | BuildIndependentTargetsInParallel = YES; 207 | LastUpgradeCheck = 1500; 208 | TargetAttributes = { 209 | 8D1107260486CEB800E47090 = { 210 | DevelopmentTeam = 372S63A2R8; 211 | LastSwiftMigration = 1500; 212 | }; 213 | }; 214 | }; 215 | buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "CustomMenus" */; 216 | compatibilityVersion = "Xcode 3.2"; 217 | developmentRegion = en; 218 | hasScannedForEncodings = 1; 219 | knownRegions = ( 220 | Base, 221 | en, 222 | fr, 223 | ja, 224 | de, 225 | ); 226 | mainGroup = 29B97314FDCFA39411CA2CEA /* CustomMenus */; 227 | projectDirPath = ""; 228 | projectRoot = ""; 229 | targets = ( 230 | 8D1107260486CEB800E47090 /* CustomMenus */, 231 | ); 232 | }; 233 | /* End PBXProject section */ 234 | 235 | /* Begin PBXResourcesBuildPhase section */ 236 | 8D1107290486CEB800E47090 /* Resources */ = { 237 | isa = PBXResourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, 241 | 210836342AA3E791006B0411 /* SUGMainWindowController.storyboard in Resources */, 242 | 1DDD58160DA1D0A300B32029 /* MainMenu.xib in Resources */, 243 | 21E7837F2AA17C0F0027D003 /* Assets.xcassets in Resources */, 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | }; 247 | /* End PBXResourcesBuildPhase section */ 248 | 249 | /* Begin PBXSourcesBuildPhase section */ 250 | 8D11072C0486CEB800E47090 /* Sources */ = { 251 | isa = PBXSourcesBuildPhase; 252 | buildActionMask = 2147483647; 253 | files = ( 254 | 210836332AA3E791006B0411 /* SUGCustomMenusAppDelegate.swift in Sources */, 255 | 2108362C2AA3E774006B0411 /* SUGSuggestionGenerator.swift in Sources */, 256 | 2108362B2AA3E774006B0411 /* SUGSuggestion.swift in Sources */, 257 | 210836442AA3E7D7006B0411 /* SUGSuggestionView.swift in Sources */, 258 | 210836322AA3E791006B0411 /* SUGMainWindowController.swift in Sources */, 259 | 2108363A2AA3E7AE006B0411 /* NSSearchField+cellClass.swift in Sources */, 260 | 210836392AA3E7AE006B0411 /* SUGSearchFieldCell.swift in Sources */, 261 | 214C5DA72AA8075900620EDD /* SUGMainWindowController+NSSearchFieldDelegate.swift in Sources */, 262 | 214C5DA92AA8079A00620EDD /* SUGMainWindowController+NSToolbarDelegate.swift in Sources */, 263 | 214C5DAB2AA807C600620EDD /* NSToolbarItemIdentifier+SearchItem.swift in Sources */, 264 | 210836452AA3E7D7006B0411 /* SUGSuggestionListWindow.swift in Sources */, 265 | 210836412AA3E7D7006B0411 /* SUGSuggestionListContentView.swift in Sources */, 266 | 210836432AA3E7D7006B0411 /* SUGSuggestionListWindowController.swift in Sources */, 267 | 210836472AA3E83C006B0411 /* NSImage+SUGMask.swift in Sources */, 268 | 210836422AA3E7D7006B0411 /* SUGSuggestionViewController.swift in Sources */, 269 | 210836352AA3E791006B0411 /* SUGMainWindowContentViewController.swift in Sources */, 270 | ); 271 | runOnlyForDeploymentPostprocessing = 0; 272 | }; 273 | /* End PBXSourcesBuildPhase section */ 274 | 275 | /* Begin PBXVariantGroup section */ 276 | 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { 277 | isa = PBXVariantGroup; 278 | children = ( 279 | 218404512A9FD3D700A7CBF8 /* en */, 280 | ); 281 | name = InfoPlist.strings; 282 | sourceTree = ""; 283 | }; 284 | 1DDD58140DA1D0A300B32029 /* MainMenu.xib */ = { 285 | isa = PBXVariantGroup; 286 | children = ( 287 | 2184044E2A9FD3D400A7CBF8 /* Base */, 288 | ); 289 | name = MainMenu.xib; 290 | sourceTree = ""; 291 | }; 292 | /* End PBXVariantGroup section */ 293 | 294 | /* Begin XCBuildConfiguration section */ 295 | C01FCF4B08A954540054247B /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ALWAYS_SEARCH_USER_PATHS = NO; 299 | CLANG_ENABLE_OBJC_WEAK = YES; 300 | CODE_SIGN_ENTITLEMENTS = CustomMenus.entitlements; 301 | CODE_SIGN_IDENTITY = "Mac Developer"; 302 | COMBINE_HIDPI_IMAGES = YES; 303 | COPY_PHASE_STRIP = NO; 304 | DEAD_CODE_STRIPPING = YES; 305 | DEVELOPMENT_TEAM = 372S63A2R8; 306 | ENABLE_HARDENED_RUNTIME = YES; 307 | GCC_DYNAMIC_NO_PIC = NO; 308 | GCC_MODEL_TUNING = G5; 309 | GCC_OPTIMIZATION_LEVEL = 0; 310 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 311 | GCC_PREFIX_HEADER = CustomMenus_Prefix.pch; 312 | INFOPLIST_FILE = "CustomMenus-Info.plist"; 313 | INSTALL_PATH = "$(HOME)/Applications"; 314 | LD_RUNPATH_SEARCH_PATHS = ( 315 | "$(inherited)", 316 | "@executable_path/../Frameworks", 317 | ); 318 | MACOSX_DEPLOYMENT_TARGET = 13.5; 319 | PRODUCT_BUNDLE_IDENTIFIER = "com.yourcompany.${PRODUCT_NAME:rfc1034identifier}"; 320 | PRODUCT_NAME = CustomMenus; 321 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 322 | SWIFT_VERSION = 5.0; 323 | }; 324 | name = Debug; 325 | }; 326 | C01FCF4C08A954540054247B /* Release */ = { 327 | isa = XCBuildConfiguration; 328 | buildSettings = { 329 | ALWAYS_SEARCH_USER_PATHS = NO; 330 | CLANG_ENABLE_OBJC_WEAK = YES; 331 | CODE_SIGN_ENTITLEMENTS = CustomMenus.entitlements; 332 | CODE_SIGN_IDENTITY = "Mac Developer"; 333 | COMBINE_HIDPI_IMAGES = YES; 334 | DEAD_CODE_STRIPPING = YES; 335 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 336 | DEVELOPMENT_TEAM = 372S63A2R8; 337 | ENABLE_HARDENED_RUNTIME = YES; 338 | GCC_MODEL_TUNING = G5; 339 | GCC_OPTIMIZATION_LEVEL = 0; 340 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 341 | GCC_PREFIX_HEADER = CustomMenus_Prefix.pch; 342 | INFOPLIST_FILE = "CustomMenus-Info.plist"; 343 | INSTALL_PATH = "$(HOME)/Applications"; 344 | LD_RUNPATH_SEARCH_PATHS = ( 345 | "$(inherited)", 346 | "@executable_path/../Frameworks", 347 | ); 348 | MACOSX_DEPLOYMENT_TARGET = 13.5; 349 | PRODUCT_BUNDLE_IDENTIFIER = "com.yourcompany.${PRODUCT_NAME:rfc1034identifier}"; 350 | PRODUCT_NAME = CustomMenus; 351 | SWIFT_VERSION = 5.0; 352 | }; 353 | name = Release; 354 | }; 355 | C01FCF4F08A954540054247B /* Debug */ = { 356 | isa = XCBuildConfiguration; 357 | buildSettings = { 358 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 359 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 360 | CLANG_WARN_BOOL_CONVERSION = YES; 361 | CLANG_WARN_COMMA = YES; 362 | CLANG_WARN_CONSTANT_CONVERSION = YES; 363 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 364 | CLANG_WARN_EMPTY_BODY = YES; 365 | CLANG_WARN_ENUM_CONVERSION = YES; 366 | CLANG_WARN_INFINITE_RECURSION = YES; 367 | CLANG_WARN_INT_CONVERSION = YES; 368 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 369 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 370 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 371 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 372 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 373 | CLANG_WARN_STRICT_PROTOTYPES = YES; 374 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 375 | CLANG_WARN_UNREACHABLE_CODE = YES; 376 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 377 | DEAD_CODE_STRIPPING = YES; 378 | ENABLE_STRICT_OBJC_MSGSEND = YES; 379 | ENABLE_TESTABILITY = YES; 380 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 381 | GCC_C_LANGUAGE_STANDARD = gnu99; 382 | GCC_NO_COMMON_BLOCKS = YES; 383 | GCC_OPTIMIZATION_LEVEL = 0; 384 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 385 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; 386 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 387 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; 388 | GCC_WARN_SHADOW = YES; 389 | GCC_WARN_SIGN_COMPARE = YES; 390 | GCC_WARN_UNDECLARED_SELECTOR = YES; 391 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 392 | GCC_WARN_UNUSED_FUNCTION = YES; 393 | GCC_WARN_UNUSED_LABEL = YES; 394 | GCC_WARN_UNUSED_VARIABLE = YES; 395 | MACOSX_DEPLOYMENT_TARGET = 13.5; 396 | ONLY_ACTIVE_ARCH = YES; 397 | RUN_CLANG_STATIC_ANALYZER = YES; 398 | SDKROOT = macosx; 399 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 400 | }; 401 | name = Debug; 402 | }; 403 | C01FCF5008A954540054247B /* Release */ = { 404 | isa = XCBuildConfiguration; 405 | buildSettings = { 406 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 407 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 408 | CLANG_WARN_BOOL_CONVERSION = YES; 409 | CLANG_WARN_COMMA = YES; 410 | CLANG_WARN_CONSTANT_CONVERSION = YES; 411 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 412 | CLANG_WARN_EMPTY_BODY = YES; 413 | CLANG_WARN_ENUM_CONVERSION = YES; 414 | CLANG_WARN_INFINITE_RECURSION = YES; 415 | CLANG_WARN_INT_CONVERSION = YES; 416 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 417 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 418 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 419 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 420 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 421 | CLANG_WARN_STRICT_PROTOTYPES = YES; 422 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 423 | CLANG_WARN_UNREACHABLE_CODE = YES; 424 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 425 | DEAD_CODE_STRIPPING = YES; 426 | ENABLE_STRICT_OBJC_MSGSEND = YES; 427 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 428 | GCC_C_LANGUAGE_STANDARD = gnu99; 429 | GCC_NO_COMMON_BLOCKS = YES; 430 | GCC_OPTIMIZATION_LEVEL = 0; 431 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 432 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; 433 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 434 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; 435 | GCC_WARN_SHADOW = YES; 436 | GCC_WARN_SIGN_COMPARE = YES; 437 | GCC_WARN_UNDECLARED_SELECTOR = YES; 438 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 439 | GCC_WARN_UNUSED_FUNCTION = YES; 440 | GCC_WARN_UNUSED_LABEL = YES; 441 | GCC_WARN_UNUSED_VARIABLE = YES; 442 | MACOSX_DEPLOYMENT_TARGET = 13.5; 443 | RUN_CLANG_STATIC_ANALYZER = YES; 444 | SDKROOT = macosx; 445 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 446 | }; 447 | name = Release; 448 | }; 449 | /* End XCBuildConfiguration section */ 450 | 451 | /* Begin XCConfigurationList section */ 452 | C01FCF4A08A954540054247B /* Build configuration list for PBXNativeTarget "CustomMenus" */ = { 453 | isa = XCConfigurationList; 454 | buildConfigurations = ( 455 | C01FCF4B08A954540054247B /* Debug */, 456 | C01FCF4C08A954540054247B /* Release */, 457 | ); 458 | defaultConfigurationIsVisible = 0; 459 | defaultConfigurationName = Release; 460 | }; 461 | C01FCF4E08A954540054247B /* Build configuration list for PBXProject "CustomMenus" */ = { 462 | isa = XCConfigurationList; 463 | buildConfigurations = ( 464 | C01FCF4F08A954540054247B /* Debug */, 465 | C01FCF5008A954540054247B /* Release */, 466 | ); 467 | defaultConfigurationIsVisible = 0; 468 | defaultConfigurationName = Release; 469 | }; 470 | /* End XCConfigurationList section */ 471 | }; 472 | rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; 473 | } 474 | -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (1)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (1)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (10)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (10)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (11)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (11)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (12)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (12)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (13)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (13)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (14)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (14)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (15)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (15)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (16)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (16)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (17)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (17)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (18)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (18)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (19)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (19)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (2)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (2)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (20)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (20)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (21)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (21)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (22)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (22)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (23)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (23)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (3)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (3)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (4)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (4)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (5)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (5)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (6)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (6)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (7)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (7)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (8)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (8)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (9)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26 (9)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (alpaca's conflicted copy 2018-03-26).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (leopard's conflicted copy 2018-03-26 (1)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (leopard's conflicted copy 2018-03-26 (1)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (leopard's conflicted copy 2018-03-26 (2)).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (leopard's conflicted copy 2018-03-26 (2)).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (leopard's conflicted copy 2018-03-26).xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/CustomMenus.xcodeproj/project.xcworkspace/xcuserdata/doug.xcuserdatad/UserInterfaceState (leopard's conflicted copy 2018-03-26).xcuserstate -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/xcuserdata/doug.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 20 | 21 | 22 | 24 | 36 | 37 | 51 | 52 | 66 | 67 | 68 | 69 | 70 | 72 | 84 | 85 | 99 | 100 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/xcuserdata/doug.xcuserdatad/xcschemes/CustomMenus.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /CustomMenus.xcodeproj/xcuserdata/doug.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CustomMenus.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 8D1107260486CEB800E47090 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Update June 13 2024 2 | 3 | I have not experimented with this yet, but it looks like the [Text entry suggestions](https://developer.apple.com/wwdc24/10124?time=1047) in Sequoia alleviate the need for this. If you can target Sequoia I recommend checking that out. 4 | 5 | # CustomMenus 6 | 7 | Many apps with search functionality have suggestion menus, letting an app provide specific suggestions as the user starts typing into a search field. 8 | 9 | ![Search suggestion menu from Safari](readme_images/sample_search_suggestion_menu.png "Search suggestion menu from Safari") 10 | 11 | AppKit does not have a standard control to provide such a menu. An app could popup an `NSMenu`, but there appears to be no way to let the search field get keystrokes while the menu is being presented. When such an `NSMenu` is visible, typing characters into the search stops working. 12 | 13 | This repository tries to provide sample code for providing such a suggestions menu. 14 | 15 | ## The App 16 | 17 | This app looks for image files in `/System/Library/Desktop Pictures`. Start entering the name of an image file, and the app will suggest a set of appropriate images. Select one of the suggestions and the app will display that image. 18 | 19 | ![This sample app showing a suggestion menu](readme_images/screenshot_app.png "This sample app showing a suggestion menu") 20 | 21 | ## How it works 22 | 23 | The suggestion menu is contained in a transparent borderless child window. The child window controller takes care of mouse events and gets relevant keyboard information from the search field cell. 24 | 25 | ## History 26 | 27 | Apple published [CustomMenus sample code](https://developer.apple.com/library/archive/samplecode/CustomMenus/Introduction/Intro.html) in 2012 to provide functionality like this. 28 | 29 | In 2018 [Doug Stein](https://github.com/dougzilla32) ported that sample code from Objective-C to Swift using Swiftify, and added a variety of fixes. The result of that work is [available on GitHub](https://github.com/dougzilla32/CustomMenus). 30 | 31 | When I needed to implement a suggestion menu in 2023, this code made that feasible. However some functionality did not work as well on Sonoma as it undoubtedly did in 2012 and 2018. So I put some effort into modernizing the codebase and focusing it on a suggestions menu. 32 | 33 | ## Notes 34 | 35 | * The sample app is not sandboxed because it needs access to `/System/Library/Desktop Pictures`. The menu generation code itself works in the sandbox without additional entitlements. 36 | * `SUGSuggestionGenerator.swift` contains the code that determines the list of suggestions. 37 | * This supports light mode and dark mode. 38 | * This sample app shows and updates the suggestion window immediately after a key is pressed in the search field. In my own app I wait a short amount of time (0.2 milliseconds for now) to let the user continue typing if they have not paused. The benefits of waiting are that the menu is not just flashing while the user is typing, and that the processor is not wasting cycles updating menu contents. The downside is it makes the suggestion menu appear slow to update. 39 | * VoiceOver works, although there is [one issue](https://github.com/jbrayton/CustomMenus/issues/1) for which I have not found a solution. Suggestions welcome. 40 | * Pull requests are welcome. 41 | 42 | ## Thank you 43 | 44 | I could not have done this work myself. Thank you to: 45 | 46 | * [Doug Stein](https://github.com/dougzilla32) for [his work to modernize this code in 2018](https://github.com/dougzilla32/CustomMenus). 47 | * [Lucas Derraugh](https://derraugh.com/), [Daniel Jalkut](https://redsweater.com/), [Gui Rambo](https://www.rambo.codes/), [Christian Tietze](https://christiantietze.de/), and [Nate Weaver](https://wevah.com/) for helping me with this in the AppKit Abusers Slack. 48 | * Apple for the original [CustomMenus sample code](https://developer.apple.com/library/archive/samplecode/CustomMenus/Introduction/Intro.html) sample code. 49 | -------------------------------------------------------------------------------- /Search Field/NSSearchField+cellClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSSearchField+cellClass.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 9/1/23. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSSearchField { 11 | 12 | // I worry that this has the potential to conflict with an NSSearchField.cellClass method 13 | // provided by the system. 14 | static public override var cellClass: AnyClass? { 15 | get { 16 | return SUGSearchFieldCell.self 17 | } 18 | set { 19 | 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Search Field/SUGSearchFieldCell.swift: -------------------------------------------------------------------------------- 1 | // Converted to Swift 4 by Swiftify v4.1.6654 - https://objectivec2swift.com/ 2 | /* 3 | File: SuggestibleTextFieldCell.m 4 | Abstract: A custom text field cell to perform two tasks. Draw with white text on a dark background, and expose any associated suggestion window as our accessibility child. 5 | Version: 1.4 6 | Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple 7 | Inc. ("Apple") in consideration of your agreement to the following 8 | terms, and your use, installation, modification or redistribution of 9 | this Apple software constitutes acceptance of these terms. If you do 10 | not agree with these terms, please do not use, install, modify or 11 | redistribute this Apple software. 12 | In consideration of your agreement to abide by the following terms, and 13 | subject to these terms, Apple grants you a personal, non-exclusive 14 | license, under Apple's copyrights in this original Apple software (the 15 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 16 | Software, with or without modifications, in source and/or binary forms; 17 | provided that if you redistribute the Apple Software in its entirety and 18 | without modifications, you must retain this notice and the following 19 | text and disclaimers in all such redistributions of the Apple Software. 20 | Neither the name, trademarks, service marks or logos of Apple Inc. may 21 | be used to endorse or promote products derived from the Apple Software 22 | without specific prior written permission from Apple. Except as 23 | expressly stated in this notice, no other rights or licenses, express or 24 | implied, are granted by Apple herein, including but not limited to any 25 | patent rights that may be infringed by your derivative works or by other 26 | works in which the Apple Software may be incorporated. 27 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 28 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 29 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 30 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 31 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 32 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 33 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 34 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 35 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 36 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 37 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 38 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 39 | POSSIBILITY OF SUCH DAMAGE. 40 | Copyright (C) 2012 Apple Inc. All Rights Reserved. 41 | */ 42 | import Cocoa 43 | 44 | class SUGSearchFieldCell: NSSearchFieldCell { 45 | 46 | var suggestionsWindow: NSWindow? 47 | 48 | override func accessibilityIsIgnored() -> Bool { 49 | return false 50 | } 51 | 52 | override func accessibilityChildren() -> [Any]? { 53 | if let parentWindow = suggestionsWindow, let children = super.accessibilityChildren(), let descendant = NSAccessibility.unignoredDescendant(of: parentWindow) { 54 | return children + [descendant] 55 | } else { 56 | return super.accessibilityChildren() 57 | } 58 | } 59 | 60 | override func accessibilitySelectedChildren() -> [Any]? { 61 | if let selectedChildren = suggestionsWindow?.contentView?.accessibilitySelectedChildren() { 62 | return selectedChildren 63 | } else { 64 | return super.accessibilityChildren() 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Suggestion List Window/NSImage+SUGMask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+SUGMask.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 8/31/23. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSImage { 11 | 12 | /* 13 | Adapted from Sapozhnik Ivan’s answer at: 14 | https://stackoverflow.com/questions/32042385/nsvisualeffectview-with-rounded-corners 15 | */ 16 | static func SUG_mask(withCornerRadius radius: CGFloat) -> NSImage { 17 | let image = NSImage(size: NSSize(width: radius * 2, height: radius * 2), flipped: false) { 18 | NSBezierPath(roundedRect: $0, xRadius: radius, yRadius: radius).fill() 19 | NSColor.black.set() 20 | return true 21 | } 22 | 23 | image.capInsets = NSEdgeInsets(top: radius, left: radius, bottom: radius, right: radius) 24 | image.resizingMode = .stretch 25 | 26 | return image 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Suggestion List Window/SUGSuggestionListContentView.swift: -------------------------------------------------------------------------------- 1 | // Converted to Swift 4 by Swiftify v4.1.6654 - https://objectivec2swift.com/ 2 | /* 3 | File: RoundedCornersView.m 4 | Abstract: A view that draws a rounded rect with the window background. It is used to draw the background for the suggestions window and expose the suggestions to accessibility. 5 | Version: 1.4 6 | Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple 7 | Inc. ("Apple") in consideration of your agreement to the following 8 | terms, and your use, installation, modification or redistribution of 9 | this Apple software constitutes acceptance of these terms. If you do 10 | not agree with these terms, please do not use, install, modify or 11 | redistribute this Apple software. 12 | In consideration of your agreement to abide by the following terms, and 13 | subject to these terms, Apple grants you a personal, non-exclusive 14 | license, under Apple's copyrights in this original Apple software (the 15 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 16 | Software, with or without modifications, in source and/or binary forms; 17 | provided that if you redistribute the Apple Software in its entirety and 18 | without modifications, you must retain this notice and the following 19 | text and disclaimers in all such redistributions of the Apple Software. 20 | Neither the name, trademarks, service marks or logos of Apple Inc. may 21 | be used to endorse or promote products derived from the Apple Software 22 | without specific prior written permission from Apple. Except as 23 | expressly stated in this notice, no other rights or licenses, express or 24 | implied, are granted by Apple herein, including but not limited to any 25 | patent rights that may be infringed by your derivative works or by other 26 | works in which the Apple Software may be incorporated. 27 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 28 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 29 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 30 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 31 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 32 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 33 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 34 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 35 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 36 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 37 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 38 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 39 | POSSIBILITY OF SUCH DAMAGE. 40 | Copyright (C) 2012 Apple Inc. All Rights Reserved. 41 | */ 42 | import Cocoa 43 | 44 | class SUGSuggestionListContentView: NSView { 45 | 46 | let cornerRadius: CGFloat = 10.0 47 | 48 | required init?(coder decoder: NSCoder) { 49 | super.init(coder: decoder) 50 | } 51 | 52 | override init(frame: NSRect) { 53 | super.init(frame: frame) 54 | let visualEffectView = NSVisualEffectView() 55 | visualEffectView.translatesAutoresizingMaskIntoConstraints = false 56 | visualEffectView.blendingMode = .withinWindow 57 | visualEffectView.material = .menu 58 | visualEffectView.state = .active 59 | visualEffectView.maskImage = .SUG_mask(withCornerRadius: self.cornerRadius) 60 | self.addSubview(visualEffectView) 61 | self.addConstraints([ 62 | visualEffectView.topAnchor.constraint(equalTo: self.topAnchor), 63 | visualEffectView.leadingAnchor.constraint(equalTo: self.leadingAnchor), 64 | visualEffectView.trailingAnchor.constraint(equalTo: self.trailingAnchor), 65 | visualEffectView.bottomAnchor.constraint(equalTo: self.bottomAnchor), 66 | ]) 67 | } 68 | 69 | override func draw(_ dirtyRect: NSRect) { 70 | let cornerRadius: CGFloat = cornerRadius 71 | let borderPath = NSBezierPath(roundedRect: bounds, xRadius: cornerRadius, yRadius: cornerRadius) 72 | NSColor.windowBackgroundColor.setFill() 73 | borderPath.fill() 74 | } 75 | 76 | override var isFlipped: Bool { 77 | return true 78 | } 79 | 80 | // MARK: Accessibility 81 | 82 | /* 83 | This view contains the list of selections. It should be exposed to accessibility, and 84 | should report itself with the role 'AXList'. Because this is an NSView subclass, most 85 | of the basic accessibility behavior (accessibility parent, children, size, position, 86 | window, and more) is inherited from NSView. Note that even the role description attribute 87 | will update accordingly and its behavior does not need to be overridden. However, since 88 | the role AXList has a number of additional required attributes, we need to declare them 89 | and implement them. 90 | */ 91 | 92 | // Make sure we are reported by accessibility. NSView's default return value is YES. 93 | 94 | override func accessibilityIsIgnored() -> Bool { 95 | return false 96 | } 97 | 98 | override func accessibilityOrientation() -> NSAccessibilityOrientation { 99 | return .vertical 100 | } 101 | 102 | override func isAccessibilityEnabled() -> Bool { 103 | return true 104 | } 105 | 106 | override func accessibilityVisibleChildren() -> [Any]? { 107 | return self.accessibilityChildren() 108 | } 109 | 110 | override func accessibilityChildren() -> [Any]? { 111 | var result = [Any]() 112 | for child in self.subviews { 113 | if let child = child as? SUGSuggestionView { 114 | result.append(child) 115 | } 116 | } 117 | return result 118 | } 119 | 120 | override func accessibilitySelectedChildren() -> [Any]? { 121 | var selectedChildren = [AnyHashable]() 122 | if let accessibilityChildren = self.accessibilityChildren() { 123 | for element: Any in accessibilityChildren { 124 | if let control = element as? SUGSuggestionView { 125 | if control.highlighted { 126 | selectedChildren.append(control) 127 | } 128 | } 129 | } 130 | } 131 | return selectedChildren 132 | } 133 | 134 | } 135 | 136 | 137 | -------------------------------------------------------------------------------- /Suggestion List Window/SUGSuggestionListWindow.swift: -------------------------------------------------------------------------------- 1 | // Converted to Swift 4 by Swiftify v4.1.6654 - https://objectivec2swift.com/ 2 | /* 3 | File: SuggestionsWindow.m 4 | Abstract: A custom window that acts as a popup menu of sorts. Since this isn't semantically a window, we ignore it for accessibility purposes. However, we need to inform accessibility of the logical relationship between this window and it parent UI element in the parent window. 5 | Version: 1.4 6 | 7 | Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple 8 | Inc. ("Apple") in consideration of your agreement to the following 9 | terms, and your use, installation, modification or redistribution of 10 | this Apple software constitutes acceptance of these terms. If you do 11 | not agree with these terms, please do not use, install, modify or 12 | redistribute this Apple software. 13 | 14 | In consideration of your agreement to abide by the following terms, and 15 | subject to these terms, Apple grants you a personal, non-exclusive 16 | license, under Apple's copyrights in this original Apple software (the 17 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 18 | Software, with or without modifications, in source and/or binary forms; 19 | provided that if you redistribute the Apple Software in its entirety and 20 | without modifications, you must retain this notice and the following 21 | text and disclaimers in all such redistributions of the Apple Software. 22 | Neither the name, trademarks, service marks or logos of Apple Inc. may 23 | be used to endorse or promote products derived from the Apple Software 24 | without specific prior written permission from Apple. Except as 25 | expressly stated in this notice, no other rights or licenses, express or 26 | implied, are granted by Apple herein, including but not limited to any 27 | patent rights that may be infringed by your derivative works or by other 28 | works in which the Apple Software may be incorporated. 29 | 30 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 31 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 32 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 33 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 34 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 35 | 36 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 37 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 38 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 39 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 40 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 41 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 42 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 43 | POSSIBILITY OF SUCH DAMAGE. 44 | 45 | Copyright (C) 2012 Apple Inc. All Rights Reserved. 46 | 47 | */ 48 | import Cocoa 49 | 50 | class SUGSuggestionListWindow: NSWindow { 51 | 52 | var parentElement: Any? 53 | 54 | // Convenience initializer that removes the syleMask and backing parameters since 55 | // they are static values for this class. 56 | 57 | convenience init(contentRect: NSRect, defer flag: Bool) { 58 | self.init(contentRect: contentRect, styleMask: .borderless, backing: .buffered, defer: true) 59 | } 60 | 61 | // We still need to override the NSWindow designated initializer to properly setup our custom window. 62 | // This allows us to set the class of a window in IB to SuggestionWindow and still get the correct 63 | // properties (borderless and transparent). 64 | 65 | override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { 66 | 67 | // Regardless of what is passed via the styleMask paramenter, always create a NSBorderlessWindowMask window. 68 | super.init(contentRect: contentRect, styleMask: .borderless, backing: .buffered, defer: flag) 69 | 70 | // This window is always has a shadow and is transparent. Force those setting here. 71 | self.hasShadow = true 72 | self.backgroundColor = NSColor.clear 73 | self.isOpaque = false 74 | 75 | } 76 | 77 | // MARK: Accessibility 78 | 79 | /* 80 | This window is acting as a popup menu of sorts. Since this isn't semantically a window, 81 | we ignore it for accessibility purposes. Similarly, the parent of this window is its 82 | logical parent in the parent window. In this code sample, the text field, but essentially any 83 | UI element that is the logical 'parent' of the window. 84 | */ 85 | 86 | override func accessibilityIsIgnored() -> Bool { 87 | return true 88 | } 89 | 90 | override func accessibilityParent() -> Any? { 91 | return (parentElement != nil) ? NSAccessibility.unignoredAncestor(of: parentElement!): nil 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /Suggestion List Window/SUGSuggestionListWindowController.swift: -------------------------------------------------------------------------------- 1 | // Converted to Swift 4 by Swiftify v4.1.6654 - https://objectivec2swift.com/ 2 | /* 3 | File: SuggestionsWindowController.m 4 | Abstract: The controller for the suggestions popup window. This class handles creating, displaying, and event tracking of the suggestion popup window. 5 | Version: 1.4 6 | Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple 7 | Inc. ("Apple") in consideration of your agreement to the following 8 | terms, and your use, installation, modification or redistribution of 9 | this Apple software constitutes acceptance of these terms. If you do 10 | not agree with these terms, please do not use, install, modify or 11 | redistribute this Apple software. 12 | In consideration of your agreement to abide by the following terms, and 13 | subject to these terms, Apple grants you a personal, non-exclusive 14 | license, under Apple's copyrights in this original Apple software (the 15 | "Apple Software"), to use, reproduce, modify and redistribute the Apple 16 | Software, with or without modifications, in source and/or binary forms; 17 | provided that if you redistribute the Apple Software in its entirety and 18 | without modifications, you must retain this notice and the following 19 | text and disclaimers in all such redistributions of the Apple Software. 20 | Neither the name, trademarks, service marks or logos of Apple Inc. may 21 | be used to endorse or promote products derived from the Apple Software 22 | without specific prior written permission from Apple. Except as 23 | expressly stated in this notice, no other rights or licenses, express or 24 | implied, are granted by Apple herein, including but not limited to any 25 | patent rights that may be infringed by your derivative works or by other 26 | works in which the Apple Software may be incorporated. 27 | The Apple Software is provided by Apple on an "AS IS" basis. APPLE 28 | MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION 29 | THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS 30 | FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND 31 | OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. 32 | IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL 33 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 34 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 35 | INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, 36 | MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED 37 | AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), 38 | STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE 39 | POSSIBILITY OF SUCH DAMAGE. 40 | Copyright (C) 2012 Apple Inc. All Rights Reserved. 41 | */ 42 | import Cocoa 43 | 44 | let kTrackerKey = "whichImageView" 45 | 46 | class SUGSuggestionListWindowController: NSWindowController { 47 | 48 | let automaticallySelectFirstSuggestion: Bool 49 | var action: Selector? 50 | var target: Any? 51 | 52 | private var parentTextField: NSTextField? 53 | private var suggestions = [SUGSuggestion]() 54 | private var viewControllers = [NSViewController]() 55 | private var trackingAreas = [AnyHashable]() 56 | private var needsLayoutUpdate = false 57 | private var localMouseDownEventMonitor: Any? 58 | private var lostFocusObserver: Any? 59 | 60 | init( automaticallySelectFirstSuggestion: Bool ) { 61 | self.automaticallySelectFirstSuggestion = automaticallySelectFirstSuggestion 62 | let contentRec = NSRect(x: 0, y: 0, width: 20, height: 20) 63 | let window = SUGSuggestionListWindow(contentRect: contentRec, defer: true) 64 | super.init(window: window) 65 | 66 | // SuggestionsWindow is a transparent window, create RoundedCornersView and set it as the content view to draw a menu like window. 67 | let contentView = SUGSuggestionListContentView(frame: contentRec) 68 | window.contentView = contentView 69 | contentView.autoresizesSubviews = false 70 | needsLayoutUpdate = true 71 | } 72 | 73 | required init?(coder: NSCoder) { 74 | fatalError() 75 | } 76 | 77 | // Custom selectedView property setter so that we can set the highlighted property of the old and new selected views. 78 | 79 | private var selectedView: NSView? { 80 | didSet { 81 | (oldValue as? SUGSuggestionView)?.highlighted = false 82 | if let oldSelectedView = oldValue as? SUGSuggestionView { 83 | oldSelectedView.highlighted = false 84 | } 85 | if let newSelectedView = self.selectedView as? SUGSuggestionView { 86 | newSelectedView.highlighted = true 87 | if let cell = self.parentTextField?.cell { 88 | NSAccessibility.post(element: cell, notification: .selectedChildrenChanged) 89 | } 90 | } 91 | } 92 | } 93 | 94 | // Set selected view and send action. 95 | 96 | func userSetSelectedView(_ view: NSView?) { 97 | self.selectedView = view 98 | NSApp.sendAction(action!, to: target, from: self) 99 | } 100 | 101 | // Position and lay out the suggestion list window, set up auto cancelling tracking, and wire up the logical relationship for accessibility. 102 | 103 | func begin(for parentTextField: NSTextField?) { 104 | guard let suggestionWindow = window, let parentTextField, let parentWindow = parentTextField.window, let parentSuperview = parentTextField.superview else { 105 | return 106 | } 107 | 108 | // Make the menu extend 5 pixels out to the left and right of the search box. This 109 | // makes the menu item icons align vertically with the search icon. 110 | let horizontalNegativeInset: CGFloat = 5.0 111 | let parentFrame: NSRect = parentTextField.frame 112 | var frame: NSRect = suggestionWindow.frame 113 | frame.size.width = parentFrame.size.width + (2 * horizontalNegativeInset) 114 | 115 | // Place the suggestion window just underneath the text field and make it the same width as the text field. 116 | var location = parentSuperview.convert(parentFrame.origin, to: nil) 117 | location = parentWindow.convertToScreen(NSRect(x: location.x - horizontalNegativeInset, y: location.y - parentTextField.frame.size.height, width: 0, height: 0)).origin 118 | location.y -= 2.0 119 | 120 | // Nudge the suggestion list window down so that it does not overlap the parent view. 121 | suggestionWindow.setFrame(frame, display: false) 122 | suggestionWindow.setFrameTopLeftPoint(location) 123 | layoutSuggestions() 124 | 125 | // The height of the window will be adjusted in -layoutSuggestions. 126 | // add the suggestion window as a child window so that it plays nice with Expose 127 | parentWindow.addChildWindow(suggestionWindow, ordered: .above) 128 | 129 | // Keep track of the parent text field in case we need to commit or abort editing. 130 | self.parentTextField = parentTextField 131 | 132 | // The window must know its accessibility parent. The control must know the window and its accessibility children. 133 | // Note that views (controls especially) are often ignored, so we want the unignored descendant - usually a cell. 134 | // Finally, post that we have created the unignored decendant of the suggestions window 135 | let unignoredAccessibilityDescendant = NSAccessibility.unignoredDescendant(of: parentTextField) 136 | (suggestionWindow as? SUGSuggestionListWindow)?.parentElement = unignoredAccessibilityDescendant 137 | (unignoredAccessibilityDescendant as? SUGSearchFieldCell)?.suggestionsWindow = suggestionWindow 138 | if let unignoredAccessibilityDescendant { 139 | NSAccessibility.post(element: unignoredAccessibilityDescendant, notification: .created) 140 | } 141 | 142 | // Setup auto cancellation if the user clicks outside the suggestion window and parent text field. 143 | // Note: this is a local event monitor and will only catch clicks in windows that belong to this 144 | // application. We use another technique below to catch clicks in other application windows. 145 | localMouseDownEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [NSEvent.EventTypeMask.leftMouseDown, NSEvent.EventTypeMask.rightMouseDown, NSEvent.EventTypeMask.otherMouseDown], handler: { [weak self] (_ event: NSEvent) -> NSEvent? in 146 | // If the mouse event is in the suggestion window, then there is nothing to do. 147 | let event: NSEvent! = event 148 | if event.window != suggestionWindow { 149 | if event.window == parentWindow { 150 | 151 | // Clicks in the parent window should either be in the parent text field or dismiss the 152 | // suggestions window. We want clicks to occur in the parent text field so that the user 153 | // can move the caret or select the search text. 154 | 155 | // Use hit testing to determine if the click is in the parent text field. Note: when 156 | // editing an NSTextField, there is a field editor that covers the text field that 157 | // is performing the actual editing. Therefore, we need to check for the field editor 158 | // when doing hit testing. 159 | let contentView: NSView? = parentWindow.contentView 160 | let locationTest: NSPoint? = contentView?.convert(event.locationInWindow, from: nil) 161 | let hitView: NSView? = contentView?.hitTest(locationTest ?? NSPoint.zero) 162 | let fieldEditor: NSText? = parentTextField.currentEditor() 163 | if hitView != parentTextField && ((fieldEditor != nil) && hitView != fieldEditor) { 164 | // Since the click is not in the parent text field, return nil, so the parent window 165 | // does not try to process it, and cancel the suggestion window. 166 | self?.cancelSuggestions() 167 | } 168 | } else { 169 | // Not in the suggestion window, and not in the parent window. This must be another window 170 | // or palette for this application. 171 | self?.cancelSuggestions() 172 | } 173 | } 174 | return event 175 | }) 176 | 177 | // As per the documentation, do not retain event monitors. 178 | // We also need to auto cancel when the window loses key status. This may be done via a mouse click 179 | // in another window, or via the keyboard (cmd-~ or cmd-tab), or a notificaiton. Observing 180 | // NSWindowDidResignKeyNotification catches all of these cases and the mouse down event monitor 181 | // catches the other cases. 182 | lostFocusObserver = NotificationCenter.default.addObserver(forName: NSWindow.didResignKeyNotification, object: parentWindow, queue: nil, using: {(_ arg1: Notification) -> Void in 183 | // lost key status, cancel the suggestion window 184 | self.cancelSuggestions() 185 | }) 186 | } 187 | 188 | // Order out the suggestion window, disconnect the accessibility logical relationship and dismantle any 189 | // observers for auto cancel. 190 | // Note: It is safe to call this method even if the suggestions window is not currently visible. 191 | func cancelSuggestions() { 192 | if let suggestionWindow = self.window, let parentTextField = self.parentTextField, suggestionWindow.isVisible { 193 | if let unignoredAccessibilityDescendant = NSAccessibility.unignoredDescendant(of: parentTextField) { 194 | NSAccessibility.post(element: unignoredAccessibilityDescendant, notification: .uiElementDestroyed) 195 | } 196 | 197 | suggestionWindow.parent?.removeChildWindow(suggestionWindow) 198 | suggestionWindow.orderOut(nil) 199 | // Disconnect the accessibility parent/child relationship 200 | ((suggestionWindow as? SUGSuggestionListWindow)?.parentElement as? SUGSearchFieldCell)?.suggestionsWindow = nil 201 | (suggestionWindow as? SUGSuggestionListWindow)?.parentElement = nil 202 | } 203 | // Dismantle any observers for auto cancel. 204 | if lostFocusObserver != nil { 205 | NotificationCenter.default.removeObserver(lostFocusObserver!) 206 | lostFocusObserver = nil 207 | } 208 | if localMouseDownEventMonitor != nil { 209 | NSEvent.removeMonitor(localMouseDownEventMonitor!) 210 | localMouseDownEventMonitor = nil 211 | } 212 | } 213 | 214 | // Update the array of suggestions. 215 | 216 | func setSuggestions(_ suggestions: [SUGSuggestion]?) { 217 | self.suggestions = suggestions! 218 | // We only need to update the layout if the window is currently visible. 219 | if (window?.isVisible)! { 220 | layoutSuggestions() 221 | } 222 | } 223 | 224 | // Returns the dictionary of the currently selected suggestion. 225 | 226 | func selectedSuggestion() -> SUGSuggestion? { 227 | var suggestion: Any? = nil 228 | // Find the currently selected view's controller (if there is one) and return the representedObject which is the NSMutableDictionary that was passed in via -setSuggestions: 229 | let selectedView: NSView? = self.selectedView 230 | for viewController: NSViewController in viewControllers where selectedView == viewController.view { 231 | suggestion = viewController.representedObject 232 | break 233 | } 234 | return suggestion as? SUGSuggestion 235 | } 236 | 237 | // MARK: Mouse Tracking 238 | 239 | /* 240 | Mouse tracking is easily accomplished via tracking areas. We setup a tracking area for suggestion view 241 | and watch as the mouse moves in and out of those tracking areas. 242 | */ 243 | 244 | // Properly creates a tracking area for an image view. 245 | 246 | func trackingArea(for view: NSView?) -> Any? { 247 | // make tracking data (to be stored in NSTrackingArea's userInfo) so we can later determine the imageView without hit testing 248 | var trackerData: [AnyHashable: Any]? = nil 249 | if let aView = view { 250 | trackerData = [ 251 | kTrackerKey: aView 252 | ] 253 | } 254 | let trackingRect: NSRect = window!.contentView!.convert(view?.bounds ?? CGRect.zero, from: view) 255 | let trackingOptions: NSTrackingArea.Options = [.enabledDuringMouseDrag, .mouseEnteredAndExited, .activeInActiveApp] 256 | let trackingArea = NSTrackingArea(rect: trackingRect, options: trackingOptions, owner: self, userInfo: trackerData) 257 | return trackingArea 258 | } 259 | 260 | // Creates suggestion views for every suggestion and resize the suggestion window accordingly. 261 | 262 | private func layoutSuggestions() { 263 | let window: NSWindow? = self.window 264 | let contentView = window?.contentView as? SUGSuggestionListContentView 265 | // Remove any existing suggestion view and associated tracking area and set the selection to nil 266 | selectedView = nil 267 | for viewController in viewControllers { 268 | viewController.view.removeFromSuperview() 269 | } 270 | viewControllers.removeAll() 271 | for trackingArea in trackingAreas { 272 | if let nsTrackingArea = trackingArea as? NSTrackingArea { 273 | contentView?.removeTrackingArea(nsTrackingArea) 274 | } 275 | } 276 | trackingAreas.removeAll() 277 | 278 | // Iterate through each suggestion creating a view for each entry. 279 | // The width of each suggestion view should match the width of the window. 280 | var contentFrame: NSRect? = contentView?.frame 281 | let itemHeight: CGFloat = 20.0 282 | let topBottomMargin: CGFloat = 6.0 283 | var frame = NSRect(x: 0, y: topBottomMargin - itemHeight, width: contentFrame!.width, height: itemHeight) 284 | // Offset the Y posistion so that the suggestion view does not try to draw past the rounded corners. 285 | for entry in suggestions { 286 | frame.origin.y += frame.size.height 287 | let viewController = SUGSuggestionViewController() 288 | let view = viewController.view as! SUGSuggestionView 289 | if self.viewControllers.isEmpty, self.automaticallySelectFirstSuggestion { 290 | selectedView = view 291 | } 292 | view.frame = frame 293 | contentView?.addSubview(view) 294 | // Don't forget to create the tracking area. 295 | let trackingArea = self.trackingArea(for: view) as? NSTrackingArea 296 | if let anArea = trackingArea { 297 | contentView?.addTrackingArea(anArea) 298 | } 299 | viewController.representedObject = entry 300 | viewControllers.append(viewController) 301 | if let anArea = trackingArea { 302 | trackingAreas.append(anArea) 303 | } 304 | } 305 | // We have added all of the suggestion to the window. Now set the size of the window. 306 | // Don’t forget to account for the extra room needed the rounded corners. 307 | contentFrame?.size.height = frame.maxY + topBottomMargin 308 | var winFrame: NSRect = NSRect(origin: window!.frame.origin, size: window!.frame.size) 309 | winFrame.origin.y = winFrame.maxY - contentFrame!.height 310 | winFrame.size.height = contentFrame!.height 311 | window?.setFrame(winFrame, display: true) 312 | } 313 | 314 | // The mouse is now over one of our child image views. Update selection and send action. 315 | 316 | override func mouseEntered(with event: NSEvent) { 317 | let view: NSView? 318 | if let userData = event.trackingArea?.userInfo as? [String: NSView] { 319 | view = userData[kTrackerKey]! 320 | } else { 321 | view = nil 322 | } 323 | userSetSelectedView(view) 324 | } 325 | 326 | // The mouse has left one of our child image views. Set the selection to no selection and send action 327 | 328 | override func mouseExited(with event: NSEvent) { 329 | userSetSelectedView(nil) 330 | } 331 | 332 | // The user released the mouse button. Force the parent text field to send its return action. 333 | // Notice that there is no mouseDown: implementation. That is because the user may hold the 334 | // mouse down and drag into another view. 335 | 336 | override func mouseUp(with theEvent: NSEvent) { 337 | parentTextField?.validateEditing() 338 | parentTextField?.abortEditing() 339 | parentTextField?.sendAction(parentTextField?.action, to: parentTextField?.target) 340 | cancelSuggestions() 341 | } 342 | 343 | override func mouseDragged(with event: NSEvent) { 344 | super.mouseDragged(with: event) 345 | if let contentView = self.window?.contentView as? SUGSuggestionListContentView { 346 | let viewPoint = contentView.convert(event.locationInWindow, from: contentView) 347 | if viewPoint.x >= contentView.bounds.origin.x && viewPoint.x <= contentView.bounds.origin.x + contentView.bounds.size.width { 348 | let subviews = contentView.subviews 349 | let y = contentView.frame.size.height - viewPoint.y 350 | for subview in subviews { 351 | let frame = subview.frame 352 | if let subview = subview as? SUGSuggestionView { 353 | if frame.origin.y <= y && frame.origin.y + frame.size.height >= y { 354 | userSetSelectedView(subview) 355 | return 356 | } 357 | } 358 | } 359 | } 360 | userSetSelectedView(nil) 361 | } 362 | } 363 | 364 | // MARK: Keyboard Tracking 365 | 366 | /* 367 | In addition to tracking the mouse, we want to allow changing our selection via the keyboard. 368 | However, the suggestion window never gets key focus as the key focus remains on te text field. 369 | Therefore we need to route move up and move down action commands from the text field and 370 | this controller. See CustomMenuAppDelegate.m -control:textView:doCommandBySelector: to see how 371 | that is done. 372 | */ 373 | 374 | // Move the selection up and send action. 375 | 376 | override func moveUp(_ sender: Any?) { 377 | let selectedView: NSView? = self.selectedView 378 | var previousView: NSView? = nil 379 | var viewWasSelected = false 380 | for viewController: NSViewController in viewControllers { 381 | let view: NSView? = viewController.view 382 | if view == selectedView { 383 | viewWasSelected = true 384 | break 385 | } 386 | previousView = view 387 | } 388 | if viewWasSelected { 389 | userSetSelectedView(previousView) 390 | } 391 | } 392 | 393 | // Move the selection down and send action. 394 | 395 | override func moveDown(_ sender: Any?) { 396 | let selectedView: NSView? = self.selectedView 397 | var previousView: NSView? = nil 398 | for viewController: NSViewController in viewControllers.reversed() { 399 | let view: NSView? = viewController.view 400 | if view == selectedView { 401 | break 402 | } 403 | previousView = view 404 | } 405 | if previousView != nil { 406 | userSetSelectedView(previousView) 407 | } 408 | } 409 | 410 | } 411 | -------------------------------------------------------------------------------- /Suggestion List Window/SUGSuggestionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUGIndividualSuggestionView.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 8/31/23. 6 | // 7 | 8 | import AppKit 9 | 10 | class SUGSuggestionView : NSView { 11 | 12 | let highlightSideMargin: CGFloat = 7.0 13 | let sideMargin: CGFloat = 6.0 14 | let imageSize: CGFloat = 13.0 15 | let spaceBetweenLabelAndImage: CGFloat = 6.0 16 | 17 | var imageView: NSImageView! 18 | var backgroundView: NSVisualEffectView! 19 | var label: NSTextField! 20 | 21 | var highlighted: Bool = false { 22 | didSet { 23 | self.backgroundView.material = self.highlighted ? .selection : .menu 24 | self.backgroundView.isEmphasized = self.highlighted 25 | self.backgroundView.state = self.highlighted ? .active : .inactive 26 | self.label.cell?.backgroundStyle = self.highlighted ? .emphasized : .normal 27 | self.imageView.cell?.backgroundStyle = self.highlighted ? .emphasized : .normal 28 | } 29 | } 30 | 31 | init() { 32 | super.init(frame: .zero) 33 | 34 | self.backgroundView = NSVisualEffectView() 35 | self.backgroundView.translatesAutoresizingMaskIntoConstraints = false 36 | self.backgroundView.maskImage = NSImage.SUG_mask(withCornerRadius: 4.0) 37 | self.addSubview(self.backgroundView) 38 | self.addConstraints([ 39 | self.backgroundView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: self.highlightSideMargin), 40 | self.backgroundView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0.0 - self.highlightSideMargin), 41 | self.backgroundView.topAnchor.constraint(equalTo: self.topAnchor), 42 | self.backgroundView.bottomAnchor.constraint(equalTo: self.bottomAnchor), 43 | ]) 44 | 45 | self.imageView = NSImageView() 46 | self.imageView.translatesAutoresizingMaskIntoConstraints = false 47 | self.backgroundView.addSubview(self.imageView) 48 | self.backgroundView.addConstraints([ 49 | self.imageView.leadingAnchor.constraint(equalTo: self.backgroundView.leadingAnchor, constant: self.sideMargin), 50 | self.imageView.centerYAnchor.constraint(equalTo: self.backgroundView.centerYAnchor), 51 | self.imageView.widthAnchor.constraint(equalToConstant: self.imageSize), 52 | self.imageView.heightAnchor.constraint(equalToConstant: self.imageSize), 53 | ]) 54 | self.imageView.contentTintColor = NSColor.labelColor 55 | 56 | self.label = NSTextField(labelWithString: "") 57 | self.label.translatesAutoresizingMaskIntoConstraints = false 58 | self.label.lineBreakMode = .byTruncatingTail 59 | self.backgroundView.addSubview(self.label) 60 | self.backgroundView.addConstraints([ 61 | self.label.leadingAnchor.constraint(equalTo: self.imageView.trailingAnchor, constant: self.spaceBetweenLabelAndImage), 62 | self.label.trailingAnchor.constraint(equalTo: self.backgroundView.trailingAnchor, constant: 0.0 - self.sideMargin), 63 | self.label.centerYAnchor.constraint(equalTo: self.backgroundView.centerYAnchor), 64 | ]) 65 | } 66 | 67 | required init?(coder: NSCoder) { 68 | fatalError("init(coder:) has not been implemented") 69 | } 70 | 71 | override func accessibilityChildren() -> [Any]? { 72 | return [Any]() 73 | } 74 | 75 | override func accessibilityLabel() -> String? { 76 | return self.label.stringValue 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Suggestion List Window/SUGSuggestionViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUGIndividualSuggestionViewController.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 8/31/23. 6 | // 7 | 8 | import AppKit 9 | 10 | class SUGSuggestionViewController : NSViewController { 11 | 12 | init() { 13 | super.init(nibName: nil, bundle: nil) 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | fatalError("init(coder:) has not been implemented") 18 | } 19 | 20 | override func loadView() { 21 | self.view = SUGSuggestionView() 22 | } 23 | 24 | override var representedObject: Any? { 25 | didSet { 26 | if let representedObject = self.representedObject as? SUGSuggestion, let view = self.view as? SUGSuggestionView { 27 | view.label.stringValue = representedObject.name 28 | view.imageView.image = NSImage(named: representedObject.imageName) 29 | } 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Suggestions/SUGSuggestion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUGSuggestion.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 8/31/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct SUGSuggestion { 11 | 12 | let name: String 13 | let url: URL 14 | let imageName: String 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Suggestions/SUGSuggestionGenerator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SUGSuggestionGenerator.swift 3 | // CustomMenus 4 | // 5 | // Created by John Brayton on 8/31/23. 6 | // 7 | 8 | import Foundation 9 | import UniformTypeIdentifiers 10 | 11 | class SUGSuggestionGenerator { 12 | 13 | private let kDesktopPicturesPath = "/System/Library/Desktop Pictures" 14 | 15 | // If true and if there is at least one applicable suggestion, the first suggestion will be immediately selected. 16 | // The search field will be populated with the name of that suggestion. Pressing return will execute that suggestion. 17 | // If false, the user must select a suggestion with the arrow keys or mouse in order to execute a selection. 18 | let automaticallySelectFirstSuggestion = false 19 | 20 | private lazy var imageUrls: [URL] = { 21 | var result = [URL]() 22 | let baseURL = URL(filePath: kDesktopPicturesPath) 23 | let keyProperties: [URLResourceKey] = [.isDirectoryKey, .typeIdentifierKey, .localizedNameKey] 24 | let dirItr: FileManager.DirectoryEnumerator? = FileManager.default.enumerator(at: baseURL, includingPropertiesForKeys: keyProperties, options: [.skipsPackageDescendants, .skipsHiddenFiles], errorHandler: nil) 25 | while let file = dirItr?.nextObject() as? URL { 26 | var isDirectory: NSNumber? = nil 27 | try? isDirectory = ((file.resourceValues(forKeys: [.isDirectoryKey]).allValues.first?.value ?? "") as? NSNumber) 28 | if isDirectory != nil && isDirectory! == 0 { 29 | var fileType: String? = nil 30 | try? fileType = ((file.resourceValues(forKeys: [.typeIdentifierKey]).allValues.first?.value ?? "") as? String) 31 | if let fileType, UTType(fileType)?.conforms(to: UTType.image) == true { 32 | result.append(file) 33 | } 34 | } 35 | } 36 | return result 37 | }() 38 | 39 | func suggestions( forSearchString searchString: String ) -> [SUGSuggestion] { 40 | 41 | guard !searchString.isEmpty else { 42 | return [SUGSuggestion]() 43 | } 44 | 45 | // Search the known image URLs array for matches. 46 | var suggestions = [SUGSuggestion]() 47 | suggestions.reserveCapacity(1) 48 | let upperSearchString = searchString.uppercased() 49 | for hashableFile: AnyHashable in imageUrls { 50 | guard let file = hashableFile as? URL else { 51 | continue 52 | } 53 | if let localizedName = try? ((file.resourceValues(forKeys: [.localizedNameKey]).allValues.first?.value ?? "") as? String) { 54 | if (localizedName.hasPrefix(searchString) || localizedName.uppercased().hasPrefix(upperSearchString)) { 55 | 56 | // Assign the first suggestion a square image, each subsequent suggestion a circle image. 57 | let imageName = suggestions.isEmpty ? "square" : "circle" 58 | let entry = SUGSuggestion(name: localizedName, url: file, imageName: imageName) 59 | suggestions.append(entry) 60 | } 61 | } 62 | } 63 | return suggestions 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | CFBundleGetInfoString = "v1.4, Copyright © 2011-2012 Apple Inc."; 4 | NSHumanReadableCopyright = "Copyright © 2011-2012, Apple Inc."; -------------------------------------------------------------------------------- /readme_images/sample_search_suggestion_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/readme_images/sample_search_suggestion_menu.png -------------------------------------------------------------------------------- /readme_images/screenshot_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jbrayton/CustomMenus/ebb88c3c87e9461410882fab31c91dde4bef9309/readme_images/screenshot_app.png --------------------------------------------------------------------------------