NSTextView not changing font size or colour from NSFontPanel - cocoa

I created a basic NSTextView, I selected the following options in Interface Builder:
Editable
Selectable
Field Editor
Rich Text
Undo
Graphics
Non-contiguous Layout
Font Panel
Ruler
Inspector Bar
I set the NSViewController to be the delegate of the NSTextView and the only other custom thing I've done for this NSTextView is to enable inserting tabs and new lines (by accepting First responder):
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
if commandSelector == #selector(insertNewline(_:)) {
textView.insertNewlineIgnoringFieldEditor(self)
return true
} else if commandSelector == #selector(insertTab(_:)) {
textView.insertTabIgnoringFieldEditor(self)
return true
} //else if commandSelector == #selector(changeColor(_:)) {
//textView.setTextColor(NSFontPanel.colo, range: <#T##NSRange#>)
//}
return false
}
When I try to use the commands from the Font Panel + Inspector Bar, All the commands work fine except changing Font size or colour, is there anything that could be wrong? Or do I need to do extra binding/delegates, etc for this to work?
It is strange because if I change the Font itself (of a selected text) or the weight, it works fine (no coding was needed).
Update
I've found the root of the problem causing this. I'm displaying the TextView in a ViewController that is displayed using a Modal segue. If I change from Modal to Show, the size and colour work fine. There's also no need for the extra commands for insert new line and tab.
Is there any reason why this is the case? Is there any customisation that should be done to the segue to avoid this? And, why is the view controller presentation affecting the behaviour of the font panel?

NSFontPanel has a 'worksWhenModal' property which sounds as if it might be set to 'false'.
A Boolean that indicates whether the receiver allows fonts to be changed in modal windows and panels.
Documentation:

Related

NSTableView selected row flickering while scrolling with arrow keys

I have a view based NSTableView and can't figure out how to work around a visual glitch where the currently selected row flickers while scrolling up or down with the arrow keys.
The selected row should appear 'glued' to either the top or bottom of the view, depending on scroll direction. The Finder shows this correct behavior in list view but a regular table view seems to not behave this way out of the box. I'm confused as to why that is and see no obvious way to circumvent it. Can anybody point me to possible causes / solutions?
Edit No. 1
A cell based NSTableView behaves in the desired way by default, so this is presumably a bug specific to the view based implementation. I don't want to use a cell based table for unrelated reasons though.
Edit No. 2
I've tried making the table view's parent view layer backed, as well as intercepting the up / down arrow keystrokes to do my own scrolling, but so far I haven't been able to eliminate the flickering.
Edit No. 3
I've created a small sample project that reproduces the issue.
It looks like the selection changes and the old and new selected rows are redrawn. Next the selected row is animated up/down. Disabling scroll animation fixes the issue. Scroll animation can be disabled by subclassing NSClipView and overriding scroll(to:).
override func scroll(to newOrigin: NSPoint) {
setBoundsOrigin(newOrigin)
}
It might have some side effects.
Edit
Copied from zrzka's solution, with some adjustments. Scroll animation is temporarily disabled when using the up arrow or down arrow key.
class TableView: NSTableView {
override func keyDown(with event: NSEvent) {
if let clipView = enclosingScrollView?.contentView as? ClipView,
(125...126).contains(event.keyCode) && // down arrow and up arrow
event.modifierFlags.intersection([.option, .shift]).isEmpty {
clipView.isScrollAnimationEnabled = false
super.keyDown(with: event)
clipView.isScrollAnimationEnabled = true
}
else {
super.keyDown(with: event)
}
}
}
class ClipView: NSClipView {
var isScrollAnimationEnabled: Bool = true
override func scroll(to newOrigin: NSPoint) {
if isScrollAnimationEnabled {
super.scroll(to: newOrigin)
} else {
setBoundsOrigin(newOrigin)
documentView?.enclosingScrollView?.flashScrollers()
}
}
}
Did you try change the view ?
scrollView.wantsLayer = true
If you used Interface Builder:
Select the scroll view
Open the View Effects Inspector (or press Cmd-Opt-8)
In the table, find the row for your scroll view and check the box.

How to align a toolbar (or its items) with the leading edge of a split view controller's child?

