How to customize preview when context menu is presented in swift - swift

I have created custom UIViewController and included it in contextMenuInteraction (below example):
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: MyPawnPreviewProvider.init) { _ -> UIMenu? in
return self.createContextMenu()
}
}
I don't know what to put inside this class to properly customize it:
class MyPawnPreviewProvider: UIViewController {
private let imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(imageView)
}
}
What I want to achieve is when I long tap a knight icon (see screenshot), I want context menu to appear but without blurred background and without rescaling the icon.
the board of the game with knight icons
Default behaviour is that the icon is zoomed and background is blurred. I want the board to be visible all the time (apart from the context menu area of course).
after tap and hold the knight icon presenting context menu

Related

Status button highlight when popover is active (Swift, macOS)

I am making a status bar macOS app. On clicking on the status bar icon, I am showing an NSPopover (not an NSMenu). However, when my NSPopover is shown, my status menu icon is not highlighted. It is only highlighted for a moment when I click it. I want it to stay highlighted, much like how it behaves with the wifi status bar icon.
I know that if I use a NSMenu instead of a NSPopover, it can probably be fixed. But the requirement is such that I need to use a NSPopover.
I have tried the following approaches, but to no avail:
1.
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
if let button = statusItem.button {
button.setButtonType(.pushOnPushOff)
}
statusItem.highlightMode = true
statusItem.button?.highlight(true)
statusItem.button?.isHighlighted = true
I am not very experienced with status bar apps. So I am not really sure about which approach to take here.
The left most icon is my status bar app icon. The popover is currently active but the icon is not highlighted. I had to crop out the popover.
This can be done, but to do it reliably requires some tight coupling. In this example, I assume you have a NSViewController named PopoverController that has a togglePopover() method and that you've set this controller as the target of the status bar button.
Step 0: Context
Here's the basic setup of the class that controls the popover:
final class PopoverController: NSViewController
{
private(set) var statusItem: NSStatusItem!
private(set) var popover: NSPopover
// You can set up your StatusItem and Popover anywhere; but here's an example with -awakeFromNib()
override func awakeFromNib()
{
super.awakeFromNib()
statusItem = NSStatusBar.system.statusItem(withLength: 20.0)
statusItem.button?.sendAction(on: [.leftMouseDown, .rightMouseDown])
statusItem.button?.image = NSImage(named: "statusBar-icon")
statusItem.button?.target = self
statusItem.button?.action = #selector(togglePopover)
popover = NSPopover()
popover.behavior = .transient
popover.delegate = self
popover.contentViewController = self
popover.setValue(true, forKeyPath: "shouldHideAnchor") // Private API
}
#objc func togglePopover()
{
if popover.isShown
{
popover.performClose(nil)
}
else if !popover.isShown,
let button: NSButton = statusItem.button,
button.window?.contentView != nil, // Exception thrown if view is nil or not part of a window.
button.superview != nil
{
popover.show(relativeTo: .zero, of: button, preferredEdge: .minY)
}
}
}
Step 1: Override the Status Button
extension NSStatusBarButton
{
public override func mouseDown(with event: NSEvent)
{
if event.modifierFlags.contains(.control)
{
self.rightMouseDown(with: event)
return
}
if let controller: PopoverController = self.target as? PopoverController
{
controller.togglePopover()
self.highlight(controller.popover.isShown)
}
}
}
Step 2: Handle Popover Closing
Make sure PopoverController conforms to NSPopoverDelegate and implement this delegate method:
func popoverDidClose(_ notification: Notification)
{
statusItem.button?.highlight(false)
}
Outcome
With all of that in place, the button highlighting now works just as it does for Apple's system status bar items like Control Center, Wifi, Battery, etc.
Note: you'll also need to add a global event monitor to listen for clicks that happen outside of your popover to ensure that it closes properly when the user clicks away from it. But that's outside the scope of this question and available elsewhere on SO.

How can I show/hide a button added to the title bar of an NSWindow?

