Creating 1Password-like macOS menubar app - swift

I am trying to create a macOS menubar app which will have a text field as the first item. The textfield will serve as a search bar for filtering other items which will be displayed below it.
It should look very similar to 1password:
This is what I managed to do:
I have accomplished this by creating a Status menu with three items and creating a custom view for the first item in the menu.
However, this approach does not seem to solve my issue. When pressing cmd + A in the search field, the focus jumps to the next item in the menu. This is the default behaviour for NSMenu.
So, my question is: Is this the right approach to create a 1Password-like app or is there a better one?

Basically the approach is correct.
But you have to catch the edit key events explicitly. Subclass NSTextField and override performKeyEquivalent
class AXCVTextField: NSTextField {
override func performKeyEquivalent(with event: NSEvent) -> Bool {
if event.modifierFlags.contains(.command),
let key = event.charactersIgnoringModifiers {
var action : String?
switch key {
case "x": action = "cut:"
case "c": action = "copy:"
case "v": action = "paste:"
case "a": action = "selectAll:"
default:
break
}
if let action = action {
return NSApp.sendAction(Selector(action), to:self.window!.firstResponder, from:self)
}
}
return super.performKeyEquivalent(with: event)
}
}

Related

How to Implement accessibilityCustomActions for VoiceOver on Mac?

I have a button that responds to various mouse clicks (regular click, right click, control+click, option+click, command+click...) to show different popup menus. Since it would be annoying for VoiceOver users to use actual physical mouse, I would like to map those to different VoiceOver actions.
However, I'm not getting the results I expected. Could someone help me to understand better what I'm missing? Here is what I discovered so far.
If I subclass NSButton and override the following functions, they work fine. Except there's one odd thing. If I press vo+command+space to bring up the list of available actions, VoiceOver says Action 1 instead of Show Menu.
override func accessibilityPerformPress() -> Bool {
print("Pressed!")
return true
}
override func accessibilityPerformShowAlternateUI() -> Bool {
print("Show Alternate UI")
return true
}
override func accessibilityPerformShowMenu() -> Bool {
print("Show Menu")
return true
}
In the same NSButton subclass, if I also override accessibilityCustomActions function, "Do Something" never comes up in the list of available actions when I press vo+command+space.
override func accessibilityCustomActions() -> [NSAccessibilityCustomAction]? {
let custom = NSAccessibilityCustomAction(name: "Do Something", target: self, selector: #selector(doSomething))
return [custom]
}
#objc func doSomething() -> Bool {
print("Done something.")
return true
}
If I subclass NSView instead of NSButton, and override the same functions from #1, everything works fine. Unlike first case, even VoiceOver correctly says "Show Menu" for the action from accessibilityPerformShowMenu instead of "Action 1".
in the same NSView subclass, if I override accessibilityCustomActions along with accessibilityPerformPress, accessibilityPerformShowMenu, or accessibilityPerformShowAlternateUI, "Do Something" doesn't come up in the action list.
However, "Do Something" does come up in the action list if I just override accessibilityCustomActions by itself without accessibilityPerformPress, accessibilityPerformShowMenu, and accessibilityPerformShowAlternateUI.
I tried creating another action with the name "Press" that does the same thing when pressing vo+space, and including in the return value of accessibilityCustomActions. However, Vo+space did not trigger the action. Instead, I had to press vo+command+space, and then select "Press". I guess the action just has the name "Press", but it's not actually connected to vo+space. I'm not sure how I can actually make that particular custom action to respond to vo+space.
I would appreciate if someone could help me to implement accessibilityCustomActions as well as accessibilityPerformPress, accessibilityPerformShowMenu, and accessibilityPerformShowAlternateUI together into NSButton.
Thanks so much!
The problem is that you are overriding these AX methods on the NSButton, not the NSButtonCell. For nearly everything to do with accessibility in NSControls, you will want to deal with the NSCell in question. If you use the custom action code you've written above and stick it in a subclass of NSButtonCell used by your button, then it will work.

Looping through entities which describe what action should be taken on screen after keypresses

