NSMenuItem with custom view in macOS 11 Big Sur - appkit

macOS 11 Big Sur in its current iteration (beta 1 through beta 6) has a bug/feature that makes it hard to have NSMenuItem with the custom view. Specifically, the custom view of an item won't get a draw(dirtyRect:) call when the menu item gets highlighted.
I managed to bypass that bug by manually calling the draw(dirtyRect:) method via the NSMenu delegate:
func menu(_ menu: NSMenu, willHighlight item: NSMenuItem?) {
if #available(OSX 11.0, *) {
// fix for bug when an item with custom view won't be called to draw the highlighting state
menu.items.filter{ $0.tag == 101 }.forEach{ $0.view?.needsDisplay = true }
}
}
But that doesn't solve the mystery of the drawing the state. MacOS 11 Big Sur has new UI look. And menu items now highlight in a different way, with rounded box around its content.
My question is: should I simulate that rounded box manually, or there's some default way in new App Kit to draw that rounded selection of menu items?
In other words, what is the best way to have NSmenuItem with custom view in macOS 11 Big Sur?

Related

macOS Big Sur toolbar item width with image

I'm trying to create an NSToolbar with items similar to the Apple's Mail app on macOS. I have an issue with the default toolbar item's width though, as it seems to be inconsistent. Since Big Sur, the items are meant to be sized automatically by AppKit and the NSToolbarItem minSize, maxSize properties have been deprecated.
I'm setting the image property for each NSToolbarItem, not using custom views. As you can see in the screenshots below, the envelope icon has a different "highlight" area (less padding on the sides) while the trash icon has a much larger highlight area.
The envelope icon is a single NSToolbarItem while the archive box and trash items are displayed using NSToolbarItemGroup with NSSegmentedControl view.
In the Apple's Mail app, even single toolbar items have the same width as the grouped items:
How to increase the toolbar item's width when using an image instead of custom view?
Deprecating a property and leaving you in the dark how to achieve a until recently simple effect without using the deprecated property is typical for how Apple deals with AppKit nowadays.
I would not be surprised of the Mail app still uses the deprecated minSize property, or that the NSToolbarItem objects are based on NSButton views with a minimum width NSLayoutConstraint (which is my current solution).
To continue using minSize without deprecation warnings, you can consider to use a simple ToolbarItem class like this:
class ToolbarItem: NSToolbarItem {
override var minSize: NSSize {
get {
return NSSize(width: 50, height: 30)
}
set {}
}
}

macOS: NSToolbar with translucency effect in Big Sur

I am working with a new Xcode project for Big Sur, with a "Split View with Sidebar" scene in the main storyboard. I want to make the window title and toolbar have the translucency effect that you see in the toolbars in Safari or Finder. In my storyboard, I specify "Full Size Content View" and "Hide Title Text", and in the storyboard it looks like what I want:
But when I build and run it, the window's toolbar is plain white:
Now if I disable the "Hide Title Bar" checkbox, it looks fine in the storyboard with the title and the toolbar items on the same line:
Now when I build and run it, the toolbar has the translucency effect I want, but the title is on a 2nd level above the toolbar items:
I'm not sure what else I can do to control this. Ideally, I would hide the title bar and keep the translucency, but it doesn't seem to be working. Is there anything else I can try to control this?
If not, I would prefer the title to be on the same level as the toolbar items, like it does in the storyboard. But even that isn't working as desired.
Any ideas on what I can try? I've tried changing the toolbar styles, but they all have similar results.
EDIT:
I've tried using Apple's own code for "Navigating Hierarchical Data Using Outline and Split Views" and after tweaking the storyboard with enabling both "Full Size Content View" and "Hide Title Bar" checkboxes, I'm seeing the same issue: i.e. the toolbar turns white. So it's likely an Apple framework bug? I'm not sure, so I have filed a bug to find out.
I created an app that build fine on High Sierra and Catalina, it may build on OX11. It removes the titlebar, but I don't use a toolbar. Try this, it may do similar to what you want - you may have to comment-out some settings, etc.
Create a file "MainWindow" that is a subclass of NSWindowController. Fill the file with:
class MainWindow: NSWindowController {
var controller: ViewController?
override func windowDidLoad() {
super.windowDidLoad()
controller = self.contentViewController as? ViewController
self.smartWindow()
self.activateWindowDrag()
}
}
// you may prefer the below as a separate file
extension NSWindowController {
func smartWindow() {
self.window?.styleMask.insert(NSWindow.StyleMask.unifiedTitleAndToolbar)
self.window?.styleMask.insert(NSWindow.StyleMask.fullSizeContentView)
self.window?.styleMask.insert(NSWindow.StyleMask.titled)
self.window?.toolbar?.isVisible = false
self.window?.titleVisibility = .hidden
self.window?.titlebarAppearsTransparent = true
}
func activateWindowDrag() {
self.window?.isMovableByWindowBackground = true
}
}

