NSPanel not hiding when focus is lost - swift

I am trying to create a window like Spotlight.
As in Spotlight it should hide when the background is clicked. I tried doing it unsuccessfully with NSWindow but I was lead to believe using NSPanel instead would solve the problem.
However, even when using NSPanel the window does not hide.
Here is the code I'm using.
let panel = NSPanel(contentRect: CGRect(x: 0, y: 0, width: 200, height: 200), styleMask: [.titled, .nonactivatingPanel], backing: .buffered, defer: true)
panel.level = .mainMenu
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
panel.orderFrontRegardless()

It is due to used window level (.mainMenu which is above all windows), so you need to hide it explicitly via delegate methods
so assuming you create window/panel in your controller, make that controller a delegate of window
panel.delegate = self
and implement something like
extension ViewController { // << your controller class here
func windowDidResignKey(_ notification: Notification) {
if let panel = notification.object as? NSWindow {
panel.close()
}
}
}

Related

Draw an NSWindow over the entire screen, from corner to corner, including the menu bar and dock

The bounty expires in 2 days. Answers to this question are eligible for a +50 reputation bounty.
JonLuca wants to draw more attention to this question.
I want to draw an NSWindow for an overlay application that completely covers the users screen - from corner to corner. I've got a borderloss, non activating panel that takes over the entire page.
import Foundation
import AppKit
import SwiftUI
class FullScreenPanel: NSPanel {
override func constrainFrameRect(_ frameRect: NSRect, to screen: NSScreen?) -> NSRect {
return frameRect
}
}
final class Panel: FullScreenPanel, NSWindowDelegate {
init(contentRect: NSRect, backing: NSWindow.BackingStoreType, defer flag: Bool) {
super.init(
contentRect: contentRect,
styleMask: [.borderless, .nonactivatingPanel],
backing: backing,
defer: flag
)
self.level = .mainMenu + 3
self.collectionBehavior.insert(.fullScreenAuxiliary) // Allows the panel to appear in a fullscreen space
self.collectionBehavior.insert(.canJoinAllSpaces)
self.titleVisibility = .hidden
self.titlebarAppearsTransparent = true
self.isMovable = false
self.isMovableByWindowBackground = false
self.isReleasedWhenClosed = false
self.isOpaque = false
self.delegate = self
}
func windowDidResignKey(_ notification: Notification) {
DispatchQueue.main.async {
appDelegate?.hideWindow()
}
}
}
I'm instantiating this with the NSScreen.main.frame CGRect
mainWindow = Panel(
contentRect: NSScreen.main!.frame,
backing: .buffered, defer: false)
However, when the window shows up, it still shows up under the menu bar. The constrainFrameRect function shows that somewhere internally the y value of the frame goes from 0 to to -44.
The window should also not trigger the native fullscreen effect, where it becomes a new "Desktop" that you can swipe between.
I think that you use NSPanel class incorrectly. Official Documentation for NSPanel:
NSPanel
A special kind of window that typically performs a function that is auxiliary to the main window.
Your window is probably main (Because it takes up the whole screen and is the only visible one), so the NSPanel is not necessary, just use generic NSWindow.
Use an NSWindowController for better code organisation.
Use
NSApplication.shared.presentationOptions = [.hideDock, .hideMenuBar]
to hide the dock and menu bar completely and
NSApplication.shared.presentationOptions = [.autoHideDock, .autoHideMenuBar]
to make them appear when you hover over the position where they have been by default.
Warning! This code might block your whole screen. Consider adding an exit button or a shortcut (In the current version you can use Cmd + Tab to focus on another window). In the worst case you must reboot your computer by holding down the power button.
Code:
class FullScreenWindowController: NSWindowController {
let viewController = FullScreenViewController()
init() {
super.init(window: NSWindow(contentViewController: viewController))
// Remove the window header
window?.styleMask = .borderless
window?.setFrame(window!.screen!.frame, display: true)
// Dock and Menu Bar are completely inaccessible
// NSApplication.shared.presentationOptions = [.hideDock, .hideMenuBar]
// Dock and Menu Bar will automatically hide when not needed
NSApplication.shared.presentationOptions = [.autoHideDock, .autoHideMenuBar]
window?.makeKeyAndOrderFront(self)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
To use the above class, just create an instance of it:
let fullScreenWindowController = FullScreenWindowController()
Also I used this view controller:
class FullScreenViewController: NSViewController {
override func loadView() {
let label = NSTextField(labelWithString: "Full Screen Window")
label.font = .systemFont(ofSize: 48)
view = label
}
}
OK I'm pretty noob in this however you could try the below (please don't flame if not working, it's just a suggestion lol I can delete the answer given if you hate it)
final class Panel: FullScreenPanel, NSWindowDelegate {
init(contentRect: NSRect, backing: NSWindow.BackingStoreType, defer flag: Bool) {
let menuBarHeight = NSStatusBar.system.thickness
let adjustedContentRect = NSRect(x: contentRect.origin.x,
y: contentRect.origin.y + menuBarHeight,
width: contentRect.size.width,
height: contentRect.size.height - menuBarHeight)
super.init(
contentRect: adjustedContentRect,
styleMask: [.borderless, .nonactivatingPanel],
backing: backing,
defer: flag
)
// Rest of your code...
}
// Rest of your code...
}

Xcode SwiftUI window not shown after successful build

I get the following warning, on main.storyboard
Unsupported Configuartion:
Window Controller requires a content view controller
This is the custom class linked to the Window Controller, which is also the StoryBoard Entry Point,
import Cocoa
import SwiftUI
class FirstWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
let contentView = ContentView(myListModel: MyListModel())
.environmentObject(UserData())
self.window?.contentView = NSHostingView(rootView: contentView)
}
}
This is inside the AppDelegate.swift which is annotated as #NSApplicationMain.
func applicationDidFinishLaunching(_ aNotification: Notification) {
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window”)
}
The main.storyboard, AppDelegate.swift, and FirstWindowController.swift are identical to those of a project which was launching and showing the application window with no problem. I just renamed it to something else, and removed the Core Data support from the code. But in this project, the window doesn’t show up after successful build.
I also have checked and compared all the configuration of the storyboard for both of these projects. They seem to be totally identical.
Any help for fixing this would be appreciated.
This might be due to a bug.
I removed all the other schemes but the one I was using in the project and the window is now loading.
Yet, the same warning is still there.

