How do I add the Share submenu in a Mac app? An example is Safari > File > Share. I poked at the Apple SharingServices sample code, but it does not include a working menu item.
Right now I have a button that displays a picker of available sharing services when tapped:
NSMutableArray *shareItems = [NSMutableArray arrayWithObject:[self.noteSynopsisView string]];
NSSharingServicePicker *sharingServicePicker = [[NSSharingServicePicker alloc] initWithItems:shareItems];
sharingServicePicker.delegate = self;
[sharingServicePicker showRelativeToRect:[self.shareButton bounds] ofView:self.shareButton preferredEdge:NSMaxYEdge];
I've also defined a Share submenu item under the File menu for my MainWindow.xib.
As I understand it, the NSSharingService list is being generated on the fly. So I can't really predefine the services to the menu item I have created in Interface Builder.
Thanks for your help.
Look at NSSharingService's +sharingServicesForItems:. In a -validateMenuItem: method you could create a submenu using the -title and -image of the NSSharingServices it returns. Associate each service with each menu item, and point the action of each menu item at this:
- (IBAction)shareFromService:(id)sender {
[[sender representedObject] performWithItems: arrayOfItemsToShare];
}
It's really quite simple. Apple did a good job on this one.
I find the gist can help you easily create a submenu of proper services.
https://gist.github.com/eternalstorms/4132533
It's a NSSharingServicePicker category.
Swift version:
extension NSSharingServicePicker {
class func menu(forSharingItems items: [AnyHashable]) -> NSMenu? {
let sharingServices = NSSharingService.sharingServices(forItems: items)
if sharingServices.isEmpty {
return nil
}
let menu = NSMenu()
for service in sharingServices {
let item = MenuItem(label: service.title, action: #selector(_openSharingService), target: self, userInfo: ["sharingItems": items])
item.image = service.image
item.representedObject = service
item.target = self
menu.addItem(item)
}
return menu
}
#objc class private func _openSharingService(sender: MenuItem) {
guard let items = sender.userInfo["sharingItems"] as? [AnyHashable], let service = sender.representedObject as? NSSharingService else {
return
}
service.perform(withItems: items)
}
}
class MenuItem: NSMenuItem {
var userInfo: [String : Any] = [:]
init(label: String, action: Selector?, target: AnyObject?, userInfo: [String : Any]) {
self.userInfo = userInfo
super.init(title: label, action: action, keyEquivalent: "")
}
required init(coder decoder: NSCoder) {
super.init(coder: decoder)
}
}
Related
I try to add a .xib based custom view into another .xib based custom view.
The result is looking like this:
for sub in v.subviews {
Swift.print(v.subviews) // returns array [sub]
Swift.print(sub) // returns sub
Swift.print(sub.superview) // return nil!
}
How can a view be in superview's subviews, but the superview property not correctly set? Can this happen during de/coding? What do I need to set in order for this to be correct?
The next question would be, why sub is shown correctly in "View Debugging" but not in when I need it during run time.
EDIT: (thanks Matt for looking into this)
My Code looks like this:
import AppKit
func showXIBDefinedInPanel(name: String, title: String ) {
if let w = loadXIBDefined(name: name) {
let c = NSViewController()
c.view = w
let window = NSPanel(contentViewController: c)
window.isMovable = true
window.collectionBehavior = .canJoinAllSpaces
window.tabbingMode = .disallowed
window.title = title
window.styleMask = [ .titled, .utilityWindow, .closable]
window.makeKeyAndOrderFront(w)
}
}
func loadXIBDefined(name: String) -> XIBDefined? {
var topLevelObjects : NSArray?
var result : XIBDefined? = nil
if Bundle.main.loadNibNamed(NSNib.Name(rawValue: name), owner: nil, topLevelObjects: &topLevelObjects) {
result = topLevelObjects!.first(where: { $0 is XIBDefined }) as? XIBDefined
}
return result
}
///used to embed a XIBDefined into another XIB
#IBDesignable class XIBEmbedder : NSView {
// Our custom view from the XIB file
var view: NSView!
var xibName: String!
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
view = loadXIBDefined(name: xibName)
addSubview(view)
self.frame = view.frame
}
required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
view = loadXIBDefined(name: xibName)
addSubview(view)
self.frame = view.frame
}
init(name: String) {
self.xibName = name
super.init(frame: NSZeroRect)
view = loadXIBDefined(name: xibName)
addSubview(view)
self.frame = view.frame
}
}
///used as class for XIB based Custom Views
#IBDesignable class XIBDefined: NSView {
///I had an issue with an oddly moved frame, so I hard coded a fix
override func setFrameOrigin(_ newOrigin: NSPoint) {
super.setFrameOrigin(NSZeroPoint)
needsDisplay = true
}
}
#IBDesignable class WelcomeCMapView : XIBEmbedder {
init() {
super.init(name: "Welcome Concept Maps")
}
required init?(coder decoder: NSCoder) {
super.init(name: "Welcome Concept Maps")
}
}
Key are the two classes XIBDefined and XIBEmbedder. Former one can be loaded from a XIB, the latter can be used in an XIB. Therefore the later embedded XIB uses XIBDefined as the custom class, the embedding XIB has a WelcomeCMapView as custom class.
The problem-showing code on top is part of the post-processing, which is executed within viewDidLoad() from the NSViewController loading the embedding XIB.
I was trying to figure out how to create a custom view using xib files.
In this question the next method is used.
NSBundle.mainBundle().loadNibNamed("CardView", owner: nil, options: nil)[0] as! UIView
Cocoa has the same method,however, this method has changed in swift 3 to loadNibNamed(_:owner:topLevelObjects:), which returns Bool, and previous code generates "Type Bool has no subscript members" error, which is obvious, since the return type is Bool.
So, my question is how to a load view from xib file in Swift 3
First of all the method has not been changed in Swift 3.
loadNibNamed(_:owner:topLevelObjects:) has been introduced in macOS 10.8 and was present in all versions of Swift. However loadNibNamed(nibName:owner:options:) has been dropped in Swift 3.
The signature of the method is
func loadNibNamed(_ nibName: String,
owner: Any?,
topLevelObjects: AutoreleasingUnsafeMutablePointer<NSArray>?) -> Bool
so you have to create an pointer to get the array of the views on return.
var topLevelObjects = NSArray()
if Bundle.main.loadNibNamed("CardView", owner: self, topLevelObjects: &topLevelObjects) {
let views = (topLevelObjects as Array).filter { $0 is NSView }
return views[0] as! NSView
}
Edit: I updated the answer to filter the NSView instance reliably.
In Swift 4 the syntax slightly changed and using first(where is more efficient:
var topLevelObjects : NSArray?
if Bundle.main.loadNibNamed(assistantNib, owner: self, topLevelObjects: &topLevelObjects) {
return topLevelObjects!.first(where: { $0 is NSView }) as? NSView
}
Swift 4 version of #vadian's answer
var topLevelObjects: NSArray?
if Bundle.main.loadNibNamed(NSNib.Name(rawValue: nibName), owner: self, topLevelObjects: &topLevelObjects) {
return topLevelObjects?.first(where: { $0 is NSView } ) as? NSView
}
I wrote an extension that is safe and makes it easy to load from nib:
extension NSView {
class func fromNib<T: NSView>() -> T? {
var viewArray = NSArray()
guard Bundle.main.loadNibNamed(String(describing: T.self), owner: nil, topLevelObjects: &viewArray) else {
return nil
}
return viewArray.first(where: { $0 is T }) as? T
}
}
Then just use like this:
let view: CustomView = .fromNib()
Whether CustomView is a NSView subclass and also CustomView.xib.
I am trying to combine my custom touch bar items together with the automatic text suggestions in the touch bar while editing text field.
Currently i am overriding makeTouchBar in custom NSTextView class, if i wont do that, the default touch bar will be created for the textView.
This is the main makeTouchBar, where i try to add the suggestions with item identifier .candidateList, but no luck:
extension ViewController: NSTouchBarDelegate {
override func makeTouchBar() -> NSTouchBar? {
let touchBar = NSTouchBar()
touchBar.delegate = self
touchBar.customizationIdentifier = .myBar
touchBar.defaultItemIdentifiers = [.itemId1,
.flexibleSpace,
.itemId2,
.itemId3,
.flexibleSpace,
.candidateList]
touchBar.customizationAllowedItemIdentifiers = [.itemId1]
return touchBar
}
}
Can someone provide a simple example of how to add this words suggestions item to a custom touch bar?
Easy. Just call super in your custom NSTextView class:
override func makeTouchBar() -> NSTouchBar {
var touchBar = super.makeTouchBar()
touchBar.delegate = self
var defaultIdentifiers = [Any](arrayLiteral:touchBar.defaultItemIdentifiers)
defaultIdentifiers.insert("CustomLabel", at: 0)
touchBar.defaultItemIdentifiers = defaultIdentifiers
return touchBar
}
override func touchBar(_ touchBar: NSTouchBar, makeItemFor identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem {
if (identifier == "CustomLabel") {
var button = NSButton(title: "Custom", target: self, action: nil)
var item = NSCustomTouchBarItem(identifier: "CustomLabel")
item.view = button
item.customizationLabel = "Custom"
return item
}
else {
return super.touchBar(touchBar, makeItemFor: identifier)
}
return nil
}
Im trying to add an item to an existing contextual menu in xcode. So far ive managed to add an item to ALL contextual menus opposed to a specific one.I am using "NSMenuDidBeginTrackingNotification.object"to reference the menu object, but not sure this is the correct way to go. code below. any ideas??
Thanks a lot!
import AppKit
var sharedPlugin: plugin?
class plugin: NSObject {
var bundle: NSBundle
lazy var center = NSNotificationCenter.defaultCenter()
// object holding all notifications:
var notificationSet = NSMutableSet();
class func pluginDidLoad(bundle: NSBundle) {
let appName = NSBundle.mainBundle().infoDictionary?["CFBundleName"] as? NSString
if appName == "Xcode" {
sharedPlugin = plugin(bundle: bundle)
}
}
init(bundle: NSBundle) {
self.bundle = bundle
self.notificationSet = NSMutableSet()
super.init()
// see name: "NSMenuDidBeginTrackingNotification":
center.addObserver(self, selector: #selector(self.createMenuItems), name:"NSMenuDidBeginTrackingNotification", object: nil)
}
deinit {
removeObserver()
}
func removeObserver() {
center.removeObserver(self)
}
func createMenuItems(notification: NSNotification) {
// checking what pops up the context menu when right clicking the console:
if (!(self.notificationSet).containsObject(notification.name)) {
print(notification.name)
print(notification.object?.className)
self.notificationSet.addObject(notification.name);
let menu = notification.object;
//creating menu item 'testing' :
let menuItem = NSMenuItem(title:"testing", action:#selector(self.doMenuAction), keyEquivalent:"");
menuItem.target = self
//adding item to context menu:
menu!.addItem(menuItem)
let actionMenuItem = NSMenuItem(title:"Do Action", action:#selector(self.doMenuAction), keyEquivalent:"")
actionMenuItem.target = self
}
}
func doMenuAction() {
print("hi")
self.notificationSet.removeAllObjects()
}
}
I'm using a WKWebView in a Mac OS X application. I want to override the contextual menu that appears when the user Control + clicks or right clicks in the WKWebView, but I cannot find a way to accomplish this.
It should be noted that the context menu changes depending on the state of the WKWebView and what element is under the mouse when the context menu is invoked. For example, the context menu only has a single "Reload" item when the mouse is over an "empty" part of the content, whereas right clicking a link presents the options "Open Link", "Open Link In New Window", and so on. It would be helpful to have granular control over these different menus if possible.
The older WebUIDelegate provides the - webView:contextMenuItemsForElement:defaultMenuItems:
method that allows you to customize the context menu for WebView instances; I'm essentially looking for the analog to this method for WKWebView, or any way to duplicate the functionality.
You can do this by intercepting the contextmenu event in your javascript, reporting the event back to your OSX container through a scriptMessageHandler, then popping up a menu from OSX. You can pass context back through the body field of the script message to show an appropriate menu, or use a different handler for each one.
Setting up callback handler in Objective C:
WKUserContentController *contentController = [[WKUserContentController alloc]init];
[contentController addScriptMessageHandler:self name:#"callbackHandler"];
config.userContentController = contentController;
self.mainWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:config];
Javascript code using jquery:
$(nodeId).on("contextmenu", function (evt) {
window.webkit.messageHandlers.callbackHandler.postMessage({body: "..."});
evt.preventDefault();
});
Responding to it from Objective C:
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:#"callbackHandler"]) {
[self popupMenu:message.body];
}
}
-(void)popupMenu:(NSString *)context {
NSMenu *theMenu = [[NSMenu alloc] initWithTitle:#"Context Menu"];
[theMenu insertItemWithTitle:#"Beep" action:#selector(beep:) keyEquivalent:#"" atIndex:0];
[theMenu insertItemWithTitle:#"Honk" action:#selector(honk:) keyEquivalent:#"" atIndex:1];
[theMenu popUpMenuPositioningItem:theMenu.itemArray[0] atLocation:NSPointFromCGPoint(CGPointMake(0,0)) inView:self.view];
}
-(void)beep:(id)val {
NSLog(#"got beep %#", val);
}
-(void)honk:(id)val {
NSLog(#"got honk %#", val);
}
You can intercept context menu items of the WKWebView class by subclassing it and implementing the willOpenMenu method like this:
class MyWebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadImage" ||
menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadLinkedFile" {
menuItem.action = #selector(menuClick(_:))
menuItem.target = self
}
}
}
#objc func menuClick(_ sender: AnyObject) {
if let menuItem = sender as? NSMenuItem {
Swift.print("Menu \(menuItem.title) clicked")
}
}
}
Instead of this you can also simply hide the menu items with menuItem.isHidden = true
Detecting the chosen menu item is one thing, but knowing what the user actually clicked in the WKWebView control is the next challenge :)
It's also possible to add new menu items to the menu.items array.
Objective C solution. The best solution is to subclass WKWebView and intercept mouse clicks. It works great.
#implementation WKReportWebView
// Ctrl+click seems to send this not rightMouse
-(void)mouseDown:(NSEvent *)event
{
if(event.modifierFlags & NSEventModifierFlagControl)
return [self rightMouseDown:event];
[super mouseDown:event]; // Catch scrollbar mouse events
}
-(void)rightMouseDown:(NSEvent *)theEvent
{
NSMenu *rightClickMenu = [[NSMenu alloc] initWithTitle:#"Print Menu"];
[rightClickMenu insertItemWithTitle:NSLocalizedString(#"Print", nil) action:#selector(print:) keyEquivalent:#"" atIndex:0];
[NSMenu popUpContextMenu:rightClickMenu withEvent:theEvent forView:self];
}
#end
This answer builds on the excellent answers in this thread.
The challenges in working with the WKWebView's context menu are:
It can only be manipulated in a subclass of WKWebView
WebKit does not expose any information about the HTML element that the user right-clicked on. Thus, information about the element must be intercepted in JavaScript and plumbed back into Swift.
Intercepting and finding information about the element the user clicked on happens by injecting JavaScript into the page prior to rendering, and then by establishing a callback into Swift. Here is the class that I wrote to do this. It works on the WKWebView's configuration object. It also assumes that there is only one context menu available at a time:
class GlobalScriptMessageHandler: NSObject, WKScriptMessageHandler {
public private(set) static var instance = GlobalScriptMessageHandler()
public private(set) var contextMenu_nodeName: String?
public private(set) var contextMenu_nodeId: String?
public private(set) var contextMenu_hrefNodeName: String?
public private(set) var contextMenu_hrefNodeId: String?
public private(set) var contextMenu_href: String?
static private var WHOLE_PAGE_SCRIPT = """
window.oncontextmenu = (event) => {
var target = event.target
var href = target.href
var parentElement = target
while (href == null && parentElement.parentElement != null) {
parentElement = parentElement.parentElement
href = parentElement.href
}
if (href == null) {
parentElement = null;
}
window.webkit.messageHandlers.oncontextmenu.postMessage({
nodeName: target.nodeName,
id: target.id,
hrefNodeName: parentElement?.nodeName,
hrefId: parentElement?.id,
href
});
}
"""
private override init() {
super.init()
}
public func ensureHandles(configuration: WKWebViewConfiguration) {
var alreadyHandling = false
for userScript in configuration.userContentController.userScripts {
if userScript.source == GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT {
alreadyHandling = true
}
}
if !alreadyHandling {
let userContentController = configuration.userContentController
userContentController.add(self, name: "oncontextmenu")
let userScript = WKUserScript(source: GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT, injectionTime: .atDocumentStart, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let body = message.body as? NSDictionary {
contextMenu_nodeName = body["nodeName"] as? String
contextMenu_nodeId = body["id"] as? String
contextMenu_hrefNodeName = body["hrefNodeName"] as? String
contextMenu_hrefNodeId = body["hrefId"] as? String
contextMenu_href = body["href"] as? String
}
}
Next, to enable this in your WKWebView, you must subclass it and call GlobalScriptMessageHandler.instance.ensureHandles in your constructor:
class WebView: WKWebView {
public var webViewDelegate: WebViewDelegate?
init() {
super.init(frame: CGRect(), configuration: WKWebViewConfiguration())
GlobalScriptMessageHandler.instance.ensureHandles(configuration: self.configuration)
}
Finally, (as other answers have pointed out,) you override the context menu handler. In this case I changed the action in target for the "Open Link" menu item. You can change them as you see fit:
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for index in 0...(menu.items.count - 1) {
let menuItem = menu.items[index]
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierOpenLink" {
menuItem.action = #selector(openLink(_:))
menuItem.target = self
And then, in your method to handle the menu item, use GlobalScriptMessageHandler.instance.contextMenu_href to get the URL that the user right-clicked:
#objc func openLink(_ sender: AnyObject) {
if let url = GlobalScriptMessageHandler.instance.contextMenu_href {
let url = URL(string: url)!
self.load(URLRequest(url: url))
}
}
Following the answers already given I was able to modify the menu and also found a way get the URL that was selected by the user. I suppose this approach can also be used to get an image or any other similar content selected, and I'm hoping this can help other folks.
This is written using Swift 5
This approach consists on performing the action from the menu item "Copy Link", so that the URL gets copied into the paste board, then retrieving the URL from the paste board to use it on a new menu item.
Note: Retrieving the URL from the pasteboard needs to be called on an async closure, allowing time for the URL to first be copied into it.
final class WebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with: NSEvent) {
menu.items.first { $0.identifier?.rawValue == "WKMenuItemIdentifierCopyLink" }.map {
guard let action = $0.action else { return }
NSApp.sendAction(action, to: $0.target, from: $0)
DispatchQueue.main.async { [weak self] in
let newTab = NSMenuItem(title: "Open Link in New Tab", action: #selector(self?.openInNewTab), keyEquivalent: "")
newTab.target = self
newTab.representedObject = NSPasteboard.general.string(forType: .string)
menu.items.append(newTab)
}
}
}
#objc private func openInNewTab(_ item: NSMenuItem) {
print(item.representedObject as? String)
}
}