Add a contextual menu item in xcode (plugin) - xcode

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

Related

Document-based app where the top-level document represents the window, not tab

When I use Xcode 12 to create a "Document App", the app template that is generated is one where the CoreData-backed "Document" represents the current tab, as seen below:
So basically if I hit cmd-S, the semantics are the to save the content of that one currently-active tab.
However, what if I wanted the "Document" to represent all the tabs in that window? Given that these default window tabs are sort of baked in, is Cocoa flexible enough to fit my design criteria?
The sample you've posted contains multiple documents (Untitled, Untitled 2, ..) inside a single window. Each of these tabs is a separate document with the tabbed interface handled transparently by macOS.
If you'd like to use tabs inside a single document - like e.g. sheets in a Numbers document - you'd have to implement that functionality on your own.
When a window tab is moved, the tabGroup property of the window changes. The tabGroup property is observable but this is not documented. Window controllers can be moved to another document with addWindowController. I don't know how to prevent a new document from opening in a tab in another document. Accessing the tabGroup seems to work but it's a hack. Switching NSWindow.allowsAutomaticWindowTabbing off and on also seems to work. Here's my test app (Xcode app template, document based, XIB. Add a WindowController subclass and change the class of the file's owner in Document.xib.)
class Document: NSDocument {
var documentData: [String]? // String per window
func addWindowTab(_ data: String?) {
let windowController = WindowController(windowNibName: NSNib.Name("Document"))
if let text = data {
windowController.text = text
}
if let otherWindowController = windowControllers.first, // document has other window(s)
let tabgroup = otherWindowController.window?.tabGroup,
let newWindow = windowController.window {
tabgroup.addWindow(newWindow)
}
else {
_ = windowController.window?.tabGroup // access tabGroup to open new documents in a new window
windowController.showWindow(self)
}
addWindowController(windowController)
}
override func makeWindowControllers() {
if let data = documentData {
for windowData in data {
addWindowTab(windowData)
}
}
else {
addWindowTab(nil)
}
}
func windowControllerDidChangeTabGroup(_ windowController: NSWindowController) {
var destinationDocument: Document?
// check if the window is in a tabgroup with windows of another document
if let tabGroup = windowController.window?.tabGroup,
tabGroup.windows.count > 1 {
for otherWindow in tabGroup.windows {
if let otherWindowController = otherWindow.delegate as? WindowController,
otherWindowController.document !== self {
destinationDocument = otherWindowController.document as? Document
break;
}
}
}
// check if this document has other windows
else if windowControllers.count > 1 {
destinationDocument = Document()
NSDocumentController.shared.addDocument(destinationDocument!)
}
if let destinationDocument = destinationDocument,
destinationDocument !== self {
destinationDocument.addWindowController(windowController) // removes windowController from self
if windowControllers.count == 0 {
close()
}
}
}
}
class WindowController: NSWindowController {
var tabObservation: NSKeyValueObservation?
override func windowDidLoad() {
super.windowDidLoad()
tabObservation = window?.observe(\.tabGroup, options: []) { object, change in
if let document = self.document as? Document {
// accessing tabGroup can change tabGroup and cause recursion, schedule on runloop
DispatchQueue.main.async{
document.windowControllerDidChangeTabGroup(self)
}
}
}
}
override func newWindowForTab(_ sender: Any?) {
if let document = document as? Document {
document.addWindowTab(nil)
}
}
}

NSTouchBar - how to combine default text filed items with custom items

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
}

how to Load initial window controller from storyboard?

