MacOS Cocoa, why am i not able to close windows? - swift

i am working in XCode Swift, with Cocoa with Storyboards, and i need to be able to create and close my own windows, programmatically.
to repeat the problem as simply as possible, i start with an empty storyboard project, and change only viewDidLoad in ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
let rect = NSRect( x:500, y:500, width:800, height:500 )
let mask = NSWindow.StyleMask( arrayLiteral: .closable, .miniaturizable, .resizable, .titled )
let win = NSWindow( contentRect: rect, styleMask: mask, backing: .buffered, defer: false )
win.orderFront( win )
}
the application runs, and puts up two windows. the smaller one is from the storyboard and can be closed without trouble, but when i close the larger window which i created, the application crashes in the appDelegate with
Thread 1: EXC_BAD_ACCESS (code=1, address=0x20)
i assume i am doing something dumb here - what am i missing?
i discovered if i set
win.isReleasedWhenClosed = false
the problem goes away, however i assume the memory is not being released.
i also found that if i append the window handle to an array before closing it, the problem also goes away:
private var broken_windows [NSWindow] = []
public func windowShouldClose( _ sender: NSWindow ) -> Bool {
broken_windows.append( sender )
return true
}
however this array is somehow completely untouchable, and any attempt to access it's members after the window has been closed will cause the application to crash.

In your updated code, the window is only retained because you are holding a strong reference to it via self.win. If you use weak var win: NSWindow?, then self.win will be nil after the window is closed. So, setting isReleasedWhenClosed = false does seem to be a valid solution.

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...
}

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

Swift - how to make window unactivable?

I want to make application unactivable.
I mean its must be inactive and non-foreground even by click on it. Active window must be the same app as it was before click on my app.
How can I do this?
upd:
Let's imagine some window/panel. Like a Dock. Let's call it "Docky"
Docky is inactive, Active window is Safari:
NSRunningApplicationSafari.isActive == true
NSRunningApplicationDocky.isActive == false
I'm clicking on ANY empty point of Docky.
Expected result:
Safari is still active window. Like before click:
NSRunningApplicationSafari.isActive == true
NSRunningApplicationDocky.isActive == false
Actual result:
Docky is active window, Safari is inactive:
NSRunningApplicationSafari.isActive == false
NSRunningApplicationDocky.isActive == true
Another sample is Keyboard Viewer. You are clicking on the virtual keyboard, but active window is another app. Ant exactly active window getting keyPress events.
Actually all you need is .nonactivatingPanel style panel. Everything else is details, like level of this window, custom views with overridden acceptsFirstMouse:, needsPanelToBecomeKey, etc. Btw, button accepts first click by default, non activating app in this case.
So your AppDelegate, for example, might look like the following:
class AppDelegate: NSObject, NSApplicationDelegate {
var docky: NSPanel!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the window and set the content view.
docky = NSPanel(
contentRect: NSRect(x: 0, y: 0, width: 120, height: 600),
styleMask: [.nonactivatingPanel],
backing: .buffered, defer: false)
docky.level = .mainMenu
....
docky.orderFrontRegardless()
}

Highlight NSWindow under mouse cursor