In iOS, a toolbar can be added to any view. In macOS however, it seems only possible to add a toolbar to a window.
I'm working on an app with a split view controller with a toolbar but the toolbar's items only have a meaning with respect to the right view controller's context.
E.g. let's say I have a text editor of some sort, where the left pane shows all documents (like in the Notes app) and the right pane shows the actual text which can be edited. The formatting buttons only affect the text in the right pane. Thus, it seems very intuitive to place the toolbar within that right pane instead of stretching it over the full width of the window.
Is there some way to achieve this?
(Or is there a good UX reason why this would be a bad practice?)
I've noticed how Apple solved this problem in terms of UX in their Notes app: They still use a full-width toolbar but align the button items that are only related to the right pane with the leading edge of that pane.
So in case, there is no way to place a toolbar in a view controller, how can I align the toolbar items with the leading edge of the right view controller as seen in the screenshot above?
Edit:
According to TimTwoToes' answer and the posts linked by Willeke in the comments, it seems to be possible to use Auto Layout for constraining a toolbar item with the split view's child view. This solution would work if there was a fixed toolbar layout. However, Apple encourages (for a good reason) to let users customize your app's toolbar.
Thus, I cannot add constraints to a fixed item in the toolbar. Instead, a viable solution seems to be to use a leading flexible space and adjust its size accordingly.
Initial Notes
It turns out this is tricky because there are many things that need to be considered:
Auto Layout doesn't seem to work properly with toolbar items. (I've read a few posts mentioning that Apple has classified this as a bug.)
Normally, the user can customize your app's toolbar (add and remove items). We should not deprive the user of that option.
Thus, simply constraining a particular toolbar item with the split view or a layout guide is not an option (because the item might be at a different position than expected or not there at all).
After hours of "hacking", I've finally found a reliable way to achieve the desired behavior that doesn't use any internal / undocumented methods. Here's how it looks:
How To
Instead of a standard NSToolbarFlexibleSpaceItem create an NSToolbarItem with a custom view. This will serve as your flexible, resizing space. You can do that in code or in Interface Builder:
Create outlets/properties for your toolbar and your flexible space (inside the respective NSWindowController):
#IBOutlet weak var toolbar: NSToolbar!
#IBOutlet weak var tabSpace: NSToolbarItem!
Create a method inside the same window controller that adjusts the space width:
private func adjustTabSpaceWidth() {
for item in toolbar.items {
if item == tabSpace {
guard
let origin = item.view?.frame.origin,
let originInWindowCoordinates = item.view?.convert(origin, to: nil),
let leftPane = splitViewController?.splitViewItems.first?.viewController.view
else {
return
}
let leftPaneWidth = leftPane.frame.size.width
let tabWidth = max(leftPaneWidth - originInWindowCoordinates.x, MainWindowController.minTabSpaceWidth)
item.set(width: tabWidth)
}
}
}
Define the set(width:) method in an extension on NSToolbarItem as follows:
private extension NSToolbarItem {
func set(width: CGFloat) {
minSize = .init(width: width, height: minSize.height)
maxSize = .init(width: width, height: maxSize.height)
}
}
Make your window controller conform to NSSplitViewDelegate and assign it to your split view's delegate property.1 Implement the following NSSplitViewDelegate protocol method in your window controller:
override func splitViewDidResizeSubviews(_ notification: Notification) {
adjustTabSpaceWidth()
}
This will yield the desired resizing behavior. (The user will still be able to remove the space completely or reposition it, but he can always add it back to the front.)
1 Note:
If you're using an NSSplitViewController, the system automatically assigns that controller to its split view's delegate property and you cannot change that. As a consequence, you need to subclass NSSplitViewController, override its splitViewDidResizeSubviews() method and notify the window controller from there. Your can achieve that with the following code:
protocol SplitViewControllerDelegate: class {
func splitViewControllerDidResize(_ splitViewController: SplitViewController)
}
class SplitViewController: NSSplitViewController {
weak var delegate: SplitViewControllerDelegate?
override func splitViewDidResizeSubviews(_ notification: Notification) {
delegate?.splitViewControllerDidResize(self)
}
}
Don't forget to assign your window controller as the split view controller's delegate:
override func windowDidLoad() {
super.windowDidLoad()
splitViewController?.delegate = self
}
and to implement the respective delegate method:
extension MainWindowController: SplitViewControllerDelegate {
func splitViewControllerDidResize(_ splitViewController: SplitViewController) {
adjustTabSpaceWidth()
}
}
There is no native way to achieve a "local" toolbar. You would have to create the control yourself, but I believe it would be simpel to make.
Aligning the toolbar items using autolayout is described here. Align with custom toolbar item described by Mischa.
The macOS way is to use the Toolbar solution and make them context sensitive. In this instance the text attribute buttons would enable when the right pane has the focus and disable when it looses the focus.

