How to show NSPopover without dismissing the first responder? - swift

I'm working on a mac app that has similar functionalities to the rocket app.
Whenever the user types : outside of my app I'll show an NSPopover with NSTableView near the cursor.
The problem I'm facing here is that The moment I show the NSPopover it becomes the first responder and the user couldn't continue typing.
So I tried this, Showed the NSPopover without making a first responder, and used Global listener to listen UpArrow, DownArrow and Enter actions to navigate and select results.
The downside of this approach: Let's say I'm composing a new mail, and the cursor responding to the up/down arrow events, eventually moved to other lines.
func showWidget(at origin: CGPoint, height: CGFloat) {
let popUpView = NSView()
let window: NSWindow = .getWindow(with: .init(width: 1, height: 1), origin: origin, withBorder: false)
window.contentView?.setBackground(color: .white)
window.contentView?.addSubview(popUpView)
popUpView.alignEdges()
popUpView.setBackground(color: .white)
window.makeKeyAndOrderFront(nil)
popover.contentViewController = widgetController
popover.behavior = .semitransient
popover.contentSize = .init(width: .inputWidgetSize.width, height: 200)
popover.animates = false
popover.appearance = NSAppearance.init(named: .darkAqua)
popover.show(relativeTo: .init(origin: .init(x: .zero, y: 0), size: .init(width: .inputWidgetSize.width, height: height)), of: popUpView, preferredEdge: .maxY)
}
Attaching video for reference:
The functionality that I'm expecting
From my app
Note: Notice the cursor position to understand what going wrong.

Related

Draw shape around cursor on macOS using Swift [duplicate]

This question already has an answer here:
How do I change my cursor on the whole screen? (not only current view/window)
(1 answer)
Closed 1 year ago.
I'm pretty new to macOS and swift development and I have been watching a few tutorials to figure this out. I also watched a udemy course that worked on 18 macOS projects which got me nowhere
All I want to do is a macOS menu bar app that will add a cursor highlight that should look something like:
I could get the cursor changed to an image doing the following
import SpriteKit
class CursorView: SKView {
override func resetCursorRects() {
if let targetImage = NSImage(named: "cursor") {
let cursor = NSCursor(image: targetImage,
hotSpot: CGPoint(x: targetImage.size.width / 2,
y: targetImage.size.height / 2))
addCursorRect(frame, cursor: cursor)
}
}
}
Three things wrong with this:
SKView is a class from SpriteKit and I don't think I should use that for my use-case
This calls the addCursorRect that add the changes to a window frame (I need to all the time regardless of the frame)
I can't have 100's of images for each style I would set in the future for the highlight color, size, or opacity
So, I'm here trying to understand how I can do this for a menu bar app that should be available on all screens and achieve a highlight as should in the above picture
Not sure if this matters but I'm using storyboard and don't mind switching to SwiftUI
I could really use some help from the community on this. Thank you
You can annotate the system mouse by drawing something around it. This can be done by
adding an CGEvent tap to capture mouse events
drawing the annotation around the cursor using a custom window
Here is a simple example application
import SwiftUI
#main
class AppDelegate: NSObject, NSApplicationDelegate {
var mouseTap: CFMachPort?
private var window: NSWindow = {
// Create the SwiftUI view that provides the window contents.
let contentView = Circle()
.stroke(lineWidth: 2)
.foregroundColor(.blue)
.frame(width: 30, height: 30)
.padding(2)
// Create the window and set the content view.
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 34, height: 34),
styleMask: [.borderless],
backing: .buffered,
defer: false
)
window.contentView = NSHostingView(rootView: contentView)
window.backgroundColor = .clear
window.level = NSWindow.Level.statusBar
window.makeKeyAndOrderFront(nil)
return window
}()
func applicationDidFinishLaunching(_ aNotification: Notification) {
if let tap = createMouseTap() {
if CGEvent.tapIsEnabled(tap: tap) {
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, CFRunLoopMode.commonModes)
mouseTap = tap
} else {
print("tap not enabled")
mouseTap = nil
}
} else {
print("tap not enabled")
}
}
func createMouseTap() -> CFMachPort? {
withUnsafeMutableBytes(of: &window) { pointer in
CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: CGEventTapOptions.listenOnly,
eventsOfInterest: (1 << CGEventType.mouseMoved.rawValue | 1 << CGEventType.leftMouseDragged.rawValue),
callback: mouseMoved,
userInfo: pointer.baseAddress
)
}
}
}
func mouseMoved(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, context: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
let window = context!.assumingMemoryBound(to: NSWindow.self).pointee
// using CGPoint+SIMD extension from https://gist.github.com/Dev1an/7973cee9d960479b35b705f88b7f38c4
window.setFrameOrigin(event.unflippedLocation - 17)
return nil
}
Drawback
Note that this implementation does require the user to allow the keyboard input option in "Universal Access" in "System Preferences".