I have created a method in an NSWindow extension that allows me to add a button next to the text in the title bar. This is similar to the "down chevron" button that appears in the title bar of Pages and Numbers. When the button is clicked, an arbitrary code, expressed as a closure, is run.
While I have that part working fine, I would also like the button to be invisible most of the time and only become visible when the mouse is scrolled into the title bar area. This would be mimicking the way that Pages and Numbers displays the button.
However, I'm having difficulties getting the show/hide to work properly. I believe I can do it if I make it completely custom in the application delegate, and possibly by subclassing NSWindow, but I would really like to keep it as a single method in an NSWindow extension. In this way the code would be easily reusable in multiple applications.
To accomplish this I believe I need to inject an additional handler/listener that will tell me when the mouse enters and leaves the appropriate area. I can define the necessary area using an NSTrackingArea, but I haven't figured out how to "inject" an event listener without the need of subclasses. Does anyone know how (or if) such a thing is possible?
The key to handling the show/hide based on the mouse position was to use an NSTrackingArea to signify the portion that we are interested in, and to handle the mouse enter and mouse exit events. But since this can't be done directly on the title bar view (since we have to subclass the view in order to add the event handlers) we need to create an additional NSView that is invisible but covers the area we want to track.
I'll post the full code below, but the key parts related to this question are the TrackingHelper class defined near the bottom of the file and the way it is added to the titleBarView with its constrains set to be equal to the size of the title bar. The class itself is designed to take three closures, one for the mouse enter event, one for the mouse exit, and one for the action to take when the button is pressed. (Technically the latter doesn't really need to be part of the TrackingHelper, but it is a convenient place to put it to ensure it does not go out of scope while the UI still exists. A more correct solution would be to subclass NSButton to keep the closure, but I have always found subclassing NSButton to be a royal pain.)
Here is the full text of the solution. Note that this has a couple of things that depend on another library of mine - but they are not necessary for the understanding of this problem and are used to deal with the button image. If you wish to use this code you will need to replace the getImage function with one that creates the image you want. (And if you want to see what KSSCocoa is adding, you can obtain it from https://github.com/klassen-software-solutions/KSSCore)
//
// NSWindowExtension.swift
//
// Created by Steven W. Klassen on 2020-02-24.
//
import os
import Cocoa
import KSSCocoa
public extension NSWindow {
/**
Add an action button to the title bar. This will add a "down chevron" icon, similar to the one used in
Numbers and Pages, just to the right of the title in the title bar. When clicked it will run the given
lambda.
*/
#available(OSX 10.14, *)
func addTitleActionButton(_ lambda: #escaping () -> Void) -> NSButton {
guard let titleBarView = getTitleBarView() else {
fatalError("You can only add a title action to an app that has a title bar")
}
guard let titleTextField = getTextFieldChild(of: titleBarView) else {
fatalError("You can only add a title action to an app that has a title field")
}
let trackingHelper = TrackingHelper()
let actionButton = NSButton(image: getImage(),
target: trackingHelper,
action: #selector(trackingHelper.action))
actionButton.setButtonType(.momentaryPushIn)
actionButton.translatesAutoresizingMaskIntoConstraints = false
actionButton.isBordered = false
actionButton.isEnabled = false
actionButton.alphaValue = 0
trackingHelper.translatesAutoresizingMaskIntoConstraints = false
trackingHelper.onButtonAction = lambda
trackingHelper.onMouseEntered = {
actionButton.isEnabled = true
actionButton.alphaValue = 1
}
trackingHelper.onMouseExited = {
actionButton.isEnabled = false
actionButton.alphaValue = 0
}
titleBarView.addSubview(trackingHelper)
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[trackingHelper]-0-|",
options: [], metrics: nil,
views: ["trackingHelper": trackingHelper]))
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[trackingHelper]-0-|",
options: [], metrics: nil,
views: ["trackingHelper": trackingHelper]))
titleBarView.addSubview(actionButton)
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:[titleTextField]-[actionButton(==7)]",
options: [], metrics: nil,
views: ["actionButton": actionButton,
"titleTextField": titleTextField]))
titleBarView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-1-[actionButton]-3-|",
options: [], metrics: nil,
views: ["actionButton": actionButton]))
DistributedNotificationCenter.default().addObserver(
actionButton,
selector: #selector(actionButton.onThemeChanged(notification:)),
name: NSNotification.Name(rawValue: "AppleInterfaceThemeChangedNotification"),
object: nil
)
return actionButton
}
fileprivate func getTitleBarView() -> NSView? {
return standardWindowButton(.closeButton)?.superview
}
fileprivate func getTextFieldChild(of view: NSView) -> NSTextField? {
for subview in view.subviews {
if let textField = subview as? NSTextField {
return textField
}
}
return nil
}
}
fileprivate extension NSButton {
#available(OSX 10.14, *)
#objc func onThemeChanged(notification: NSNotification) {
image = image?.inverted()
}
}
#available(OSX 10.14, *)
fileprivate func getImage() -> NSImage {
var image = NSImage(sfSymbolName: "chevron.down")!
if NSApplication.shared.isDarkMode {
image = image.inverted()
}
return image
}
fileprivate final class TrackingHelper : NSView {
typealias Callback = ()->Void
var onMouseEntered: Callback? = nil
var onMouseExited: Callback? = nil
var onButtonAction: Callback? = nil
override func mouseEntered(with event: NSEvent) {
onMouseEntered?()
}
override func mouseExited(with event: NSEvent) {
onMouseExited?()
}
#objc func action() {
onButtonAction?()
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
for trackingArea in self.trackingAreas {
self.removeTrackingArea(trackingArea)
}
let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways]
let trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil)
self.addTrackingArea(trackingArea)
}
}

