Is it possible for an NSMenu object to notify BEFORE it'll close, not after? Its delegate has method didClose(_:) but I want to update its items before it actually closes, since the disappearing animation is too long and the eye can see the change.
I've tried to monitor NSEvents, but it's useless because NSMenu hasn't public property containing its NSWindow object.
It's theoretically possible to achieve by creating a custom NSViews for each menu items. But I don't like this because then I'll have to draw all the drawing of the items, including selection and click animation.
UPDATE:
I've tried to subclass the NSPopUpButton to track menu updates:
class CustomPopUpButton: NSPopUpButton {
var isMenuShown: Bool = false
var onClosingMenu: ((NSMenu)->())?
override var needsDisplay: Bool {
willSet {
if let menu = self.menu, isMenuShown, newValue {
onClosingMenu?(menu)
isMenuShown = false
}
}
}
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
isMenuShown = true
super.willOpenMenu(menu, with: event)
}
}
I'm not proud of that piece of code but it works in general. Yet the 'onClosingMenu' method is being called just after the menu closing animation is finished. Not before.
Video of what I want to achieve: https://drive.google.com/file/d/1GAceKp-fTlurxSybdB3h0epVZrtQjthm/view?usp=sharing
Finally, I've found the solution. No need to fight the system and update something before the button menu closes. I've found the another way and it's pretty simple.
I've subclassed the NSPopUpButton and created another NSMenu in the subclass, called 'attributedMenu'. Overrided all properties of the NSPopUpButton that deal with menu items (insertions and removals) and redirected that actions to the 'attributedMenu' property.
The initial menu property of NSPopUpButton I'm using only for selected items, removing non-selected items right away.
I intercept the click on the button to show 'attributedMenu', not the default menu of the class.
That solution even made possible to display 'multiple values' title if I select more than one element. Like in Apple Pages' font picker when you select text written with multiple fonts. All it takes is to add an NSMenuItem with title 'Multiple Values' and call super to select it.
That's it, now it works as perfect as in Apple Pages font picker button. As long, as I'm not touching the original 'menu' property of NSPopUpButton class.
UPDATE
Uploaded the subclass to GitHub: https://github.com/CineDev/AttributedPopUpButton
Related
So I have a menu bar application that has a slider inside a custom view: I want the ability to press the option key while dragging to make the slider snap to increments.
Right now, everything is working... except I cannot find a single way to notify my menu app that the option key is being pressed. Here's what I've tried:
NSEvent.addGlobalMonitorForEvents
"Note that your handler will not be called for events that are sent to your own application."
NSEvent.addLocalMonitorForEvents
"Your handler will not be called for events that are consumed by nested event-tracking loops such as control tracking, menu tracking, or window dragging"
NSApplication.shared.currentEvent
This dosesn't work because the flagsChanged event isn't handled by the app, so this value is set to another value (mouse button presses).
flagsChanged(with event: NSEvent)
This doesn't get called. I've set the viewController to acceptFirstResponder as well as resigned the first responder of the view. Also, I've subclassed the view and overridden flagsChanged and that hasn't worked.
I've tried creating a menu item that has a key like "s" or "p" to trigger an action. But when the "action" gets triggered, the menu bar hides...
So I'm trying to find how I can be notified when the option key is pressed. Is this possible? I can clarify if you have any questions.
In your custom slider view's mouseDragged method, you'd simply check the NSEvent parameter's modifierFlags to see if it contains the option key.
--
Since it could be interpreted that the slider is an actual NSSlider, if that's the case then the slider's control is really implemented in with an NSSliderCell, and the mouse tracking gets handed off from the view to the cell within mouseDown, so the NSSlider's mouseDragged wouldn't be called, and instead you'd have to do work in the cell's trackMouse:inRect:... method. But if you're using an NSSlider, then you'd probably simply want to use altIncrementValue.
I'm currently implementing the NSTouchBar api to my macOS application.
At this moment, the only touch bar I have has the main View Controller as its delegate and I can add items to it fine. The catch is, I need some of those items to appear only when a certain condition is met (a row is selected in a table).
Consider I have a boolean that indicates whether or not the button should be visible. How do I update the NSTouchBar on the fly to show/hide this button in case my boolean changes? (I don't need to observe this boolean, I could simply make the call to update in another method I already implemented)
What I did for now is the following: in touchBar(:makeItemForIdentifier), I have a switch for all identifiers, and under the proper case, I either return the NSCustomTouchBarItem with the button, or nil if my boolean is false.
I tried calling makeTouchBar again after a row of the table is selected but it doesn't update the buttons' visibility, as if touchBar(:makeItemForIdentifier) is not called again.
Thanks!
Four ideas:
Try changing your touch bar's defaultItemIdentifiers to the set of item identifiers that should be shown. Note that this would be problematic if the user has customized the touch bar, but I think swapping items on-demand and customizing the touch bar doesn't go well together anyway. This also has the advantage that you don't need to return nil in touchBar(:makeItemForIdentifier:).
Calling makeTouchBar() will create a new NSTouchBar instance, but not change the touchBar property. Try something like
viewController.touchBar = viewController.makeTouchBar()
or
viewController.touchBar = nil
Set the touchBar property on the NSTableRowView that should show extra items when selected, and make sure to include the otherItemsProxy in your defaultItemIdentifiers. As the contents of the touch bar are comprised of all elements in the responder chain, this might include the touchBar property of the table row (provided it can become first responder).
Are you sure that these items should be hidden when the row is not selected? Consider disabling them instead (e.g. by setting the enabled property of the buttons they contain to false).
Just invalidate the touchbar in your view controller:
self.touchbar = nil
The delegate method makeTouchBar() will then automatically be called. Using your flags, you can easily choose the icons to appear.
EDIT: this solution has been tested and works fine.
I have an NSDocuments & storyboard app created with the wizard in Xcode 8. In the ViewController Scene I have added a NSTextView. Certain menu items are disabled, such as Bold and Italic.
Looking at the First Responder the actions for bold and italic are not there. Am I supposed to write these methods myself? Is this due to the responder chain not being correctly set up? Why does underline show up but not bold?
Edit: Adding an image to show how I can edit text with the Inspector Bar, but the Format menu does not show the commands I would expect.
There is a historical (?) reason of this problem.
When the main menu was created in a xib file, xib file automatically contained a NSFontManager instance and such menu items like Bold were connected to it.
However in a modern storyboard, there is no preset NSFontManager instance.
Well then, you can connect them to a FontManager manually following the following steps.
Create a normal Object instance (blue cube) in the Application scene.
Change class of the Object instance to NSFontManager.
Connect the menu items to addFontTrait(_:) action of the fontManager. Likewise, connect "Bigger" and "Smaller" items to modifyFont(_:).
You also need to set menuItems' tag, however they are actually already set. Set the correspondent tag also manually only if menuItem's tag is 0.
If you ctrl drag from menu item to first responder, in menu view, you get the same options, just ctrl click. Then you implement whatever function you just connected. If you connect File > New to newDocument and implement in your ViewController
func newDocument(_ sender: Any?){
print("func newDocument(_ sender: Any? \(String(describing: sender)))")
}
It will get called. First responder lists all the same methods as the added Object with NSFontManager as class. I don't use #IBAction in front of method because I don't connect it.
I want to have a NSToolbar in my macOS-App. I have created a Toolbar in the Window of my storyboard and connected this with a swift class called MainToolbar.
The source text of this class is the following (at this moment):
import Cocoa
class MainToolbar: NSToolbar, NSToolbarDelegate {
override init(identifier: String) {
super.init(identifier: identifier)
}
}
Now, I want to change the title of the Colors-Element and add a share button as two examples.
The Colors-Element has the identifier "NSToolbarShowColorsItem" in the storyboard.
I know, that there is the possibility of getting the items with "self.items", but there is now way of adding elements because it is immutable. And I also cannot find the way of getting elements with the identifier.
In order to do this you have to go to the storyboard and click on the toolbar.
The toolbar will open up and show two sections. The top section is the available buttons and the bottom section is the default buttons for the application.
I don't think it is wise to actually change the standard buttons, i.e. change the meaning of Colors. It is better to add a new NSToolbarItem to the top section. After you added it, you can double click on the title to give it a title and you can set the image by providing an image name in the Attribute Inspector.
Next you drag the new button from the top section to the bottom section.
Actions should be set from the top section and not from the bottom section.
I have a view with a textfield at the top, a textview under it and two buttons below the textview.
The keyboard is configured with a "Done" button. Once the user has typed in their info, they click the save button, which is below the textview. First they click Done to hide the keyboard (and reveal the save button) then click the save button.
I need to allow carriage returns in the textview but "Return" is already taken up by Done.
How is it usually handled when you need a Return key and ability to also hide the keyboard?
Drag a button into your view, delete the text, and resize it to take up the entire view. In the document outline, select the new button and drag it to the top of the list of elements. This puts it in the background so it is not hiding the elements of your view.
Add this code to your ViewController:
#IBAction func hideKeyboard(sender: AnyObject) {
self.textField.resignFirstResponder()
}
Link the button to this action and you're all set.
If you're using a UINavigationBar or have other buttons or fields, activating any of those UIControls could be detected and used to dismiss the keyboard via resignFirstResponder(). In fact Save/Cancel/Done are UIBarButtonItems and are a standard mechanism for completing things and changing state, and create a framework for accomplishing what you want. If you don't take that approach then you have to get creative with how you do it, and also make it clear to the user what needs to be done.
In Interface Builder you can change the type of your main view from UIView to UIControl and then use addTarget() to detect touch events as a 'touch outside' area and use those actions to resign first responder as well. But you might want to consider a UINavigationBar or some other button bar or tab interface to make state transitions.
Also review iOS Human Interface Guidelines document. It's a great document for understanding how iOS is designed to handle common situations like what you are dealing with, and it can get you out of design ruts. It's well written and worth re-visiting periodically.