How to enable Copy menu item

I have a document-based Cocoa MacOS Swift app, and I want to be able to Copy the document to the Pasteboard. The document is just represented by a document window: it's not text and there's nothing to 'select'.


I've overridden the document's copy function, but the Copy menu item is still greyed out. I've ticked "Enabled" in the Attributes panel. How do I enable it and get the document's as its 'selected' object of operation?


Thanks

Accepted Reply

How is your copy function declared? Note that the function you want is copy(_:), not copy(). The first of those is the clipboard copy action method (and should be prefixed with @IBAction). The second is NSObject's default object copy method, definitely not what you want.


Also, NSDocument doesn't have a copy(_:) method, so if you're actually providing an override (as you said in your OP), you're definitely using the wrong function declaration.

Replies

Did you deselect "AutoEnable menu items" in the parent menu ?

Or do it by code.

Ah: Found it. OK, thanks. Though it would be nice to just have that menu item enabled and not everything else.


The menu item now works, but it's not actually calling the function.


Baby steps....

So, was it at auto enable issue ?


If so, thanks to close the thread on the correct answer, … and open a new one for the next issue.


Good luck.

Well, it's an answer, but it's a bit of a "hammer to crack a nut" 😀 (Sorry!) I'll wait a bit to see to see if there are any other suggestions.


I appreciate that you've replied to a lot of my questions (often the only reply!), always with useful info, if not a definitive answer.

So, let's go on.


From your original post, could you tell what remains unsolved ? And please elaborate a bit more on those points.


I have a document-based Cocoa MacOS Swift app, and I want to be able to Copy the document to the Pasteboard. The document is just represented by a document window: it's not text and there's nothing to 'select'.


I've overridden the document's copy function, but the Copy menu item is still greyed out. I've ticked "Enabled" in the Attributes panel. How do I enable it and get the document's as its 'selected' object of operation?

I have two (OK, three) problems: Firstly, the Copy menu is not enabled, despite my overriding the copy method, and linking the Copy menu with the function in FirstResponder.


Secondly, even when I enable the entire Edit menu, the copy function still doesn't seem to be called -- Log messages in the function are not logged.


The final problem is getting the data from the read function into the copy function, of which full details can be found here:

https://stackoverflow.com/questions/57955487/how-to-get-data-from-nsdocuments-read-method

hi,


i'm not sure we have seen enough code yet, but 2 quick things:


1) did you implement validateMenuItem in the NSDocument ? a simple implementation would be this (and then expand it for other menu items as needed).


override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {

  guard let action = menuItem.action else {
       return super.validateMenuItem(menuItem)
  }

  switch action {

  case #selector(copy):
       return true

  default:
       return super.validateMenuItem(menuItem)
  }
}


2) check in IB that the sent action associated with the Copy menu item going to the First Responder's copy method.


hope that helps,

DMG

Thanks. No, I hadn't added validateMenuItem. I've pasted your code in, but still Copy remains disabled. Alternatively, if I turn off Auto-Enable on the whole Edit menu, I can invoke the Copy item, but I'm still getting no logging, nor indeed actual copying.


Yes, the menu item is associated with the FR action.

The essential bit of Document/swift is:


override func read(from data: Data, ofType typeName: String) throws {

self.theMIDIPlayer = try AVMIDIPlayer.init(data: data, soundBankURL: nil)

if self.theMIDIPlayer == nil {

throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)

}

}


The app loads in a MIDI file, creates a small window with playing controls. So ViewController handles functions to the AVMIDIPlayer object, based on the controls. There's very little to it, and it works well. But I want to add the ability to copy the entire MIDI file to the clipboard.

How is your copy function declared? Note that the function you want is copy(_:), not copy(). The first of those is the clipboard copy action method (and should be prefixed with @IBAction). The second is NSObject's default object copy method, definitely not what you want.


Also, NSDocument doesn't have a copy(_:) method, so if you're actually providing an override (as you said in your OP), you're definitely using the wrong function declaration.

