Cocoa osx NSButton show text when mouse over - macos

In my application for Mac I want to show some info text when the users moves the mouse pointer over a button.
Something like this:
How can I achieve this correctly?
Thanks in advance.

This works for me in Xcode 6.2:
In the Identity Inspector(the pane on the right hand side in the image below), in the Tool Tip section enter "Sad face":

In Interface-Builder you can set a 'tooltip' for most objects, including NSButton (Open the Inspector, then choose the "Help" section).
However, if you're using a NSToolbar, this also has tooltips; you may choose to do this programmatically. Try typing setToolTip in your source, then option-double-click it for more information. (option=alternate).

To programmatically add a custom tooltip in Swift, subclass the corresponding view
var trackingArea: NSTrackingArea!
Add a tracking area for the view
let opts: NSTrackingAreaOptions = ([NSTrackingAreaOptions.MouseEnteredAndExited, NSTrackingAreaOptions.ActiveAlways])
trackingArea = NSTrackingArea(rect: bounds, options: opts, owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
Mouse entered Event
override func mouseEntered(theEvent: NSEvent) {
self.tooltip = "Sad face : Select the option for very poor"
}
Or you can make a separate tooltip for each range of a string: https://stackoverflow.com/a/18814112/308315

You can also do it programmatically.
(Assume someButton is your NSButton object)
[someButton setToolTip:#"Sad face: Select this option for \"Very poor\""];

Related

NSAppearance is not updating when toggling dark mode

I have a macOS app that runs only in the macOS status bar. I changed the "Application is agent (UIElement)" property in the Info.plist to "YES":
<key>LSUIElement</key>
<true/>
I have a timer that prints out the appearance's name every 5 seconds like this:
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { _ in
let appearance = NSAppearance.currentDrawing()
print(appearance.name)
}
Problem
The name doesn't actually change when I toggle dark/light mode in system settings. It always prints the name of the appearance that was set when the application launched.
Is there a way to listen to system appearance changes?
Goal
My end goal is actually to draw an NSAttributedString to an NSImage, and use that NSImage as the NSStatusItem button's image.
let image: NSImage = // generate image
statusItem.button?.image = image
For the text in the attributed string I use UIColor.labelColor that is supposed to be based on the system appearance. However it seems to not respect the system appearance change.
When I start the application in Dark Mode and then switch to Light Mode:
When I start the application in Light Mode and then switch to Dark Mode:
Side note
The reason why I turn the NSAttributedString into an NSImage and don't use the NSAttributedString directly on the NSStatusItem button's attributedTitle is because it doesn't position correctly in the status bar.
The problem with drawing a NSAttributedString is, that NSAttributedString doesn't know how to render dynamic colors such as NSColor.labelColor. Thus, it doesn't react on appearance changes. You have to use a UI element.
Solution
I solved this problem by passing the NSAttributedString to a NSTextField and draw that into an NSImage. Works perfectly fine.
func updateStatusItemImage() {
// Use UI element: `NSTextField`
let attributedString: NSAttributedString = ...
let textField = NSTextField(labelWithAttributedString: attributedString)
textField.sizeToFit()
// Draw the `NSTextField` into an `NSImage`
let size = textField.frame.size
let image = NSImage(size: size)
image.lockFocus()
textField.draw(textField.bounds)
image.unlockFocus()
// Assign the drawn image to the button of the `NSStatusItem`
statusItem.button?.image = image
}
React on NSAppearance changes
In addition, since NSImage doesn't know about NSAppearance either I need to trigger a redraw on appearance changes by observing the effectiveAppearance property of the button of the NSStatusItem:
observation = statusItem.observe(\.button?.effectiveAppearance, options: []) { [weak self] _, _ in
// Redraw
self?.updateStatusItemImage()
}

Voice over do not work in NSMenu in status bar with custom view for NSMenuItem

I have added NSMenu in NSStatusBar. I have used custom views for first NSMenuItem with the setView: method to include progressIndicator. All other items are default.
Issue here is with VO (voice over for disabled people). When I enable VO from Setting -> Accessibility -> VoiceOver and press option+command+M+M it focus on menus in status bar. Now using left and right keys I navigate to my app and then press down key of open NSMenu.
I can got down and select my options but cannot change to other app (wifi, date) from here. When I remove this custom view item it works fine. I also observed same with one another app.
Do I need to set any properties for custom views.
Edit
I created sample app in swift.
let menu = NSMenu()
let menuitem1 = NSMenuItem()
let view1 = NSView(frame: NSRect(x: 0, y: 0, width: 100, height: 30))
menuitem1.view = view1
menu.addItem(menuitem1)
menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P"))
menu.addItem(NSMenuItem.separator())
menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
Even in this simple app I cannot get VO working properly.
Steps
VO (control+option) + M + M
VO + space to select our app options.
VO + down key to navigate inside.
Then try to go left or right in other apps. It do not switch to other app.
Edit 2
Observation is when I choose custom view first and try to switch to other app it works. But once I choose normal menuitem from custom view it falls in some issue. Only solution I find is either have all default menu items or all items with custom view.
For my NSMenuItems displayed in a table, I can add the accessibilityLabel on the newAutoLayoutView.
NSView *cellView = [NSView newAutoLayoutView];
cellView.accessibilityLabel = #"Voiceover Tab Name";

How to add a view to NSGroupTouchBarItem

Trying to make a mutable list of buttons that show on the touch bar
override func windowDidLoad() {
super.windowDidLoad()
let customViewItem = NSCustomTouchBarItem(identifier: NSTouchBarItemIdentifier(rawValue: "identifier"))
customViewItem.view = NSTextField(labelWithString: "🌎 📓")
groupTouchBarItem.view?.addSubview(customViewItem)
}
But I get Cannot invoke 'addSubview' with an argument list of type '(NSCustomTouchBarItem)' on the bottom line.
How can I add buttons to the touch bar and remove them at will? Is there an array I can through views into or should I always addSubview on the touchbar view?
I'd recommend checking out this guide on creating NSTouchBar and NSTouchBarItems, specifically "NSTouchBar Objects", "Using NSTouchBarItem Objects"; as well as the documentation for NSGroupTouchBarItem
In short, when you create Touch Bar content you do so by composing NSTouchBarItems into an NSTouchBar, and not their views. (You primarily only deal with views when setting them on NSCustomTouchBarItems) And with NSGroupTouchBarItem, you are adding additional items to its groupTouchBar.
There are some conveniences, e.g.: NSGroupTouchBarItem(identifier: .myGroupItem, items: [customViewItem]), but you can also incrementally add to it with:
groupItem.groupTouchBar.templateItems.insert(customViewItem)
groupItem.groupTouchBar.defaultItemIdentifiers.append(customViewItem.identifier)

How do you create a new NSWindow using Swift in XCode and add content to it programatically?

I come from a background in Java, and I'm trying out using swift for creating OSX and iOS applications. My current project is essentially a flashcard application, and it needs to be able to create a popup window for text-based user prompts (ie, to ask what the question is for the card, or to add String tags for sorting the flashcards by type). Here is the code that I put together so far:
//Pulls up a prompt box to add tags
#IBAction func AddTagButton(sender: AnyObject) {
//Declare new subwindow
var win = NSWindow(contentRect: NSMakeRect(100, 100, 400, 150),
styleMask: 1 | 2 | 4 | 8,
backing: NSBackingStoreType.Buffered, defer: true);
win.title = "Tag Adder";
win.center();
//Add the window to the main viewer
window.addChildWindow(win, ordered:NSWindowOrderingMode.Above);
var controller = NSWindowController(window: win);
controller.showWindow(self);
}
This pulls up a new window with the ability to close, resize, minimize, and so on. I need to add a WrappedTextField to this window programmatically, but I couldn't find any resources on how to do so. In Java, the closest analogy would be something along the lines of
JFrame frame = new Jframe();
JLabel label = new JLabel("Sample text");
frame.add(label); //How is this done in Swift?
frame.setVisible(true);
I wrote the main NSWindow by modifying the .xib in XCode (Xcode 6, Beta version 6), but I can't figure out for the life of me how to use the WYSIWYG editor to make a window appear at the push of a button. The best I could do was to make another NSWindow that was minimized/hidden by default, but would show itself when you pushed the button (which isn't exactly a very good solution). The other feature I found was an NSAlert, but that doesn't have a text field for users to input data. My question is how do you add content to an NSWindow that pops up at the push of a button, either by modifying the above method, or by using the .xib GUI editor that XCode provides?
You should add content to the contentView of NSWindow.
let textField =. NSTextView()
textView.stringvalue = "Some string"
textView.frame = CGRectMake(10,20,50,400)
mywindow.contentView.addSubview(textView)

