Subclass NSView in Xojo + pagefooter

I use a NSTextView (which is a subclass of NSView) to print. I proceed like this (in short) :

Dim pNSPrintInfo as Ptr = sharedPrintInfo (Cocoa.ClassFromString(“NSPrintInfo”))
Dim pNSMutableDictionary as Ptr = MutableCopyPtr (dictionary ( pNSPrintInfo))

setValue (pNSMutableDictionary, myNSValue, “myKey”)
’ … other Keys / Values
setValueString (pNSMutableDictionary, “YES”, “NSPrintHeaderAndFooter”)

Dim pNSPrintInfo2 as Ptr = initWithDictionary(cocoa.allocFromString (“NSPrintInfo”), pNSMutableDictionary)
Dim pNSPrintOperation as Ptr = printOperationWithViewInfo (Cocoa.ClassFromString (“NSPrintOperation”), pNSTextView, pNSPrintInfo2 )
runOperation pNSPrintOperation

To make a custom pagefooter, one need to subclass the NSView and override the method “beginDocument”.
Doing so, it should be possible to create a custom NSAttributedString or a drawing to be used as a pagefooter.
I’ve seen the CocoaDelegate in macoslib. I have no experience in subclassing Cocoa objects in Xojo, and I don’t know how to use it.
Thanks in advance

You have to do the subclassing using the obj-c runtime. Take a look here for how I subclassed UIView (the iOS equivalent of NSView). The most relevant methods are the shared methods that begin with impl, the the TargetClass shared method and the BuildTargetClass method of UIKit. These do the subclassing of the object and event handling. You should be able to do a similar thing for the beginDocument event of NSView just by changing the class which is subclassed. Sorry this isn’t a great answer, but hopefully it will get you on your way. I can try to post a more complete answer tomorrow if no one else has helped you before then.

You have to subclass NSView in the Objective-C Runtime. You do that by using

objc_allocateClassPair class_addMethod // override beginDocument and set the implementation to a shared method of a class in Xojo objc_registerClassPair
from the Objective-C Runtime API (Objective-C Runtime Reference).

I’m not on my computer today, so can’t copy and post code. You’ll find examples in macoslib. Scroll down to MakeDelegateClass.

OK, i think I understand : Subclassing in Objective-C and overriding in Xojo

I gave a look at Cocoa.NSImageView.MakeObjCClass, it is a sub-sub-class of NSView.
But in this example, class_addMethod is not used since it overrides a Cocoa method to another Cocoa method.

dim newClassId as Ptr = objc_allocateClassPair(NSClassFromString(superclassName), className, 0) objc_registerClassPair (newClassId) Call AddInstanceMethod(newClassId, "mouseDown:", AddressOf impl_MouseDown, "v@:@")

Just adding impl_MouseDown as a shared method in Xojo is enough to override newClassId.MouseDown event ?

What are the MethodTypeEncoding characters for beginDocument method which has no arguments :
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/#//apple_ref/occ/instm/NSView/beginDocument

Thank you Jason for you sample code.

“v@:”

encoding reference
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100

Follow AddInstanceMethod and it uses a declare to class_addMethod

Also, the docs for beginDocument say

See these for how to do that
https://forum.xojo.com/11168-obj-c-declares-how-to-call-super/0
https://forum.xojo.com/24163-apple-docs-help-needed-objc-msgsendsuper-os-x-ios/0

It seems easy to create a subclass, but I can’t alloc a new instance :

// New Class "NewTextView" dim newClassId as Ptr = objc_allocateClassPair(Cocoa.allocFromString ("NSTextView"), "NewTextView", 0) If newClassId = nil then return bl = class_addMethod(newClassId, NSSelectorFromString("beginDocument:"), AddressOf impl_beginDocument, "v@:") objc_registerClassPair (newClassId) dim pNewTextView as Ptr = Cocoa.alloc(newClassId)

The “alloc” makes the app hang, I don’t see why. The new newClassId has a correct Ptr and is not = nil.
I tried many times, and even when using “NSObject” instead of “NSTextView” as the superclass, I am not able to allocate an instance of the new class.
But I can get a ClassRef : NSClassFromString (“NewTextView”) returns a valid class pointer (= newClassId).
It means that the new class has been created though.

You don’t want to allocate this class, it should be a class reference (or some specific term I’m forgetting).

Try Cocoa.NSClassFromString(“NSTextView”) instead.

Also, I believe NSSelectorFromString(“beginDocument:”) shouldn’t have the colon.

And don’t forget to init before using :slight_smile:

[code]// Get class “NewTextView”
Dim cls As Ptr = objc_lookUpClass(“NewTextView”)

// If it doesn’t exist, register it
If cls = Nil Then
cls = objc_allocateClassPair(NSClassFromString(“NSTextView”), “NewTextView”, 0)
If Not class_addMethod(cls, NSSelectorFromString(“beginDocument”), AddressOf beginDocument, “v@:”) Then
// Could not register new method
End
objc_registerClassPair(cls)
End