Please forgive me if I don't describe this question too well, I am new to programming MacOS apps using Swift. I know the way I'm going about this is probably wrong and I just need someone to tell me the right way.
My main app screen
I have a Core Data application that stores an ordered list of entities called Items. These Items are intended to describe a single step in an activity that describes what should happen on screen. If you know the Mac application QLab each Item is like a single cue in QLab.
I have created an Activity class that is designed to read through each Item to determine the Item type and it's related information. Once the Item type has been determined the Activity class needs to present a View with information related to that particular Item and then wait until the user presses the right arrow key to then proceed to the next Item in the Core Data store where the process repeats until all Items have been read. Each time a new Item is read in the loop, the information on the screen should change after the user presses the right arrow each time.
The problem is that I don't know exactly how the best way of going about this should be programatically speaking. I have the code that retrieves the array of Items as an NSFetchRequest:
let moc = (NSApplication.shared.mainWindow?.contentViewController?.representedObject as! NSPersistentDocument).managedObjectContext!
let fetchRequest : NSFetchRequest = Item.fetchRequest()
do {
let items = try moc.fetch(fetchRequest)
print("Found " + String(items.count) + " items to use in the activity.")
for item in items {
print(item.itemType)
// How do I pause this loop for a user keypress after using data from this Item to display?
}
} catch {
print("Error retrieving Items")
}
I can retrieve the keydown event using NSEvent.addLocalMonitorForEvents(matching: .keyDown) and I'm also able to create View Controllers to display the information on a second screen. I just don't know how I should create the 'main loop', so to speak, so that information is displayed and then the app waits until the user presses a key to proceed...
I can share my project code if more information is needed and many thanks to anyone who can enlighten me... :)
You could try using a NSPageController. In your NSPageController you add a ContainerView which will display the ViewControllers that display information for each item. Each ViewController will need a storyboard identifier, e.g. ViewControllerItem1.
Your NSPageController class must conform to the NSPageControllerDelegate protocol and contains an array of ViewControllers to display.
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
arrangedObjects = ["ViewControllerItem1", "ViewControllerItem2", "...","ViewControllerItemN" ]
}
Note about arrangedObjects from the NSPageController documentation: An array containing the objects displayed in the page controller’s view.
Then you implement NSPageControllers viewControllerForIdentifier to return the ViewController that you currently want to display in the ContainerView.
func pageController(_ pageController: NSPageController, viewControllerForIdentifier identifier: String) -> NSViewController {
switch identifier {
case "ViewControllerItem1":
return mainStoryboard().instantiateController(withIdentifier:"ViewControllerItem1") as? ViewControllerItem1
case "...":
default:
}
}
In your action handler for the key down event you implement.
self.navigateForward(sender) or self.navigateBack(sender)
I also implemented this method but I don't remember whether it was required.
func pageControllerDidEndLiveTransition(_ pageController: NSPageController) {
self.completeTransition()
}

comboBoxSelectionDidChange with multiple NScomboboxes

I have an app with two combo boxes in the same view that is both loaded from an SQLite database. If the user makes a selection in combo box 1, I want to fire a method to restrict the values in combo box 2.
I think I need comboBoxSelectionDidChange function, but I don't know how to tell whether the function has been fired by a selection in combo box 1 or 2?
Have looked at the function parameters but can't see how I can tell which combo box fired the function?
The notification contains an object which represents the NSComboBox instance.
If you have created an NSComboBox outlet
#IBOutlet weak var firstComboBox : NSComboBox!
you can compare the instance with the notification object
func comboBoxSelectionDidChange(_ notification: Notification) {
let comboBox = notification.object as! NSComboBox
if comboBox == firstComboBox {
// do something with firstComboBox
} else {
// it's another comboBox
}
}
The solution for ascertaining which combobox fired the selectionDidChange method worked fine. One thing I did learn though was that the method is fired before .stringValue for the new selection is updated. I wanted to use the new .stringValue in the selectionDidChange method but discovered the previous .stringValue.
The way I got around this was to use the indexOfSelectedItem which is updated before the selectionDidChange method is fired.
So my code became
//In order to be able to restrict the contacts available when a company is selected use the comboboSelectionDidChange function
func comboBoxSelectionDidChange(_ notification: Notification) {
//Define a constant combobox that takes the notification object and casts it as an NSCombobox
let combobox = notification.object as! NSComboBox
//Can now test on the combobox object because we only want to do something if cmbCompany selection changes
if combobox == cmbCompany {
//The issue with comboboSelectionDidChange function is that the method is fired before the .stringValue is updated. So using .stringValue wont work as it will use the .stringValue from previous selected item. Therefore use index of selected item as this is updated before the comboboSelectionDidChange function is fired
//Define index which is the index of the newly selected item
let index = cmbCompany.indexOfSelectedItem
//Because the compIndex index will be the same as the index of the selected item we can get the newly selected company from the compIndex which is used to populate the cmbCombobox
let company = compIndex[index]
//Get the idCompany from combobox selection passing in the company from the new selection
idcom = (delegate as! ViewController).getPrimaryKeyComp(company: company)
//Call the function to reload the contIndex with only contacts for the company selected
contIndex = (delegate as!ViewController).getContactForCompany(ic: idcom)
cmbContact.reloadData()
}
}

