├── .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 |
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 | 
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 | 
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
--------------------------------------------------------------------------------