Cursor isn’t changed properly when drag a file to a draggable view on Cocoa's WKWebView

I wrote the following code to put a draggable view on WKWebView.
With this code, I expected a "+" icon will be displayed nearby a cursor when I dragged a file to the view.
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rect = NSRect(x: 0, y: 0, width: 200, height: 200)
let webView = WKWebView(
frame: rect,
configuration: WKWebViewConfiguration())
webView.load(URLRequest(
url: URL(string: "https://i.imgur.com/D5ru3Q7.jpg")!))
let draggableView = DraggableView(frame: rect)
draggableView.registerForDraggedTypes([.fileURL])
self.view.addSubview(webView)
self.view.addSubview(draggableView)
}
}
class DraggableView: NSView {
override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
return .copy
}
}
The result is here:
Sometimes the cursor changes to a magnifying glass (same as a mouse over behavior on WKWebView).
And sometimes "+" icon is shown.
I think the webView prevents the cursor changing.
So I tried the followings. But I couldn't fix.
Overriding hitTest of the webView worked only for non-dragging mouse over actions. But not for dragging.
webView.unregisterDraggedTypes() didn't work.
Is there anyway to fix this?
Found a solution:
for t in webView.trackingAreas {
webView.removeTrackingArea(t)
}
I think removing tracking areas on a web view is not a good idea.
But at least it satisfies my need.

Modal Panel with rounded corners

I'm wondering if there is a way to display a modal window with rounded corners instead of the default sharp corners. The image shows what corners I'm referring to.
I tried with changing the contentView.layer?.cornerRadius but it didn't work. What can I do to get the result I need?
After working a lot on it, I finally found a solution:
Frist create a new borderless window in your interface builder. Place a custom box in that view and make sure it leaves a bit of space from the top border of the window:
Then add an outlet of that window object in your app delegate:
#IBOutlet weak var saveWindow: NSWindow!
So copy that extension for loading and dismissing a panel as a modal sheet:
extension NSWindow {
public func loadPanel(named: NSWindow) {
named.isOpaque = false
named.backgroundColor = NSColor.clear
named.hasShadow = false
self.beginSheet(named, completionHandler: nil)
}
public func closePanel(named: NSWindow) {
self.endSheet(named)
}
}
You just need to call this two functions if you want to open a panel.
window.loadPanel(named: saveWindow)
And when you're done:
window.closePanel(named: saveWindow)
This is the result:

How can I use NSVisualEffectView to blend window with background

There seem to be a bunch of questions on this for old versions of Swift/Xcode, but for some reason it hasn't been working with the latest update. I created a NSVisualEffectView, blurryView, and added the subview to my main view:
class ViewController: NSViewController {
#IBOutlet weak var blurryView: NSVisualEffectView!
override func viewDidLoad() {
super.viewDidLoad()
//background styling
blurryView.wantsLayer = true
blurryView.blendingMode = NSVisualEffectBlendingMode.behindWindow
blurryView.material = NSVisualEffectMaterial.dark
blurryView.state = NSVisualEffectState.active
self.view.addSubview(blurryView, positioned: NSWindowOrderingMode.above, relativeTo: nil)
// Do any additional setup after loading the view.
}
...
}
But when I run it, there is no effect on the window. (when I set it to within window, and layer it on top of my other view, the blur works correctly, but I only want the window to blur.) I also tried doing the same thing in my App Delegate class, but I can't connect my window as an outlet, and therefore can't add the blurry view to the window. Here's what the code would look like:
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
blurryView.wantsLayer = true
blurryView.blendingMode = NSVisualEffectBlendingMode.withinWindow
blurryView.material = NSVisualEffectMaterial.dark
blurryView.state = NSVisualEffectState.active
self.window.contentView?.addSubview(blurryView)
}
...
}
To get an idea if what I'm looking for: NSVisualEffectView Vibrancy
It works quite easy:
In Interface Builder drag a NSVisualEffectView directly as a subview of the main view of your scene.
In the Properties Inspector set Blending Mode to Behind Window
Add the rest of the views you need as subviews of the NSVisualEffectView
That's it, you're done
Here's an example:
Panel 1 View Controller is my blurred view, Background View is the first (non-blurred) view in my "real"view hierarchy.
Swift 5:
Simply add this to your viewWillAppear and it should work:
override func viewWillAppear() {
super.viewWillAppear()
//Adds transparency to the app
view.window?.isOpaque = false
view.window?.alphaValue = 0.98 //you can remove this line but it adds a nice effect to it
let blurView = NSVisualEffectView(frame: view.bounds)
blurView.blendingMode = .behindWindow
blurView.material = .fullScreenUI
blurView.state = .active
view.window?.contentView?.addSubview(blurView)
}