// Instantiate
Dim pNewTextView As Ptr = init(alloc(cls))[/code]
This is without macoslib, as I’ve never used it.

You know what ? IT WORKS !?
Thank you for your help, really.

@Will : Cocoa.NSClassFromString(“NSTextView”) is the same pointer as newClassId newly created?
Cocoa.allocFromString (“NewTextView”) = Cocoa.alloc (newClassId)

[code] Dim newClassId As Ptr = objc_lookUpClass(“NewTextView”)
If newClassId = Nil Then
newClassId = objc_allocateClassPair(NSClassFromString(“NSTextView”), “NewTextView”, 0)
If Not class_addMethod(newClassId, NSSelectorFromString(“beginDocument”), AddressOf impl_beginDocument, “v@:”) Then
// Could not register new method
End if
objc_registerClassPair(newClassId)
End If

// Instantiate
Dim pNewTextView As Ptr = initWithFrame (Cocoa.alloc(newClassId), NSRect1)[/code]

The impl method :

[code]Declare Function objc_msgSendSuper lib CocoaLib (byref sup As objc_super, sel As Ptr) As Ptr
Declare Function sel_registerName Lib CocoaLib (str As CString) As Ptr

Dim sup As objc_super
sup.receiver = id
sup.pClass = Cocoa.ClassFromString(“NSTextView”)

’ -------- Context
declare function context lib CocoaLib selector “context” ( obj_id as Ptr) as Ptr
dim gPtr as Ptr = context (pNSPrintOperationRef)
if gPtr <> nil then
’ // write or draw pagefooter in the context
end if

Dim sel as Ptr = sel_registerName(“beginDocument”)
Dim pMethod as Ptr = objc_msgSendSuper(sup, sel)[/code]

Remarks :
The overrided “beginDocument” method fires 7 times after runOperation, but gPtr and pMethod are nil all the times except for the last call. That is why I declared objc_msgSendSuper as a function rather than a sub to be able to check if it succeeds or not.

That 7th firing (the only working one) occurs after displaying the PrintPanel and the preview page.
It means even if I draw in the context, i could not see it in the preview.

I am not sure of encodings, I’d like to override 2 other methods :
“pagefooter” and “beginPageInRect:atPlacement:” from NSView class.
I guess my encodings are wrong.

[code] If Not class_addMethod(newClassId, NSSelectorFromString(“pagefooter”), AddressOf impl_pagefooter, “v@:@:”) Then
// failure pagefooter
End if

If Not class_addMethod(newClassId, NSSelectorFromString("beginPageInRect:atPlacement:"), AddressOf impl_beginPage, "v@:@:@:") Then
  // failure beginPageInRect
End if[/code]

And in superclass methods implementation, it must be wrong since nothing is printed :

[code]Shared Sub impl_beginPage(id as Ptr, aRect as NSRect, location as NSPoint)
’ superclass method impl
Declare Function objc_msgSendSuper lib CocoaLib (byref sup As objc_super, sel As Ptr) As Ptr
Declare Function sel_registerName Lib CocoaLib (str As CString) As Ptr

Dim sup As objc_super
sup.receiver = id
sup.pClass = Cocoa.ClassFromString(“NSTextView”)

Dim sel as Ptr = sel_registerName(“beginPageInRect:atPlacement:”)
Dim pMethod as Ptr = objc_msgSendSuper(sup, sel)
End Sub[/code]

Private Shared Sub impl_pagefooter(id as Ptr) ' don't even fire ! End Sub

“beginPageInRect:atPlacement:”
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/#//apple_ref/occ/instm/NSView/beginPageInRect:atPlacement:

“pagefooter”
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/#//apple_ref/occ/instp/NSView/pageFooter

[quote=208159:@Patrick Morel]I am not sure of encodings, I’d like to override 2 other methods :
“pagefooter” and “beginPageInRect:atPlacement:” from NSView class.
I guess my encodings are wrong.

[code] If Not class_addMethod(newClassId, NSSelectorFromString(“pagefooter”), AddressOf impl_pagefooter, “v@:@:”) Then
// failure pagefooter
End if

If Not class_addMethod(newClassId, NSSelectorFromString("beginPageInRect:atPlacement:"), AddressOf impl_beginPage, "v@:@:@:") Then
  // failure beginPageInRect
End if[/code]

And in superclass methods implementation, it must be wrong since nothing is printed :

[code]Shared Sub impl_beginPage(id as Ptr, aRect as NSRect, location as NSPoint)
’ superclass method impl
Declare Function objc_msgSendSuper lib CocoaLib (byref sup As objc_super, sel As Ptr) As Ptr
Declare Function sel_registerName Lib CocoaLib (str As CString) As Ptr

Dim sup As objc_super
sup.receiver = id
sup.pClass = Cocoa.ClassFromString(“NSTextView”)

Dim sel as Ptr = sel_registerName(“beginPageInRect:atPlacement:”)
Dim pMethod as Ptr = objc_msgSendSuper(sup, sel)
End Sub[/code]

Private Shared Sub impl_pagefooter(id as Ptr) ' don't even fire ! End Sub

