8

WebView supports, through the WebEditingDelegate, a mechanism for the delegate to implement custom behavior for a variety of actions the WebView (or the private WebHTMLView) receives. When an action such as:

-(void)changeAttributes:(id)sender

is received in WebHTMLView, it is passed through to the delegate method:

-(BOOL)webView:(WebView *)webView doCommandBySelector:(SEL)command

Unfortunately, the mechanism does not provide for conveyance of the "sender" in the original action method.

For the vast majority of actions, the sender is unimportant, but for changeAttributes, and changeFont, for example, the contract requires that "sender" be called by the recipient in order to e.g. convertAttributes: or convertFont:.

For the changeFont case, it turns out that calling [[NSFontManager sharedFontManager] convertFont:] is sufficient, as coincidentally this is what the sender is.

In the changeAttributes case, in particular when strikethrough is changed, the sender may be a private class "NSFontEffectsBox" which presumably corresponds to the subsection of the font panel that is responsible for changing strikethrough/etc settings.

Unfortunately, calling [[NSFontManager sharedFontManager] convertAttributes:] does NOT obtain the expected attribute changes. This leaves a delegate who is interested in implementing this method meaningfully in a bit of a conundrum:

  1. WebKit does not convey the sender, so the delegate can't make the contractual [sender convertAttributes:] call.

  2. The changeAttributes: call is sent to a private WebKit class, WebHTMLView, which cannot be subclassed to, e.g., customize the behavior of changeAttributes:.

  3. The sender for the changeAttributes: call, NSFontEffectsBox, is a private class and cannot be accessed e.g. as [NSFontEffectsBox sharedFontEffectsBox].

In short: there appears to be no way for a developer to meaningfully override the behavior of changeAttributes: for a WebView.

Any ideas?

danielpunkass
  • 17,527
  • 4
  • 24
  • 38

2 Answers2

4

This is an evil one. A suitably evil pair of actions (neither of them particularly clean or ideal) would be:

  1. Do some inline assembler to look back up the stack to read the sender argument from the caller's stack (or the caller's caller, as the case should be). This of course assumes that the sender is placed on the stack and not in %eax when the call to WebHTMLView was made. That will always apply to PowerPC code however, so it's likely a non-starter there.

  2. Put a category on WebHTMLView with a method named something like __my_evil_hacky_nasty_ugly_changeAttributes_thing: and at runtime use method_exchangeImplementations() from the ObjC runtime to swap your category's implementation with theirs. Your method becomes changeAttributes: and theirs becomes __my_evil_hacky_nasty_ugly_changeAttributes_thing:, which you can then call to pass on the original call.

As I said, neither is particularly ideal, but the second has the advantage of full runtime support (i.e. the runtime is explicitly designed to let you do this), and since you're looking up the class and methods at runtime, it's failure-tolerant. Failure in this case gets you back to square one however.

Really it needs a bug logged against WebKit to have them pass on the sender to make it meaningful at all. Your overridden version could potentially look for a method -(BOOL)webView:(WebView*)webView doCommandBySelector:(SEL)selector sender:(id)sender and call that if found, otherwise just call through to the original method. This is what Apple's code should be doing, TBH.

Jim Dovey
  • 11,166
  • 2
  • 33
  • 40
  • Thanks for the response. I was hoping to avoid the swizzling approach, since I am not sure what risk that puts me in as far as Apple's Mac App Store prohibitions on "private API". – danielpunkass Dec 10 '10 at 17:30
  • Should be no risk on swizzling. I've used it in iOS apps with no problems. They don't even run `strings` on the binaries, so NSClassFromString(@"WebHTMLView") goes undetected. The ObjC runtime is all public too, so you're free to use anything in there-- I use it to swap out methods using `-performSelectorOnMainThread:` with `dispatch_async()` versions at runtime in a bunch of classes too, albeit only in my own classes. – Jim Dovey Dec 10 '10 at 17:33
  • FWIW I finally reported this bug: http://www.openradar.me/radar?id=4965931952373760 It took me this long because I let the underlying issue fall by the wayside. I think I will swizzle in the short term while hoping for a more lasting fix down the road. – danielpunkass Aug 12 '15 at 03:36
3

Have you looked at the source code?

WebHTMLView.mm

I don't see how -changeAttributes: is calling -webView:doCommandBySelector:, as within this class it's only called inside its own -doCommandBySelector: method.

- (void)changeAttributes:(id)sender
{
    [self _applyStyleToSelection:[self _styleForAttributeChange:sender] withUndoAction:EditActionChangeAttributes];
}


- (void)doCommandBySelector:(SEL)aSelector
{
…
    if (![[webView _editingDelegateForwarder] webView:webView doCommandBySelector:aSelector] && coreFrame) {
…
}

Also, why can't you subclass WebHTMLView? Is it because of the Mac App Store restrictions on API? Does WebKit count as private? I thought it was Open Source.

-Wil

Wil Shipley
  • 9,343
  • 35
  • 59
  • Wil, I don't know what version of the source that is. In the latest trunk version I am looking at, many of those action methods in WebHTMLView have a macro at the top, "COMMAND_PROLOGUE", which takes care of the dispatching of the message to the delegate. As far as subclassing WebHTMLView goes, there is the fact that it's a private API, but also that WebView does not expose access to the implementation detail of the document view that WebHTMLView is used for. As far as WebKit being open source, I don't think that matters. Apple wants to prevent private dependencies against system. – danielpunkass Dec 10 '10 at 17:37