Big Sur Toolbar Items in the Sidebar

In Big Sur, Xcode and Calendar have toolbar items that stay over the sidebar when open but remain visible on the left side when the sidebar's collapsed.
Sidebar open:
Sidebar collapsed:
In "Adopt the New Look of macOS" at 13:55, John says "items placed before the separator [sidebarTrackingSeparator] will appear over the full-height sidebar", just as they are in Xcode and Calendar. I haven't been able to make this work.
Here's a sample project that demonstrates the issue. I used the IB-defined "Window Controller with Sidebar" and added a toolbar item for toggling the sidebar. In a subclass of NSWindowController I insert .sidebarTrackingSeparator after the .toggleSidebar item:
override func windowDidLoad() {
// Sometimes the toolbar items aren't loaded yet--async is a quick and dirty way to prevent a crash
DispatchQueue.main.async {
self.window?.toolbar?.insertItem(withItemIdentifier: .sidebarTrackingSeparator, at: 1)
}
}
Sometimes this has no effect (the toggle button remains to the right of the sidebar). Sometimes the sidebar toggle get put in an overflow menu:
I haven't seen any discussion of implementing this toolbar design outside that WWDC session. Has anyone been able to get this to work?
This is a IB/Code timing disagreement. Interface Builder configures and installs the toolbar before you add the .sidebarTrackingSeparator toolbar item.
So you're doing the right thing, just too late. And too later with the dispatch. I think the important thing is to have the item in there before the toolbar is set on the window.
Unfortunately, that isn't really possible with IB, unless I believe, you create a whole new toolbar and reassign it. But that's a bad idea, because then you may run into trouble auto-saving the state of your toolbar.
The trick is to configure the separator in Interface Builder. If you look at the ObjC documentation for this constant, you'll see a longer name: NSToolbarSidebarTrackingSeparatorItemIdentifier.
The best we can do here is hope that the symbol's name is the same value as the identifier. If you really want to verify this, you can just print the symbol's value in the debugger:
(lldb) po NSToolbarSidebarTrackingSeparatorItemIdentifier
NSToolbarSidebarTrackingSeparatorItemIdentifier
If we create a custom toolbar item in IB, and add that according to John's video...
low and behold:

Toolbar of NSScrollView inside SplitView [duplicate]