“beginPageInRect:atPlacement:”
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/#//apple_ref/occ/instm/NSView/beginPageInRect:atPlacement:

“pagefooter”
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/#//apple_ref/occ/instp/NSView/pageFooter[/quote]

Try “v@:” for pageFooter and “v@:{NSRect=ffff}{NSPoint=ff}” for beginPage. Second one might not be right, but I think I will do the trick. Explanation of how to make the type encodings is here:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html

AFAIK the encodings can be an empty string when the added method is overriding a method in the superclass.

I always use “id As Ptr, sel As Ptr” for the first 2 parameters. id is the instance being called upon and sel is the selector being called. Both are required to match the dynamic-dispatch signature of ObjC methods.

[quote=208127:@Patrick Morel]@Will : Cocoa.NSClassFromString(“NSTextView”) is the same pointer as newClassId newly created?
Cocoa.allocFromString (“NewTextView”) = Cocoa.alloc (newClassId)[/quote]
For my own pendantic pride I was talking about in this line where the super parameter (NSTextView) is being allocated

dim newClassId as Ptr = objc_allocateClassPair(Cocoa.allocFromString ("NSTextView"), ...

That should be a class reference not an instance. Eli fixed it too, as have you :slight_smile:

I have made a lot of experiments (I really mean a lot !), here are my 3 conclusions :

[quote]Eli is right : “class_addMethod” does not need any encodings. Whatever you specify an empty string or anything, it has no effect

Will is right : we MUST specify sel argument in our Implementation method

We must specify variable arguments in “objc_msgSendSuper”[/quote]

Example with beginPageInRect which needs 2 arguments NSRect and NSPoint :

If Not class_addMethod(newClassId, NSSelectorFromString("beginPageInRect:atPlacement:"), AddressOf impl_beginPage, "") Then msgbox "failure beginPage" End if

[code]Private Shared Sub impl_beginPage(id as Ptr, sel as Ptr, aRect as NSRect, location as NSPoint)
Declare Function objc_msgSendSuper lib CocoaLib (byref sup As objc_super, sel As Ptr, aRect as NSRect, location as NSPoint) As Ptr

Dim sup As objc_super
sup.receiver = id
sup.pClass = Cocoa.ClassFromString(“NSTextView”)

Dim pMethod as Ptr = objc_msgSendSuper(sup, sel, aRect, location)
End Sub[/code]

Particular case of pagefooter method : I am failing using it.
The implementation seems to go well, no problem with that :

If Not class_addMethod(newClassId, NSSelectorFromString("pagefooter"), AddressOf impl_pagefooter, "v@:") Then msgbox "failure pagefooter" End if

Private Shared Sub pagefooter(id as Ptr, sel as Ptr) msgbox "pagefooter" End Sub

OR // supposed to return a NSAttributedString

Private Shared Function pagefooter(id as Ptr, sel as Ptr) As Ptr msgbox "pagefooter" End Sub

It never fires.
As said in sdk, “Footers are generated only if the user defaults contain the key NSPrintHeaderAndFooter with the value YES”. I am sure it has successfully been made since the default pagefooter is printed (just the string “Page NPage on NbPages”).

setValueString (pNSMutableDictionary, "YES", "NSPrintHeaderAndFooter")

It seems that “pagefooter” selector is not valid ?

https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/#//apple_ref/occ/instp/NSView/pageFooter

Selectors are case sensitive so you need to make sure you use the same capitalization presented in the docs.

Bingo Jason. “pageFooter” instead of “pagefooter” does the trick. Now impl_ pageFooter fires.

But how to declare the impl method to retrieve NSAttributedString ?

Private Shared Sub impl_pageFooter(id as Ptr, sel as Ptr, pAttr as Ptr) Private Shared Function impl_pageFooter(id as Ptr, sel as Ptr) As Ptr

These 2 declarations don’t work.
The first one returns a kind of pointer in pAttr, but it seems fake and is unusable.
The second one : i don’t know how to retrieve the return value

OK, please forget my last post.
Here is the solution :

[code]Private Shared Function impl_pageFooter(id as Ptr, sel as Ptr) As Ptr

// Creation of a new very simple NSMutableAttributedString
declare function initWithString lib CocoaLib selector “initWithString:” (obj_id as Ptr, aString as Ptr) as Ptr
declare function stringWithString lib CocoaLib selector “stringWithString:” ( cls as Ptr, value as CFStringRef ) as Ptr

Dim s as string = “This is my Custom footer”
Dim pNSString As Ptr = stringWithString (Cocoa.NSClassFromString(“NSString”), s)
Dim pNSMutableAttributedString as Ptr = initWithString ( Cocoa.allocFromString(“NSMutableAttributedString”), pNSString )

return pNSMutableAttributedString
End Function[/code]

My Custom pagefooter is correctly printed at the bottom of the page.

I think the subject is completely solved.
Thank you very much, I am sure I would not have succeeded without your help

Hey Patrick,

are you willing to share your experience post the code/project ?