Mac NSTextInputClient implementation
KT <[email protected]> Thu, 26 Jun 2025 12:08:06 -0700 (PDT)
| Newsgroups | gmane.comp.lib.wxwindows.general |
|---|---|
| Message-ID | <[email protected]> |
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.
--
Please read https://www.wxwidgets.org/support/mlhowto.htm before posting.
---
You received this message because you are subscribed to the Google Groups "wx-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To view this discussion visit https://groups.google.com/d/msgid/wx-users/790804db-0bb4-4937-9255-e510266eeff8n%40googlegroups.com.