It's easy to enable the "inspector bar" for text views so that a bar appears at the top of the screen with various formatting buttons. (Although I had some confusion until I learned to make sure I was selecting the text view in a scroll view, and not the scroll view itself). I can either programmatically use [textView setUsesInspectorBar:YES] or go to the Attributes Inspector and check the "Inspector Bar" box in the "Uses" section.
My question is, how can I further control the inspector bar? I'm having trouble finding information on it in the XCode documentation or online. I'd like to be able to position it in a different place on the screen. Being able to pick and choose which specific controls are in the bar would be great too.
The answer is, you aren't meant to further control the inspector bar. There's nothing in the documentation because, well, there's nothing. Apple's saying, use it or don't use it.
However, if you dig into it a bit, you will find that the inspector bar is a very interesting control. It's not displayed as part of the text view, but rather (privately) embedded in the "window view" itself. When I say "window view," I mean the superview of the content view.
If you list the subviews of that "window view":
NSLog(#"%#", [self.testTextView.window.contentView superview].subviews);
You end up with:
2012-08-02 15:59:30.145 Example[16702:303] (
"<_NSThemeCloseWidget: 0x100523dc0>", // the close button
"<_NSThemeWidget: 0x100525ce0>", // the minimize button?
"<_NSThemeWidget: 0x100524e90>", // the maximize button?
"<NSView: 0x100512ad0>", // the content view
"<__NSInspectorBarView: 0x100529d50>", // the inspector view
"(<NSToolbarView: 0x10054e650>: FD2E0533-AB18-4E7E-905A-AC816CB80A26)" // the toolbar
)
As you can see, AppKit puts the inspector bar at the same level as other top level window controls. Now this is getting into the land of private APIs, but simply tinkering with the "window view" shouldn't get any apps rejected.
You can try to get a reference to the __NSInspectorBarView from here. It seems like it is always the subview right after the content view, so something like this may work:
NSArray *topLevelViews = [self.testTextView.window.contentView superview].subviews;
NSUInteger indexOfContentView = [topLevelViews indexOfObject:self.testTextView.window.contentView];
if (indexOfContentView + 1 < topLevelViews.count) {
NSView *inspectorBar = [topLevelViews objectAtIndex:indexOfContentView + 1];
NSLog(#"%#", inspectorBar);
}
NSLog(#"%#", topLevelViews);
Since this immediately breaks if Apple changes the ordering of the top level views, it may not be a good idea for an application for production. Another idea is:
NSView *inspectorBarView = nil;
for (NSView *topLevelView in topLevelViews) {
if ([topLevelView isKindOfClass:NSClassFromString(#"__NSInspectorBarView")]) {
inspectorBarView = topLevelView;
}
}
NSLog(#"%#", inspectorBarView);
I don't know if the use of NSClassFromString() will pass App Store review guidelines, however, since once again, it's dependent on private APIs.
That being said, once you get a reference to the inspector bar view, things still don't work too well. You can try repositioning it at the bottom:
if (inspectorBarView) {
NSRect newFrame = inspectorBarView.frame;
newFrame.origin = NSZeroPoint;
[inspectorBarView setAutoresizingMask:NSViewMaxYMargin | NSViewMaxXMargin];
[inspectorBarView setFrame:newFrame];
}
But you end up with a misdrawn toolbar, so more work would be necessary there:
My ideas would be to try to shift the content view's height up to cover up the gray left-over area (which would have to be done every time the window is resized, maybe tinkering with autoresizing masks may make it easier) and custom draw a background for the inspector bar at the bottom.
EDIT
Oh, and you should file a feature request for this too. bugreport.apple.com
This is four years late, but I feel like someone on the internet may benefit from this in the future. I spent way too long trying to figure this out.
The inspector bar class, as the others have pointed out, seems to be a private class (__NSInspectorBarView). Therefore, it's probably not recommended to modify.
Nevertheless! The curious have to know. The inspector bar is inserted, at the time of this post (April 2016) into the window's accessory bar. You can get a list of accessory views as of OS X 10.10 using the array property in NSWindow called titlebarAccessoryViewControllers[].
Here's some Swift 2.0 code to do just that, assuming you haven't inserted any other accessory views into the window beforehand.
if window.titlebarAccessoryViewControllers.count > 0 {
let textViewInspectorBar = self.titlebarAccessoryViewControllers[0].view
let inspectorBarHeight: CGFloat = textViewInspectorBar!.frame.height // 26.0 pt
}
It's worth noting that accessory views are handled differently in full screen mode apps: https://developer.apple.com/library/mac/documentation/General/Conceptual/MOSXAppProgrammingGuide/FullScreenApp/FullScreenApp.html
I personally would not attempt to move an accessory view, as they are special kinds of views designed to stay in the toolbar (if I fully understood what I have read).
NSTitlebarAccessoryViewController Reference:
https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSTitlebarAccessoryViewController_Class/
Another 3 years on, but I suspect some will find this useful. My specific problem was in having a window fully filled by a tabView - ideal for setting various kinds of user defaults. Only one of these tab pages had a couple of text views for which I wanted the inspector bar visible. Tabbing to that page made the inspector bar appear, and pushed the whole lot down, ruining my carefully planned layouts. Tabbing away from the page did not hide it again.
The obvious thing was to get the inspector bar to appear on the relevant tab page only. Having got hold of it ("on the shoulders of giants" - thanks to giant Vervious) it is relatively easy to reposition it in the view hierarchy. You are still left with the problem of space for an empty toolbar pushing the content down. The window's view hierarchy changes radically when the inspector bar first appears, and I gave up on trying to do anything with it.
My solution is to increase the content view's height. (Why height and not origin I can't say.)
func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) {
if let inspectorBar = window!.titlebarAccessoryViewControllers.first(where:
{$0.view.className == "__NSInspectorBarView"} )?.view {
// move content view back to where it should be
var sz = window!.contentView!.frame.size
sz.height = window!.frame.size.height - 21
window!.contentView?.setFrameSize(sz)
// put the inspector where we want it
inspectorBar.removeFromSuperview()
let y = textPage.frame.size.height - inspectorBar.frame.size.height - 10
inspectorBar.setFrameOrigin(NSPoint(x: 0, y: y))
textPage.subviews.insert(inspectorBar, at: 0)
}
}
The code belongs in a NSTabViewDelegate which I made my window controller conform to, remembering to set the tabView's delegate to File's Owner in the xib, and is called whenever a new tab is selected. textPage is the view inside the relevant tabViewItem.
There are some arbitrary constants found by trial and error. The function only need run once. Repeated calls are harmless, but you could put in a flag to make an early return from subsequent calls.
You cannot do anything to position this thing.
Clearly, the corruption noted by #Vervious is real, but only if you do not have an NSToolBar.
You see, this inspectorBar is sadly a mostly private and mostly (publicly) undocumented but awesome tool. And it is very much intended for use in a window that has an NSToolBar visible... go figure.
After you have a toolbar added to your view
Still with a toolbar but hidden, and inspector bar is cool
(as in via the view menu or the method it invokes, which is toggleToolBarShown: and is an NSResponder friendly message )
So it is obvious, no you cannot do much with this. It's design is poorly documented. It works as intended as a pseudo accessory view bar under the place an NSToolbar goes (which is also not adjustable)

How to improve tap-ability of buttons on toolbars and navigation bars in iOS 7

With the new iOS 7 "flat" look, take for example the + button to add new items such as in iOS's Contacts app. In my app, the + is very hard to tap, the button size seems very small and was never an issue in iOS < 7 and now in iOS 7 it is an issue.
I looked at the Contacts app and if you experiment with it, notice how far left of the button you can tap and the button registers the tap. The same applies as I noticed in the iPad mail app above the e-mail item list, the Edit button for the UITableView also registers the tap far left of the word "Edit".
How can I improve tap-ability of buttons like this on either navigation bars or toolbars? Both of which are BarButtonItems. My main concern is the + button implementation but it looks like whatever technique Apple is using would be a good design to adopt to improve tap-ability of button items.
Thank you.
If you use UIBarButtonSystemItemAdd, you should be able to tap a fairly large distance away from the button. Here's a screenshot showing where I was tapping, and as you can see the button is pressed:
A button like this can be created using code like this:
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:#selector(addPressed:)]
You can also create one in interface builder:

Resources