UICollectionView effective drag and drop

I am currently trying to implement the UITableView reordering behavior using UICollectionView.
Let's call a UItableView TV and a UICollectionView CV (to clarify the following explanation)
I am basically trying to reproduce the drag&drop of the TV, but I am not using the edit mode, the cell is ready to be moved as soon as the long press gesture is triggered. It works prefectly, I am using the move method of the CV, everything is fine.
I update the contentOffset property of the CV to handle the scroll when the user is dragging a cell. When a user goes to a particular rect at the top and the bottom, I update the contentOffset and the CV scroll. The problem is when the user stop moving it's finger, the gesture doesn't send any update which makes the scroll stop and start again as soon as the user moves his finger.
This behavior is definitely not natural, I would prefer continu to scroll until the user release the CV as it is the case in the TV. The TV drag&drop experience is awesome and I really want to reproduce the same feeling. Does anyone know how they manage the scroll in TV during reordering ?
I tried using a timer to trigger a scroll action repeatedly as long as the gesture position is in the right spot, the scroll was awful and not very productive (very slow and jumpy).
I also tried using GCD to listen the gesture position in another thread but the result is even worst.
I ran out of idea about that, so if someone has the answer I would marry him!
Here is the implementation of the longPress method:
- (void)handleLongPress:(UILongPressGestureRecognizer *)sender
{
ReorganizableCVCLayout *layout = (ReorganizableCVCLayout *)self.collectionView.collectionViewLayout;
CGPoint gesturePosition = [sender locationInView:self.collectionView];
NSIndexPath *selectedIndexPath = [self.collectionView indexPathForItemAtPoint:gesturePosition];
if (sender.state == UIGestureRecognizerStateBegan)
{
layout.selectedItem = selectedIndexPath;
layout.gesturePoint = gesturePosition; // Setting gesturePoint invalidate layout
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
layout.gesturePoint = gesturePosition; // Setting gesturePoint invalidate layout
[self swapCellAtPoint:gesturePosition];
[self manageScrollWithReferencePoint:gesturePosition];
}
else
{
[self.collectionView performBatchUpdates:^
{
layout.selectedItem = nil;
layout.gesturePoint = CGPointZero; // Setting gesturePoint invalidate layout
} completion:^(BOOL completion){[self.collectionView reloadData];}];
}
}
To make the CV scroll, I am using that method:
- (void)manageScrollWithReferencePoint:(CGPoint)gesturePoint
{
ReorganizableCVCLayout *layout = (ReorganizableCVCLayout *)self.collectionView.collectionViewLayout;
CGFloat topScrollLimit = self.collectionView.contentOffset.y+layout.itemSize.height/2+SCROLL_BORDER;
CGFloat bottomScrollLimit = self.collectionView.contentOffset.y+self.collectionView.frame.size.height-layout.itemSize.height/2-SCROLL_BORDER;
CGPoint contentOffset = self.collectionView.contentOffset;
if (gesturePoint.y < topScrollLimit && gesturePoint.y - layout.itemSize.height/2 - SCROLL_BORDER > 0)
contentOffset.y -= SCROLL_STEP;
else if (gesturePoint.y > bottomScrollLimit &&
gesturePoint.y + layout.itemSize.height/2 + SCROLL_BORDER < self.collectionView.contentSize.height)
contentOffset.y += SCROLL_STEP;
[self.collectionView setContentOffset:contentOffset];
}
This might help
https://github.com/lxcid/LXReorderableCollectionViewFlowLayout
This is extends the UICollectionView to allow each of the UICollectionViewCells to be rearranged manually by the user with a long touch (aka touch-and-hold). The user can drag the Cell to any other position in the collection and the other cells will reorder automatically. Thanks go to lxcid for this.
Here is an alternative:
The differences between DraggableCollectionView and LXReorderableCollectionViewFlowLayout are:
The data source is only changed once. This means that while the user is dragging an item the cells are re-positioned without modifying the data source.
It's written in such a way that makes it possible to use with custom layouts.
It uses a CADisplayLink for smooth scrolling and animation.
Animations are canceled less frequently while dragging. It feels more "natural".
The protocol extends UICollectionViewDataSource with methods similar to UITableViewDataSource.
It's a work in progress. Multiple sections are now supported.
To use it with a custom layout see DraggableCollectionViewFlowLayout. Most of the logic exists in LSCollectionViewLayoutHelper. There is also an example in CircleLayoutDemo showing how to make Apple's CircleLayout example from WWDC 2012 work.
As of iOS 9, UICollectionView now supports reordering.
For UICollectionViewControllers, just override collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)
For UICollectionViews, you'll have to handle the gestures yourself in addition to implementing the UICollectionViewDataSource method above.
Here's the code from the source:
private var longPressGesture: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
self.collectionView.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
Sources:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UICollectionView_class/#//apple_ref/doc/uid/TP40012177-CH1-SW67
http://nshint.io/blog/2015/07/16/uicollectionviews-now-have-easy-reordering/
If you want to experiment rolling out your own, I just wrote a Swift based tutorial you can look. I tried to build the most basic of cases so as to be easier to follow this.
Here is another approach:
Key difference is that this solution does not require a "ghost" or "dummy" cell to provide the drag and drop functionality. It simply uses the cell itself. Animations are in line with UITableView. It works by adjusting the collection view layout's private datasource while moving around. Once you let go, it will tell your controller that you can commit the change to your own datasource.
I believe it's a bit simpler to work with for most use cases. Still a work in progress, but yet another way to accomplish this. Most should find this pretty easy to incorporate into their own custom UICollectionViewLayouts.

Resources