I thought this might be helpful for someone. My application implements a fully custom text-editing interface. Of course the problem with that is that you don't get the Mac interface niceties like long-press accents, three-finger tap to lookup, proper IME positioning/handling, and dictation.
This addresses that. It's not a proper patch, so I haven't provided it as such (partly because I have a feeling I took some shortcuts that are not necessarily in keeping with wxMac design). But it's pretty easy to drop in.
Two
mac/ wxWidgets source files have to be changed —
window.h and
window.mm — and then of course you have to implement things in application-side.
window.h:Before the declaration of
wxWindowMac, add:
typedef struct _wxNSRange {
unsigned long location = ULONG_MAX;
unsigned long length = 0;
} wxNSRange;
typedef wxNSRange* wxNSRangePtr;
Then, in the
public section of
wxWindowMac, add:
virtual void MacInsertText(wxString&, wxNSRange& /*replacementRange*/) {}
virtual bool MacHasMarkedText() { return false; }
virtual wxNSRange MacMarkedRange() { return wxNSRange(); }
virtual wxNSRange MacSelectedRange() { return wxNSRange(); }
virtual void MacSetMarkedText(const wxString&, const wxNSRange& /*selectedRange*/, const wxNSRange& /*replacementRange*/) {}
virtual void MacUnmarkText() {}
virtual wxString MacAttributedSubstringForProposedRange(const wxNSRange&, wxNSRangePtr& actualRange)
{ actualRange = NULL; return wxEmptyString; }
// Using wx coordinates:
virtual int MacCharacterIndexForPoint(const wxPoint&) { return UINT_MAX; }
virtual wxRect MacFirstRectForCharacterRange(const wxNSRange&, wxNSRangePtr& actualRange)
{ actualRange = NULL; return wxRect(); }
window.mm:The section beginning
@implementation wxNSView(TextInput) contains stub functions that can be replaced in their entirety with:
@implementation wxNSView(TextInput)
// Conversions
wxNSRange NSRangeToWx(const NSRange nsrange)
{
wxNSRange wxrange;
wxrange.location = nsrange.location;
wxrange.length = nsrange.length;
return wxrange;
}
NSRange wxNSRangeToNS(const wxNSRange wxrange)
{
return NSMakeRange(wxrange.location, wxrange.length);
}
void wxOSX_insertText(NSView* self, SEL _cmd, NSString* text);
- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange
{
wxLogDebug("insertText: '%s' at %s", wxStringWithNSString(aString), wxStringWithNSString(NSStringFromRange(replacementRange)));
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
wxString str = wxStringWithNSString(aString);
wxNSRange wxrange = NSRangeToWx(replacementRange);
// MacInsertText() may modify str or wxrange
impl->GetWXPeer()->MacInsertText(str, wxrange);
if (str.IsEmpty())
return;
aString = wxNSStringWithWxString(str);
replacementRange = wxNSRangeToNS(wxrange);
}
wxOSX_insertText(self, @selector(insertText:), aString);
}
- (void)doCommandBySelector:(SEL)aSelector
{
wxLogDebug("doCommandBySelector: %s", wxStringWithNSString(NSStringFromSelector(aSelector)));
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
impl->doCommandBySelector(aSelector, self, _cmd);
}
- (void)setMarkedText:(id)aString selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
{
if ([aString isKindOfClass:[NSAttributedString class]])
{
// wxString can only use the string portion of NSAttributedString
aString = [aString string];
}
wxLogDebug("setMarkedText: '%s' at %s, %s", wxStringWithNSString(aString), wxStringWithNSString(NSStringFromRange(selectedRange)), wxStringWithNSString(NSStringFromRange(replacementRange)));
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
impl->GetWXPeer()->MacSetMarkedText(wxStringWithNSString(aString), NSRangeToWx(selectedRange), NSRangeToWx(replacementRange));
}
}
- (void)unmarkText
{
wxLogDebug("unmarkText");
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
impl->GetWXPeer()->MacUnmarkText();
}
}
- (NSRange)selectedRange
{
wxLogDebug("selectedRange");
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
return wxNSRangeToNS(impl->GetWXPeer()->MacSelectedRange());
}
return NSMakeRange(NSNotFound, 0);
}
- (NSRange)markedRange
{
wxLogDebug("markedRange");
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
return wxNSRangeToNS(impl->GetWXPeer()->MacMarkedRange());
}
return NSMakeRange(NSNotFound, 0);
}
- (BOOL)hasMarkedText
{
wxLogDebug("hasMarkedText");
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
return impl->GetWXPeer()->MacHasMarkedText() ? YES : NO;
}
return NO;
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
{
wxLogDebug("attributedSubstringForProposedRange: %s", wxStringWithNSString(NSStringFromRange(aRange)));
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
wxNSRange actual = NSRangeToWx(*actualRange);
wxNSRangePtr actualPtr = &actual;
NSString *html = wxNSStringWithWxString(impl->GetWXPeer()->MacAttributedSubstringForProposedRange(NSRangeToWx(aRange), actualPtr));
NSData *htmlData = [html dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *options = @{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType, NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)};
NSMutableAttributedString *aStr = [[NSMutableAttributedString alloc] initWithHTML:htmlData options: options documentAttributes:nil];
return aStr;
}
return nil;
}
- (NSArray*)validAttributesForMarkedText
{
return nil;
}
- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange
{
wxLogDebug("firstRectForCharacterRange: %s", wxStringWithNSString(NSStringFromRange(aRange)));
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
wxNSRange actual = NSRangeToWx(*actualRange);
wxNSRangePtr actualPtr = &actual;
wxRect rect = impl->GetWXPeer()->MacFirstRectForCharacterRange(NSRangeToWx(aRange), actualPtr);
wxPoint pt = rect.GetTopLeft();
NSPoint nspt = wxToNSPoint(NULL, pt);
NSRect nsrect = NSMakeRect(nspt.x, nspt.y, rect.width, rect.height);
return nsrect;
}
return NSMakeRect(0, 0, 0, 0);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)aPoint
{
wxLogDebug("characterIndexForPoint", wxStringWithNSString(NSStringFromPoint(aPoint)));
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if (impl)
{
wxPoint pt = wxFromNSPoint(NULL, aPoint);
int index = impl->GetWXPeer()->MacCharacterIndexForPoint(pt);
return index;
}
return NSNotFound;
}
@end // wxNSView(TextInput)Implementation:You'll need to implement most or all of the new
Mac*() methods from window.h in your
wxWindow-derived class.
If the application does not currently have a way to convert a given text position into an absolute position from the start of the document, you'll need to do that, as all
NSRange parameters are given as such.
An important thing to know is that marked text and selected text are not the same. Selected text is what a user has highlighted. Marked text is text that
NSInputClient is showing prior to committing (i.e., via IME input or dictation). You'll need to keep track of it (and display it) separately.
Note that
insertText() will send an invalid
NSRange when typing normally, but a valid range when replacing marked text (i.e., for a long-press accent, etc.).
attributedSubstringForProposedRange returns a piece of text given the proposed range, such as the highlighted word when three-finger tapping. You'll need to determine the highlighted text.
My implementation of attributedSubstringForProposedRange() uses a simple HTML string. Note that if you're styling the text for that highlighting in the returned HTML that it seems that only system font faces seem to work, not user ones.
characterIndexForPoint() returns the 0-based index in the document text of a given coordinate point. firstRectForCharacterRange() returns the rect for a given range, used for positioning things like three-finger tap highlighting, the accent or IME panel, etc. firstRectForCharacterRange() will send a zero-length NSRange with a valid location when positioning the IME panel or dictation. (Only the position of the rect seems to matter.)
For convenience application-side,
MacCharacterIndexForPoint() and
MacFirstRectForCharacterRange() deal in wx coordinates (
wxPoint and
wxRect).
Additionally, the API seems to provide no notification that an IME panel, dictation instance, etc. is being dismissed so you'll have to figure that out on your own. I can't see a non-client-specific way to do any of that, as it's pretty dependent on the program's behaviour. Chromium, for instance, I think says that to get around this they just clear marked text on every keystroke and let it get re-marked. I don't quite do that, since it doesn't work for my application flow, but I do call
MacUnmarkText() in a number of places.
There are probably a few more NSTextInputClient quirks I'm missing.