NSPanel not hiding when focus is lost

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()
}
}
}

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 to open a NSPopover at a distance from the system bar?

I'm opening a NSPopover with the action of an icon in the status bar.
myPopover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
This works fine with the exception that the distance from the popover and the system bar is zero:
I'd like to achieve the same result as the Dropbox app which renders the popover at a small distance from the system bar:
I've tried using button.bounds.offsetBy(dx: 0.0, dy: 20.0) which doesn't affect the position of the popover and button.bounds.offsetBy(dx: 0.0, dy: -20.0) which puts the popover above the system bar:
So how can I position the NSPopover at some distance from the system bar?
First, the reason why button.bounds.offsetBy(dx: 0.0, dy: -20.0) didn't work is because those coordinate fell outside the "window" of the status bar item which is the status bar itself. So anything outside of it was cropped.
I solved this problem by collecting information here and there:
Create an invisible window.
Find the coordinates in the screen of the status bar item and position the invisible window under it.
Show the NSPopover in relation to the invisible window and not the status bar item.
The red thing is the invisible window (for demonstration purposes).
Swift 4 (Xcode 9.2)
// Create a window
let invisibleWindow = NSWindow(contentRect: NSMakeRect(0, 0, 20, 5), styleMask: .borderless, backing: .buffered, defer: false)
invisibleWindow.backgroundColor = .red
invisibleWindow.alphaValue = 0
if let button = statusBarItem.button {
// find the coordinates of the statusBarItem in screen space
let buttonRect:NSRect = button.convert(button.bounds, to: nil)
let screenRect:NSRect = button.window!.convertToScreen(buttonRect)
// calculate the bottom center position (10 is the half of the window width)
let posX = screenRect.origin.x + (screenRect.width / 2) - 10
let posY = screenRect.origin.y
// position and show the window
invisibleWindow.setFrameOrigin(NSPoint(x: posX, y: posY))
invisibleWindow.makeKeyAndOrderFront(self)
// position and show the NSPopover
mainPopover.show(relativeTo: invisibleWindow.contentView!.frame, of: invisibleWindow.contentView!, preferredEdge: NSRectEdge.minY)
NSApp.activate(ignoringOtherApps: true)
}
I was trying to use show(relativeTo: invisibleWindow.frame ...) and the popup wasn't showing up because NSWindow is not an NSView. For the popup to be displayed a view has to be passed.
you can move the contentView of the popover right after showing it:
myPopover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
let popoverWindowX = myPopover.contentViewController?.view.window?.frame.origin.x ?? 0
let popoverWindowY = myPopover.contentViewController?.view.window?.frame.origin.y ?? 0
myPopover.contentViewController?.view.window?.setFrameOrigin(
NSPoint(x: popoverWindowX, y: popoverWindowY + 20)
)
myPopover.contentViewController?.view.window?.makeKey()
in terms of UI you will get a slight slide of the arrow but in your case, with the very small padding, it'll be most imperceptible.
i'm using something similar to make sure my popover doesn't go offscreen. you can see the slight slide.

How to get action of button in Swift MacOS

I am trying to set up zoom for a game where you click on a button to zoom in, and another to zoom out (which will be part of the UI).
I am having problems getting the input of the buttons to the code. I can read that the button is not pressed, but I cannot figure out how to read if it is pressed.
Looking through the API reference gave me the idea to try using the "state" of the button, but it always returns 0, even when the button is pressed. This could be because the update function does not update as fast as the button's state changes.
Here is my code:
class GameScene: SKScene {
override func sceneDidLoad() {
cam = SKCameraNode()
cam.xScale = 1
cam.yScale = 1
self.camera = cam
self.addChild(cam)
cam.position = CGPoint(x: 0, y: 0)
setUpUI()
}
func setUpUI(){
let button1Rect = CGRect(x: 20, y: 80, width: 80, height: 40)
let zoomIn = NSButton(frame: button1Rect) //place a button in the Rect
zoomIn.setButtonType(NSMomentaryPushInButton)
zoomIn.title = "Zoom In"
zoomIn.alternateTitle = "Zoom In"
zoomIn.acceptsTouchEvents = true
view?.addSubview(zoomIn) //put button on screen
let button2Rect = CGRect(x: 20, y: 30, width: 80, height: 40)
let zoomOut = NSButton(frame: button2Rect) //place a button in the Rect
zoomOut.setButtonType(NSMomentaryPushInButton)
zoomOut.title = "Zoom Out"
zoomOut.alternateTitle = "Zoom Out"
zoomOut.acceptsTouchEvents = true
view?.addSubview(zoomOut) //put button on screen
if zoomIn.state == 1{ //this loop is never true no matter how many times I press the button
print("zoom")
}
}
}
I did cut out some of the code involving the mouse as it is not relevant.
Answer is from TheValyreanGroup.
Simply define the button outside of the setUpUI() function (outside of the GameScene or within it works, outside of the GameScene is easier).