Track NSWindow focus? (Swift, macOS)

I'm trying to track a created windows focus so I can perform actions on those events.
I'm able to track a windows frame dimensions with the notification center using NSView.frameDidChangeNotification, but I cant find a correct way of using something like NSWindowDidResignKeyNotification or NSWindowDidResignMainNotification.
private func newWindow() {
let windowInfo = WindowInfo()
let contentView = ContentView()
.environmentObject(windowInfo)
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 350, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
window.contentView = NSHostingView(rootView: contentView)
window.contentView?.postsFrameChangedNotifications = true
window.makeKeyAndOrderFront(nil)
// vvv I'm trying to use a similar method of being notified when a created window loses/regains focus. vvv
// v v v
NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: nil, queue: nil) { (notification) in
windowInfo.frame = window.frame
}
}
Also, if theres some method for detecting window focus in SwiftUI, I could use that too. The notifications that actions are performed on are for my Views in SwiftUI
This was just a dumb oversight from me (it was pretty late at night), and I found the line I needed.
Adding NSWindow.didBecomeKeyNotification did the trick to properly catch the focus events.
Also changing object: nil to object: window made sure the focus events caught were only for the specific window, instead of all.

MacOS SwiftUI, how to prevent window close action

So I have a swiftUI application, at some point I create a NSWindow and assign the contentView, like this:
// ////////////////////////////////////////////////////////
// Add token window
// ////////////////////////////////////////////////////////
let configurationView = ConfigurationView().environmentObject(store)
configurationWindow = NSWindow(
contentRect: NSRect(x:0, y: 0, width: 480, height: 500),
styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView],
backing: .buffered, defer: false
)
configurationWindow.center()
configurationWindow.setFrameAutosaveName("BSchauer")
let hostingController = NSHostingController(rootView: configurationView)
configurationWindow.contentViewController = hostingController
configurationWindow.makeKeyAndOrderFront(nil)
configurationWindow.setIsVisible(false)
...
// later on in the code
#objc func toggleConfigurationWindow() {
if self.configurationWindow.isVisible {
self.configurationWindow.setIsVisible(false)
if let button = self.statusBarItem.button {
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
} else {
self.configurationWindow.setIsVisible(true)
self.configurationWindow.contentViewController?.view.window?.becomeKey()
}
}
You see that the way I interact with the window to present it to the user is via the visible flag, now the problem is when the window is shown and closed via the close button on the top bar, the window get somehow unmounted(?) and the next time the user tries to interact with the app and re-open the window I get a segmentation fault.
One of the things I tried was to instead of setting the visibility to false, just re-create the window again, but I still get the segmentation error.
All the previous answers I found are dealing with the old way of dealing with the NSViewController and overriding the windowShouldClose method, but I cannot seem to get that working.
TL:DR: When the user presses the red close button on the window, instead of the window getting destroyed I just want to set its visibility to false
I made it work, no need to set the contentViewController, you can use the standard contentView:
configurationWindow.contentView = NSHostingView(rootView: configurationView)
and to disable the window getting released when closed:
configurationWindow.isReleasedWhenClosed = false
I would still be interested in knowing when the window closed, to maybe perform an action afterwards, but this still solves my problem

How do I get a titlebar to show programmatically in NSWindow?

Im trying to develop an OS X cocoa application programmatically, and I'm trying to display a window with a title bar which displays the usual traffic light (close, minimise, fullscreen) options at the top.
However, when the window displays on the screen, there is just an empty window.
Here is the code I am using:
class AppDelegate: NSObject, NSApplicationDelegate {
let window = NSWindow(contentRect: NSMakeRect(10, 10, 200, 200),
styleMask: NSWindowStyleMask.closable,
backing: NSBackingStoreType.buffered, defer: true)
func applicationDidFinishLaunching(_ aNotification: Notification) {
self.titleVisibility = .visible;
self.titlebarAppearsTransparent = false;
self.isMovableByWindowBackground = true;
let controller = NSWindowController(window: window)
controller.showWindow(self);
}
I have tried different NSWindowStyleMask when constructing the NSWindow but to no success.
This is what I see:
I am using Xcode 8.3 on 10.12
So you need 4 style masks.
NSWindowStyleMask.closable
NSWindowStyleMask.miniaturizable
NSWindowStyleMask.resizable
NSWindowStyleMask.titled
To put them all into one, you can use array
[NSWindowStyleMask.closable, NSWindowStyleMask.miniaturizable, NSWindowStyleMask.resizable, NSWindowStyleMask.titled]
But try to write in swift style
[.closable, .miniaturizable, .resizable, .titled]