Eureka PushRow on a modal, view won't close after item selection

I am using a SplitViewController and on the detail page (which is set to "defines context"), the user can select "+" in the navbar and I present the next view controller "modally over current context" using a segue. On that view controller I am using Eureka and one of the rows I want to use is PushRow. The issue I am running into is when I select an option on the PushRow, the view (table of options to choose that Eureka generated) never closes. The list of options stays full screen. I can see that PushRow.onChange is called and it has the correct value. For some reason that topmost view will not close.
I dug deeper and it seems like I need to modify the PushRow presentationMode to be "presentModally" since I am presenting it from a modal. However, I am not sure what to put for the controllerProvider. Is this the right path? If so, what would the correct syntax be? I also tried doing a reload in the onChange but that didn't make a difference.
private func getGroupPushRow() -> PushRow<String> {
return PushRow<String>() {
$0.title = "Group"
$0.selectorTitle = "What is the Group?"
$0.noValueDisplayText = "Select a Group..."
$0.options = self.getGroups()
$0.presentationMode = PresentationMode.presentModally(controllerProvider: ControllerProvider<VCType>, onDismiss: { () in
})
$0.onChange({ (row) in
print("in onchange \(row.value)")
// row.reload()
// self.tableView.reloadData()
})
}
}
I eventually figured out a solution so I'm posting it here to hopefully help out somebody else. Going off of the example above, replace presentationMode & onChange with this code. Note that if you are using another object in your PushRow besides String, then the type in PushSelectorCell should be that type instead.
$0.presentationMode = PresentationMode.presentModally(
controllerProvider: ControllerProvider.callback {
return SelectorViewController<SelectorRow<PushSelectorCell<String>>> { _ in }
},
onDismiss: { vc in
vc.dismiss(animated: true)
})
$0.onChange({ (row) in
row.reload()
})

Clicking keyboard 'Next' key using UIAutomation

I have a search field in my app and I have set the return key type of the keyboard for this field to UIReturnKeyNext. I am attempting to write a UIAutomation test that clicks the Next button on the keyboard using the following line:
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().keys().firstWithName("next");
This call is failing because the key with name 'next' is not being found. I have done a dump of all of the elements in my app using:
UIATarget.localTarget().frontMostApp().logElementTree();
This reveals that there is indeed a key in the keyboard with name 'next', but somehow my attempt to retrieve it as show above still fails. I can however retrieve other keys (like the key for the letter 'u') using this method. Is there a known issue here or am I doing something wrong?
I've tried other variations with no luck:
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().elements()["next"];
Here is a screen capture of the elements in my UIAKeyboard:
If you just want to click it, and you know the keyboard has "next" as "Return key" (defined in your nib), then you can use this:
app.keyboard().typeString("\n");
Jelle's approach worked for me. But I also found an alternative way if anybody needed it.
XCUIApplication().keyboards.buttons["return"].tap()
Where you can create XCUIApplication() as a singleton on each UI Test session. The thing about this approach is you can now distinguish between return and done and other variants and even check for their existence.
You can go extra and do something like following:
extension UIReturnKeyType {
public var title: String {
switch self {
case .next:
return "Next"
case .default:
return "return"
case .continue:
return "Continue"
case .done:
return "Done"
case .emergencyCall:
return "Emergency call"
case .go:
return "Go"
case .join:
return "Join"
case .route:
return "Route"
case .yahoo, .google, .search:
return "Search"
case .send:
return "Send"
}
}
}
extension XCUIElement {
func tap(button: UIReturnKeyType) {
XCUIApplication().keyboards.buttons[button.title].tap()
}
}
And you can use it like:
let usernameTextField = XCUIApplication().textFields["username"]
usernameTextField.typeText("username")
usernameTextField.tap(button: .next)
I dont't have an example to test, but as the "Next" button is an UIAButton, and not an UIAKey you could try :
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().buttons()["next"];
If it doesn't work, you can also try
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().buttons()[4];
The following works for me:
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().buttons().firstWi‌​thPredicate("name contains[c] 'next'");
For me, keyboard does not fall under mainWindow() in the View Hierarchy. It is at the same level as mainWindow() when you logElementTree() from top level. So, what you want to do is:
UIATarget.localTarget().frontMostApp().keyboard().buttons()["next"];
This worked for me when I was trying to press the "Search" button on keyboard.