I thought I'd have the idea to change the name of the copy function, so it wasn't overriding anything. So I changed 'override func copy' to 'func myCopy', but it doesn't show up in FirstResponder, and I can't CTRL-link to it directl in Document.swift.

1 & 2) Take whatever you have done for copy so far and get rid of it. You seem to be doing it all wrong if you have the entire Edit menu enabled. The interaction between menu items, apps, documents, and first responders is very subtle and not documented in any way.


You don't want to override validateMenuItem for this. That is for something else.


You also don't want to change anything in Interface Builder related to actions on Edit menu items. Most system menu items, especially anything in the Edit menu, is dynamically created. What you see in Interface Builder is more of a suggestion of what the running app might see in some hypothetical world.


Normally, one doesn't "copy" raw data like this. Various user interface elements that might provide meaningful data will offer their data to the clipboard and, depending on focus state, will be the first responder and will activate the copy menu item. That's how the copy menu item normally gets activated. You can certainly copy raw data, but it isn't normally done and that's why you are having difficulty. The solution is pretty simple, just provide a "copy" implementation in your document class. That's all you need to do. However, it does need to look exactly like this:


- (IBAction) copy: (id) sender

{

NSPasteboard * pasteboard = [NSPasteboard generalPasteboard];


[pasteboard clearContents];


[pasteboard setString: @"This is a test" forType: NSPasteboardTypeString];

}


That is an example implementation in Objective-C, of course. You will have to re-write it in Swift and jump through all kinds of Swift hoops. You're on your own for that.


3) You normally wouldn't copy to the pasteboard in "read". You save some reference to your data and then, when the user requests a copy, you do the copy there. However, you can't just stuff data onto the pasteboard. The pasteboard is designed to transfer data from one app to another. You can define your own data type if you are strictly copying data from one place in your own app to another place. If you know that some other app accepts a very specific type of data, you might be able to just shove in that data type. I don't know.


But, in general, you just have to find the most general representation of your data and put that on the pasteboard. For MIDI data, I assume that would be NSPasteboardTypeSound (via an NSSound object). I don't know anything about that class. Supposedly it accepts anything that CoreAudio or Quicktime can handle. I assume they can handle MIDI data, so that should be what you can use. I expect that it would be copying something that really is low-level MIDI data, rather than a conversion. You can inspect it with the Apple Clipboard Viewer app.

Now we're getting somwhere! I think you're right: I'm mixing up my copy functions.


The following code works:


extension NSPasteboard.PasteboardType {
    static let typeMidi = NSPasteboard.PasteboardType(rawValue: "public.midi-audio")
}

@IBAction func copy(_: Any) {
     let pboard = NSPasteboard.general
     pboard.clearContents()
     pboard.setData(myData, forType: .typeMidi)
{

    override func read(from data: Data, ofType typeName: String) throws {
        self.theMIDIPlayer = try AVMIDIPlayer.init(data: data, soundBankURL: nil)
        self.myData = data
        if self.theMIDIPlayer == nil {
             throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil)
    }
    }


And what is more, my data imports into commercial apps with MIDI pasteboard capabilities!
Many thanks to one and all. Yet again, it's the tiniest of differences between "not working" and "working perfectly", to paraphrase Spinal Tap.

Thanks, John. I think I'm there. Not helped by using the wrong copy method. I wasn't planning on doing the copying in read(), but making the data in read() available to the copy function. The more or less complete code is above.


MIDI is not a sound, but just a stream of hex digits, which are instruction codes followed by parameters. There are commercial apps that copy data as Pasteboardtype "public.midi-audio" to the clipboard, and I can now copy and paste to them!

You can always do something like:


NSData * data = [@"This is a test" dataUsingEncoding: NSUTF8StringEncoding];

[pasteboard setData: data forType: @"public.midi-audio"];


That will definitely put data on the pasteboard. If I had actual MIDI data, it might work. Of course Swift...