I have gone through many questoions but none of them snaswers my query.
I am trying to load initial window programmatically
Here is what I have done.
I have added main.swift as-
import Cocoa
private func runApplication(
application: NSApplication = NSApplication.sharedApplication(),
delegate: NSApplicationDelegate? = AppDelegate(),
bundle: NSBundle = NSBundle.mainBundle(),
nibName: String = "MainMenu",
var topLevelObjects: NSArray? = nil) {
setApplicationDelegate(application, delegate)
}
private func setApplicationDelegate(application: NSApplication, delegate: NSApplicationDelegate?) -> NSApplication {
if let delegate = delegate {
application.delegate = delegate
}
return application
}
runApplication()
Appdelegate.swift is-
import Cocoa
//#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var appControl:AppFlow?
func applicationDidFinishLaunching(aNotification: NSNotification) {
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
override init() {
//
self.appControl = AppFlow()
super.init()
}
}
And in AppFlow I am trying to load window controller from storyboard.-
import Cocoa
class AppFlow{
let initialStoryBoard:NSStoryboard?
override init() {
self.initialStoryBoard = NSStoryboard(name: "Main" , bundle : nil)
super.init()
var windowController = (self.initialStoryBoard?.instantiateControllerWithIdentifier("mainWindow")) as! NSWindowController
windowController.window?.makeKeyAndOrderFront(nil)
}
}
But I am not able to launch initial window controller and view controller. App starts and terminates automatically, no window is presented to user.
What I am doing wrong? Thanks for your help.
Here is what I did in order to load initial window from storyboard (as well as MainMenu) programmatically without attribute #NSApplicationMain and function NSApplicationMain(_, _)
File: AppConfig.swift (Swift 4)
struct AppConfig {
static var applicationClass: NSApplication.Type {
guard let principalClassName = Bundle.main.infoDictionary?["NSPrincipalClass"] as? String else {
fatalError("Seems like `NSPrincipalClass` is missed in `Info.plist` file.")
}
guard let principalClass = NSClassFromString(principalClassName) as? NSApplication.Type else {
fatalError("Unable to create `NSApplication` class for `\(principalClassName)`")
}
return principalClass
}
static var mainStoryboard: NSStoryboard {
guard let mainStoryboardName = Bundle.main.infoDictionary?["NSMainStoryboardFile"] as? String else {
fatalError("Seems like `NSMainStoryboardFile` is missed in `Info.plist` file.")
}
let storyboard = NSStoryboard(name: NSStoryboard.Name(mainStoryboardName), bundle: Bundle.main)
return storyboard
}
static var mainMenu: NSNib {
guard let nib = NSNib(nibNamed: NSNib.Name("MainMenu"), bundle: Bundle.main) else {
fatalError("Resource `MainMenu.xib` is not found in the bundle `\(Bundle.main.bundlePath)`")
}
return nib
}
static var mainWindowController: NSWindowController {
guard let wc = mainStoryboard.instantiateInitialController() as? NSWindowController else {
fatalError("Initial controller is not `NSWindowController` in storyboard `\(mainStoryboard)`")
}
return wc
}
}
File main.swift (Swift 4)
// Making NSApplication instance from `NSPrincipalClass` defined in `Info.plist`
let app = AppConfig.applicationClass.shared
// Configuring application as a regular (appearing in Dock and possibly having UI)
app.setActivationPolicy(.regular)
// Loading application menu from `MainMenu.xib` file.
// This will also assign property `NSApplication.mainMenu`.
AppConfig.mainMenu.instantiate(withOwner: app, topLevelObjects: nil)
// Loading initial window controller from `NSMainStoryboardFile` defined in `Info.plist`.
// Initial window accessible via property NSWindowController.window
let windowController = AppConfig.mainWindowController
windowController.window?.makeKeyAndOrderFront(nil)
app.activate(ignoringOtherApps: true)
app.run()
Note regarding MainMenu.xib file:
Xcode application template creates storyboard with Application Scene which contains Main Menu. At the moment seems there is no way programmatically load Main Menu from Application Scene. But there is Xcode file template Main Menu, which creates MainMenu.xib file, which we can load programmatically.
This is not how you start (and maintain) an application's main run loop. See #NSApplicationMain. This causes the main run loop to be set up and run until it's terminated. There's no need for a main.swift file any longer, as you can just put this into your app delegate's file directly.
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet weak var window: NSWindow!
func applicationDidFinishLaunching(aNotification: NSNotification) {
// Insert code here to initialize your application
}
}
Xcode's new application project template does this for you.

How can the context menu in WKWebView on the Mac be modified or overridden?

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

NSSharingService Sharing Submenu

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

Resources