Updating status bar appearance by toggling between lightContent and default using UIbutton

I am making an app which allows users to change the theme, eg. changing the background colour. Assuming I have a button, when click, toggles the change in background colour.
Eg. in code:
#IBAction func floodLightAction(sender: AnyObject) {
previousFloodlightOption = loadFloodlightOption()
if previousFloodlightOption {
print("OFF")
saveFloodlightOption(false)
// configure theme for themeViewController:
UIApplication.sharedApplication().statusBarStyle = .Default
setNeedsStatusBarAppearanceUpdate()
configureFloodlight(false)
} else {
print("ONN")
saveFloodlightOption(true)
// configure theme for themeViewController:
UIApplication.sharedApplication().statusBarStyle = .LightContent
setNeedsStatusBarAppearanceUpdate()
configureFloodlight(true)
}
However, setNeedsStatusBarAppearanceUpdate() does not seem to update the status bar style when the button is pressed. I have a few custom functions which are explained below.
My custom save func:
saveFloodlightOption(bool: Bool)
This saves a boolean value using NSUserDefaults.
My theme configuration func:
configureFloodLight(bool: Bool)
This just change objects' colour in the view controller based on the theme picked.
I hope I gave enough information, thanks!
Have you set View Controller Based Status bar appearance to false in your info.plist?
View Controller Based status bar appearance

NSBorderlessWindowMask Subview NSTextfield not keybard editable Swift

I'm using a NSBorderlessWindowMask for my main window on a Swift project (without storyboards), when I load a Subview, the NSTextfield outlet is not keybard editable. I already put this code on the initialisation:
self.window?.makeKeyWindow()
self.window?.becomeKeyWindow()
this allows the outlet to be "blue" like on focus, but the keyboard editing is disabled, i can copy/paste on the textfield
You need to use a custom subclass of NSWindow and override canBecomeKeyWindow() to return true. By default, it returns false for windows without title bars (as documented).
You probably want to do the same for canBecomeMainWindow().
Also, never call becomeKeyWindow() (except to call super in an override). That is called by Cocoa to inform the window that it has become the key window. It does not instruct the window to become the key window.
I found an awesome workaround for this problem:
basically setup at beginning the NSWindow mask as NSTitledWindowMask, when application is loaded, remove set up the new mask NSBorderlessWindowMask
func applicationWillFinishLaunching(notification: NSNotification) {
self.window?.titleVisibility = NSWindowTitleVisibility.Hidden
self.window?.styleMask = NSTitledWindowMask // adds title bar
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
self.window?.makeKeyWindow()
self.window?.becomeKeyWindow()
self.window.setIsVisible(true)
self.window?.styleMask = NSBorderlessWindowMask // removes title bar
}

Xcode 4, Cocoa Title bar removing from interface builders disables textView from editing

I am designing an application without a title bar, however when I remove the title bar using interface builder in Xcode 4 , it causes the editable fields (the ones I tried are textView, and textField) not being editable, despite editable checked in there properties? why this happens and is there anyway to prevent it?
You have to subclass your window and overwrite the following methods:
- (BOOL)canBecomeKeyWindow {
// because the window is borderless, we have to make it active
return YES;
}
- (BOOL)canBecomeMainWindow {
// because the window is borderless, we have to make it active
return YES;
}
Updated for Swift 4 and general tips about how to do this in 2018:
The canBecomeKeyWindow and canBecomeMainWindow methods no longer exist on an NSWindow. They have since been replaced by stored properties called canBecomeKey and canBecomeMain. Because they are stored, if you want to override them you can do so by making them computed properties, like this:
override var canBecomeKey: Bool {
return true
}
override var canBecomeMain: Bool {
return true
}

Resources