Since this is quite a lot of code and it probably helps if there is a sample project where you can better understand the current problem I made a simple sample project which you can find on GitHub here: https://github.com/dehlen/Stackoverflow
I want to implement some functionality pretty similar what the macOS screenshot tool does. When the mouse hovers over a window the window should be highlighted. However I am having issues only highlighting the part of the window which is visible to the user.
Here is a screenshot of what the feature should look like:
My current implementation however looks like this:
My current implementation does the following:
1. Get a list of all windows visible on screen
static func all() -> [Window] {
let options = CGWindowListOption(arrayLiteral: .excludeDesktopElements, .optionOnScreenOnly)
let windowsListInfo = CGWindowListCopyWindowInfo(options, CGMainDisplayID()) //current window
let infoList = windowsListInfo as! [[String: Any]]
return infoList
.filter { $0["kCGWindowLayer"] as! Int == 0 }
.map { Window(
frame: CGRect(x: ($0["kCGWindowBounds"] as! [String: Any])["X"] as! CGFloat,
y: ($0["kCGWindowBounds"] as! [String: Any])["Y"] as! CGFloat,
width: ($0["kCGWindowBounds"] as! [String: Any])["Width"] as! CGFloat,
height: ($0["kCGWindowBounds"] as! [String: Any])["Height"] as! CGFloat),
applicationName: $0["kCGWindowOwnerName"] as! String)}
}
2. Get the mouse location
private func registerMouseEvents() {
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
self.mouseLocation = NSEvent.mouseLocation
return $0
}
NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { _ in
self.mouseLocation = NSEvent.mouseLocation
}
}
3. Highlight the window at the current mouse location:
static func window(at point: CGPoint) -> Window? {
// TODO: only if frontmost
let list = all()
return list.filter { $0.frame.contains(point) }.first
}
var mouseLocation: NSPoint = NSEvent.mouseLocation {
didSet {
//TODO: don't highlight if its the same window
if let window = WindowList.window(at: mouseLocation), !window.isCapture {
highlight(window: window)
} else {
removeHighlight()
}
}
}
private func removeHighlight() {
highlightWindowController?.close()
highlightWindowController = nil
}
func highlight(window: Window) {
removeHighlight()
highlightWindowController = HighlightWindowController()
highlightWindowController?.highlight(frame: window.frame, animate: false)
highlightWindowController?.showWindow(nil)
}
class HighlightWindowController: NSWindowController, NSWindowDelegate {
// MARK: - Initializers
init() {
let bounds = NSRect(x: 0, y: 0, width: 100, height: 100)
let window = NSWindow(contentRect: bounds, styleMask: .borderless, backing: .buffered, defer: true)
window.isOpaque = false
window.level = .screenSaver
window.backgroundColor = NSColor.blue
window.alphaValue = 0.2
window.ignoresMouseEvents = true
super.init(window: window)
window.delegate = self
}
// MARK: - Public API
func highlight(frame: CGRect, animate: Bool) {
if animate {
NSAnimationContext.current.duration = 0.1
}
let target = animate ? window?.animator() : window
target?.setFrame(frame, display: false)
}
}
As you can see the window under the cursor is highlighted however the highlight window is drawn above other windows which might intersect.
Possible Solution
I could iterate over the available windows in the list and only find the rectangle which does not overlap with other windows to draw the highlight rect only for this part instead of the whole window.
I am asking myself whether the would be a more elegant and more performant solution to this problem. Maybe I could solve this with the window level of the drawn HighlightWindow? Or is there any API from Apple which I could leverage to get the desired behavior?
I messed around with your code, and #Ted is correct. NSWindow.order(_:relativeTo) is exactly what you need.
Why NSWindow.level wont work:
Using NSWindow.level will not work for you because normal windows (like the ones in your screenshot) all have a window level of 0, or .normal. If you simply adjusted the window level to, say "1" for instance, your highlight view would appear above all the other windows. On the contrary, if you set it to "-1" your highlight view would appear below all normal windows, and above the desktop.
Problems to be introduced using NSWindow.order(_: relativeTo)
No great solution comes without caveats right? In order to use this method you will have to set the window level to 0 so it can be layerd among the other windows. However, this will cause your highlighting window to be selected in your WindowList.window(at: mouseLocation) method. And when it's selected, your if-statement removes it because it believes it's the main window. This will cause a flicker. (a fix for this is included in the TLDR below)
Also, if you attempt to highlight a window that does not have a level of 0, you will run into issues. To fix such issues you need to find the window level of the window you are highlighting and set your highlighting window to that level. (my code didn't include a fix for this problem)
In addition to the above problems, you need to consider what happens when the user hovers over a background window, and clicks on it without moving the mouse. What will happen is the background window will become front.. without moving the highlight window. A possible fix for this would be to update the highlight window on click events.
Lastly, I noticed you create a new HighlightWindowController + window every time the user moves their mouse. It may be a bit lighter on the system if you simply mutate the frame of an already exsisting HighlightWindowController on mouse movement (instead of creating one). To hide it you could call the NSWindowController.close() function, or even set the frame to {0,0,0,0} (not sure about the 2nd idea).
TLDR; Show us some code
Here's what I did.
1. Change your window struct to include a window number:
struct Window {
let frame: CGRect
let applicationName: String
let windowNumber: Int
init(frame: CGRect, applicationName: String, refNumber: Int) {
self.frame = frame.flippedScreenBounds
self.applicationName = applicationName
self.windowNumber = refNumber
}
var isCapture: Bool {
return applicationName.caseInsensitiveCompare("Capture") == .orderedSame
}
}
2. In your window listing function ie static func all() -> [Window], include the window number:
refNumber: $0["kCGWindowNumber"] as! Int
3. In your window highlighting function, after highlightWindowController?.showWindow(nil), order the window relative to the window you are highlighting!
highlightWindowController!.window!.order(.above, relativeTo: window.windowNumber)
4. In your highlight controller, make sure to set the window level back to normal:
window.level = .normal
5. The window will now flicker, to prevent this, update your view controller if-statement:
if let window = WindowList.window(at: mouseLocation) {
if !window.isCapture {
highlight(window: window)
}
} else {
removeHighlight()
}
Best of luck and have fun swifting!
Edit:
I forgot to mention, my swift version is 4.2 (haven't upgraded yet) so the syntax may be ever so slightly different.
I'm not used to Swift, sorry, but it seems to me the natural solution to this would be to use - orderWindow:relativeTo:. In ObjC that would be (added just after the highlight window is shown):
[highlightWindow orderWindow:NSWindowAbove relativeTo:window];
And let the window server handle all the details of hiding obscured portions. Of course, this creates a different headache of keeping the highlight window directly above the target window as users move stuff around on-screen, but...

How to manage multiple NSWindows in a dictionary

What I'm trying to do:
I'm currently developing a Command-Line-Tool that creates a blurred window for each desktop space available. I save these windows in a dictionary where the spaceIdentifier is the keyand the NSWindow is the value.
In order to keep the windows in sync with the spaces I periodically check if there are new spaces or if spaces are closed.
My Code:
import AppKit
import Foundation
func createBlurWindow(screen: NSScreen?, contentRect: NSRect, brightness: Int) -> NSWindow
{
# irrelevant code that creates a blurred window
}
class AppController: NSObject, NSApplicationDelegate
{
var spaceWindowList: [NSNumber: NSWindow] = [:]
func applicationDidFinishLaunching(_ aNotification: Notification)
{
self.manageWindows()
Timer.scheduledTimer(timeInterval: 5.0, target: self, selector: #selector(self.manageWindows), userInfo: nil, repeats: true)
}
#objc func manageWindows()
{
// create windows
for scr in NSScreen.screens {
let scrHeight = scr.frame.height
let scrWidth = scr.frame.width
let windowHeight = CGFloat(22)
let size = CGRect(x: 0, y: scrHeight-windowHeight, width: scrWidth, height: windowHeight)
let currentScreenSpaces = WindowHelper.getSpacesforScreen(scr)
for space in currentScreenSpaces!
{
if self.spaceWindowList[space] == nil
{
let w = createBlurWindow(screen: scr, contentRect: size, brightness: 9)
let windowID = NSNumber.init(value: w.windowNumber)
// #TODO this may fail if the space was created just now -> fix this by waiting some time
WindowHelper.moveWindow(windowID, toSpace: space)
self.spaceWindowList[space] = w
// debuginfo
let displayNr = scr.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")]!
print(getTime(), " Display:", displayNr, " added Window(", windowID , ") for Space: ", space, separator: "")
}
}
}
let currentSpaces = WindowHelper.getAllSpaces()
for (space, window) in self.spaceWindowList
{
// destroy window if space was closed
if !(currentSpaces?.contains(space))!
{
window.close()
//self.spaceWindowList.removeValue(forKey: space)
// debuginfo
print(getTime(), "Destroyed window of closed Space:", space)
}
}
}
}
let app = NSApplication.shared
let controller = AppController()
app.delegate = controller
app.run()
The complete source can be found here
My Problem:
When detecting that a space was closed I try to programatically close the corresponding NSWindow, however the next time manageWindows() is called the application segfaults on this loop: for (space, window) in self.spaceWindowList. To debug I activated zombieobjects and got the following errormessage -[NSWindow retain]: message sent to deallocated instance 0x101241150
I don't understand why there is still a pointer to my window the next time spaceWindowList is iterated. I also tried to nil the dictionary entry with self.spaceWindowList.removeValue(forKey: space) but that also lead to a segfault.
I'm fairly new to swift and macdev in general so I'm not that confident with memory management in swift.
I would really appreciate if someone could tell me the best way to safely close windows organized in some kind of dictionary/map/whatever.
PS:
If you are wondering why I don't use .canJoinAllSpaces instead of creating the window for each space: When using .canJoinAllSpaces you can see the blurred NSEffectView flickering on a space change if the spaces have different desktop images. The only way to circumvent this is to create individual windows for each space.