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
}
Related
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)
}
}
}
In my macOS application, I'm trying to replicate the Photos.app implementation of NSSegmentedControl in NSToolbar to control an NSTabViewController. For reference, here's what that looks like:
So, my approach was as follows:
Hide the default NSTabView header using the Interface Builder
Programmatically add an NSToolbar
Insert NSSegmentedControl as an NSToolbarItem.
Use a #selector to listen for changes to NSSegmentedControl.
Here's the current implementation:
class WindowController: NSWindowController, NSToolbarDelegate {
// MARK: - Identifiers
let mainToolbarIdentifier = NSToolbar.Identifier("MAIN_TOOLBAR")
let segmentedControlIdentifier = NSToolbarItem.Identifier("MAIN_TABBAR")
// MARK: - Properties
var tabBar: NSSegmentedControl? = NSSegmentedControl(labels: ["One", "Two"], trackingMode: NSSegmentedControl.SwitchTracking.selectOne, target: self, action: #selector(didSwitchTabs))
var toolbar: NSToolbar?
var tabBarController: NSTabViewController?
// MARK: - Life Cycle
override func windowDidLoad() {
super.windowDidLoad()
self.toolbar = NSToolbar(identifier: mainToolbarIdentifier)
self.toolbar?.allowsUserCustomization = false
self.toolbar?.delegate = self
self.tabBar?.setSelected(true, forSegment: 0)
self.tabBarController = self.window?.contentViewController as? NSTabViewController
self.tabBarController?.selectedTabViewItemIndex = 0
self.window?.toolbar = self.toolbar
}
// MARK: - NSToolbarDelegate
public func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? {
var toolbarItem: NSToolbarItem
switch itemIdentifier {
case segmentedControlIdentifier:
toolbarItem = NSToolbarItem(itemIdentifier: segmentedControlIdentifier)
toolbarItem.view = self.tabBar
case NSToolbarItem.Identifier.flexibleSpace:
toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
default:
fatalError()
}
return toolbarItem
}
public func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
}
public func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
return [NSToolbarItem.Identifier.flexibleSpace, segmentedControlIdentifier, NSToolbarItem.Identifier.flexibleSpace]
}
// MARK: - Selectors
#objc func didSwitchTabs(sender: Any) {
let segmentedControl = sender as! NSSegmentedControl
if (segmentedControl.selectedSegment == 0) {
self.tabBarController?.selectedTabViewItemIndex = 0
} else if (segmentedControl.selectedSegment == 1) {
self.tabBarController?.selectedTabViewItemIndex = 1
}
}
}
And, here it is in action:
Now, I am new to macOS development and this feels like it's a very complicated and convoluted way of solving this problem. Is there an easier way I could achieve the same thing ? Perhaps somehow in Interface Builder ? What could be done to improve here ? What have I done wrong ?
Thanks for your time.
For anybody implementing NSSegmentedControl on the toolbar and it did not trigger IBAction, I got the same problem and pay my half-day to resolve this.
The problem is I connect my segmented with the NSWindowController class.
To fix this, create a subclass of NSWindow, set that class to base class of window on your storyboard, then create #IBOutlet #IBAction link to NSWindow. Remember, link it with NSWindowController will not work.
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 trying to change Cancel button color in UISearchBar implemented with UISearchController (iOS 8 and greater). This is a code I use:
if self.resultSearchController.active {
for subView in self.resultSearchController.searchBar.subviews {
for subsubView in subView.subviews {
if subsubView.isKindOfClass(UIButton) {
subsubView.tintColor = UIColor.whiteColor()
}
}
}
}
If I paste it in viewDidLoad, it doesn't work, cause I think Cancel button initialize only when SearchController becomes Active.
If I paste code in viewDidLayoutSubviews everything work great, but I'm not sure its a correct way.
So, where I should put this code in TableViewController?
Also, I don't understand, how I can receive notification in my TableViewController that SearchController becomes inactive. In other words where I should put code like this:
if self.resultSearchController.active == false {
//Do something
}
First you should insert delegate methods :-
class HomeViewController: UIViewController,UISearchResultsUpdating, UISearchBarDelegate {
var searchController: UISearchController!
override func viewDidLoad() {
super.viewDidLoad()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search here..."
searchController.searchBar.delegate = self
searchController.searchBar.sizeToFit()
searchController.hidesNavigationBarDuringPresentation = true
tableView.tableHeaderView = searchController.searchBar
searchController.searchBar.tintColor = UIColor.whiteColor()
}
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
}
func searchBarCancelButtonClicked(searchBar: UISearchBar) {
}
func searchBarSearchButtonClicked(searchBar: UISearchBar) {
}
func updateSearchResultsForSearchController(searchController: UISearchController) {
}
}
then used delegate methods and change cancel button colors and thing what you want
You can try this in AppDelegate's didFinishLaunchWithOptions:.
UIBarButtonItem.appearanceWhenContainedInInstancesOfClasses([UISearchBar.self]).tintColor = UIColor.whiteColor()
PS: This is a generic method and would affect UIBarButtonItem in UISearchBar across app.
Swift 4.2, 4.0+ An answer is added here for a custom search bar that can be customized as below,
You can check the usage of SearchBar class.
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)
}
}