/******************************************************************************* Copyright (c) 2007 Adobe Systems Incorporated Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ********************************************************************************/ #include "ScintillaMacOSX.h" #include "ExtInput.h" using namespace Scintilla; // uncomment this for a log to /dev/console // #define LOG_TSM 1 #if LOG_TSM FILE* logFile = NULL; #endif static EventHandlerUPP tsmHandler; static EventTypeSpec tsmSpecs[] = { { kEventClassTextInput, kEventTextInputUpdateActiveInputArea }, // { kEventClassTextInput, kEventTextInputUnicodeForKeyEvent }, { kEventClassTextInput, kEventTextInputOffsetToPos }, { kEventClassTextInput, kEventTextInputPosToOffset }, { kEventClassTextInput, kEventTextInputGetSelectedText } }; #define kScintillaTSM 'ScTs' // The following structure is attached to the HIViewRef as property kScintillaTSM struct TSMData { HIViewRef view; // this view TSMDocumentID docid; // the TSM document ID EventHandlerRef handler; // the event handler ScintillaMacOSX* scintilla; // the Scintilla pointer int styleMask; // the document style mask int indicStyle [3]; // indicator styles save int indicColor [3]; // indicator colors save int selStart; // starting position of selection (Scintilla offset) int selLength; // UTF-8 number of characters int selCur; // current position (Scintilla offset) int inhibitRecursion; // true to stop recursion bool active; // true if this is active }; static const int numSpecs = 5; // Fetch a range of text as UTF-16; delete the buffer after use static char* getTextPortion (TSMData* data, UInt32 start, UInt32 size) { Scintilla::TextRange range; range.chrg.cpMin = start; range.chrg.cpMax = start + size; range.lpstrText = new char [size + 1]; range.lpstrText [size] = 0; data->scintilla->WndProc (SCI_GETTEXTRANGE, 0, (uptr_t) &range); return range.lpstrText; } static pascal OSStatus doHandleTSM (EventHandlerCallRef, EventRef inEvent, void* userData); void ExtInput::attach (HIViewRef viewRef) { if (NULL == tsmHandler) tsmHandler = NewEventHandlerUPP (doHandleTSM); ::UseInputWindow (NULL, FALSE); #ifdef LOG_TSM if (NULL == logFile) logFile = fopen ("/dev/console", "a"); #endif // create and attach the TSM data TSMData* data = new TSMData; data->view = viewRef; data->active = false; data->inhibitRecursion = 0; ::GetControlProperty (viewRef, scintillaMacOSType, 0, sizeof( data->scintilla ), NULL, &data->scintilla); if (NULL != data->scintilla) { // create the TSM document ref InterfaceTypeList interfaceTypes; interfaceTypes[0] = kUnicodeDocumentInterfaceType; ::NewTSMDocument (1, interfaceTypes, &data->docid, (long) viewRef); // install my event handler ::InstallControlEventHandler (viewRef, tsmHandler, numSpecs, tsmSpecs, data, &data->handler); ::SetControlProperty (viewRef, kScintillaTSM, 0, sizeof (data), &data); } else delete data; } static TSMData* getTSMData (HIViewRef viewRef) { TSMData* data = NULL; UInt32 n; ::GetControlProperty (viewRef, kScintillaTSM, 0, sizeof (data), &n, (UInt32*) &data); return data; } void ExtInput::detach (HIViewRef viewRef) { TSMData* data = getTSMData (viewRef); if (NULL != data) { ::DeleteTSMDocument (data->docid); ::RemoveEventHandler (data->handler); delete data; } } void ExtInput::activate (HIViewRef viewRef, bool on) { TSMData* data = getTSMData (viewRef); if (NULL == data) return; if (on) { ::ActivateTSMDocument (data->docid); HIRect bounds; ::HIViewGetBounds (viewRef, &bounds); ::HIViewConvertRect (&bounds, viewRef, NULL); RgnHandle hRgn = ::NewRgn(); ::SetRectRgn (hRgn, (short) bounds.origin.x, (short) bounds.origin.y, (short) (bounds.origin.x + bounds.size.width), (short) (bounds.origin.y + bounds.size.height)); #if LOG_TSM fprintf (logFile, "TSMSetInlineInputRegion (%08lX, %ld:%ld-%ld:%ld)\n", (long) data->docid, (long) bounds.origin.x, (long) bounds.origin.y, (long) (bounds.origin.x + bounds.size.width), (long) (bounds.origin.y + bounds.size.height)); fflush (logFile); #endif ::TSMSetInlineInputRegion (data->docid, HIViewGetWindow (data->view), hRgn); ::DisposeRgn (hRgn); ::UseInputWindow (NULL, FALSE); } else { #if LOG_TSM fprintf (logFile, "DeactivateTSMDocument (%08lX)\n", (long) data->docid); fflush (logFile); #endif ::DeactivateTSMDocument (data->docid); } } static void startInput (TSMData* data, bool delSelection = true) { if (!data->active && 0 == data->inhibitRecursion) { data->active = true; // Delete any selection if( delSelection ) data->scintilla->WndProc (SCI_REPLACESEL, 0, reinterpret_cast("")); // need all style bits because we do indicators data->styleMask = data->scintilla->WndProc (SCI_GETSTYLEBITS, 0, 0); data->scintilla->WndProc (SCI_SETSTYLEBITS, 5, 0); // Set the target range for successive replacements data->selStart = data->selCur = data->scintilla->WndProc (SCI_GETCURRENTPOS, 0, 0); data->selLength = 0; // save needed styles for (int i = 0; i < 2; i++) { data->indicStyle [i] = data->scintilla->WndProc (SCI_INDICGETSTYLE, i, 0); data->indicColor [i] = data->scintilla->WndProc (SCI_INDICGETFORE, i, 0); } // set styles and colors data->scintilla->WndProc (SCI_INDICSETSTYLE, 0, INDIC_SQUIGGLE); data->scintilla->WndProc (SCI_INDICSETFORE, 0, 0x808080); data->scintilla->WndProc (SCI_INDICSETSTYLE, 1, INDIC_PLAIN); // selected converted data->scintilla->WndProc (SCI_INDICSETFORE, 1, 0x808080); data->scintilla->WndProc (SCI_INDICSETSTYLE, 2, INDIC_PLAIN); // selected raw data->scintilla->WndProc (SCI_INDICSETFORE, 2, 0x0000FF); // stop Undo data->scintilla->WndProc (SCI_BEGINUNDOACTION, 0, 0); } } static void stopInput (TSMData* data, int pos) { if (data->active && 0 == data->inhibitRecursion) { // First fix the doc - this may cause more messages // but do not fall into recursion data->inhibitRecursion++; ::FixTSMDocument (data->docid); data->inhibitRecursion--; data->active = false; // Remove indicator styles data->scintilla->WndProc (SCI_STARTSTYLING, data->selStart, INDICS_MASK); data->scintilla->WndProc (SCI_SETSTYLING, pos - data->selStart, 0); // Restore old indicator styles and colors data->scintilla->WndProc (SCI_SETSTYLEBITS, data->styleMask, 0); for (int i = 0; i < 2; i++) { data->scintilla->WndProc (SCI_INDICSETSTYLE, i, data->indicStyle [i]); data->scintilla->WndProc (SCI_INDICSETFORE, i, data->indicColor [i]); } // remove selection and re-allow selections to display data->scintilla->WndProc (SCI_SETSEL, pos, pos); data->scintilla->WndProc (SCI_TARGETFROMSELECTION, 0, 0); data->scintilla->WndProc (SCI_HIDESELECTION, 0, 0); // move the caret behind the current area data->scintilla->WndProc (SCI_SETCURRENTPOS, pos, 0); // re-enable Undo data->scintilla->WndProc (SCI_ENDUNDOACTION, 0, 0); // re-colorize int32_t startLine = data->scintilla->WndProc (SCI_LINEFROMPOSITION, data->selStart, 0); int32_t startPos = data->scintilla->WndProc (SCI_POSITIONFROMLINE, startLine, 0); int32_t endLine = data->scintilla->WndProc (SCI_LINEFROMPOSITION, pos, 0); if (endLine == startLine) endLine++; int32_t endPos = data->scintilla->WndProc (SCI_POSITIONFROMLINE, endLine, 0); data->scintilla->WndProc (SCI_COLOURISE, startPos, endPos); } } void ExtInput::stop (HIViewRef viewRef) { TSMData* data = getTSMData (viewRef); if (NULL != data) stopInput (data, data->selStart + data->selLength); } static char* UTF16toUTF8 (const UniChar* buf, int len, int& utf8len) { CFStringRef str = CFStringCreateWithCharactersNoCopy (NULL, buf, (UInt32) len, kCFAllocatorNull); CFRange range = { 0, len }; CFIndex bufLen; CFStringGetBytes (str, range, kCFStringEncodingUTF8, '?', false, NULL, 0, &bufLen); UInt8* utf8buf = new UInt8 [bufLen+1]; CFStringGetBytes (str, range, kCFStringEncodingUTF8, '?', false, utf8buf, bufLen, NULL); utf8buf [bufLen] = 0; CFRelease (str); utf8len = (int) bufLen; return (char*) utf8buf; } static int UCS2Length (const char* buf, int len) { int n = 0; while (len > 0) { int bytes = 0; char ch = *buf; while (ch & 0x80) bytes++, ch <<= 1; len -= bytes; n += bytes; } return n; } static int UTF8Length (const UniChar* buf, int len) { int n = 0; while (len > 0) { UInt32 uch = *buf++; len--; if (uch >= 0xD800 && uch <= 0xDBFF && len > 0) { UInt32 uch2 = *buf; if (uch2 >= 0xDC00 && uch2 <= 0xDFFF) { buf++; len--; uch = ((uch & 0x3FF) << 10) + (uch2 & 0x3FF); } } n++; if (uch > 0x7F) n++; if (uch > 0x7FF) n++; if (uch > 0xFFFF) n++; if (uch > 0x1FFFFF) n++; if (uch > 0x3FFFFFF) n++; } return n; } static OSStatus handleTSMUpdateActiveInputArea (TSMData* data, EventRef inEvent) { UInt32 fixLength; int caretPos = -1; UInt32 actualSize; ::TextRangeArray* hiliteRanges = NULL; char* hiliteBuffer = NULL; bool done; // extract the text UniChar* buffer = NULL; UniChar temp [128]; UniChar* text = temp; // get the fix length (in bytes) OSStatus err = ::GetEventParameter (inEvent, kEventParamTextInputSendFixLen, typeLongInteger, NULL, sizeof (long), NULL, &fixLength); // need the size (in bytes) if (noErr == err) err = ::GetEventParameter (inEvent, kEventParamTextInputSendText, typeUnicodeText, NULL, 256, &actualSize, temp); // then allocate and fetch if necessary UInt32 textLength = actualSize / sizeof (UniChar); fixLength /= sizeof (UniChar); if (noErr == err) { // this indicates that we are completely done done = (fixLength == textLength || fixLength < 0); if (textLength >= 128) { buffer = text = new UniChar [textLength]; err = ::GetEventParameter (inEvent, kEventParamTextInputSendText, typeUnicodeText, NULL, actualSize, NULL, (void*) text); } // set the text now, but convert it to UTF-8 first int utf8len; char* utf8 = UTF16toUTF8 (text, textLength, utf8len); data->scintilla->WndProc (SCI_SETTARGETSTART, data->selStart, 0); data->scintilla->WndProc (SCI_SETTARGETEND, data->selStart + data->selLength, 0); data->scintilla->WndProc (SCI_HIDESELECTION, 1, 0); data->scintilla->WndProc (SCI_REPLACETARGET, utf8len, (sptr_t) utf8); data->selLength = utf8len; delete [] utf8; } // attempt to extract the array of hilite ranges if (noErr == err) { ::TextRangeArray tempTextRangeArray; OSStatus tempErr = ::GetEventParameter (inEvent, kEventParamTextInputSendHiliteRng, typeTextRangeArray, NULL, sizeof (::TextRangeArray), &actualSize, &tempTextRangeArray); if (noErr == tempErr) { // allocate memory and get the stuff! hiliteBuffer = new char [actualSize]; hiliteRanges = (::TextRangeArray*) hiliteBuffer; err = ::GetEventParameter (inEvent, kEventParamTextInputSendHiliteRng, typeTextRangeArray, NULL, actualSize, NULL, hiliteRanges); if (noErr != err) { delete [] hiliteBuffer; hiliteBuffer = NULL; hiliteRanges = NULL; } } } #if LOG_TSM fprintf (logFile, "kEventTextInputUpdateActiveInputArea:\n" " TextLength = %ld\n" " FixLength = %ld\n", (long) textLength, (long) fixLength); fflush (logFile); #endif if (NULL != hiliteRanges) { for (int i = 0; i < hiliteRanges->fNumOfRanges; i++) { #if LOG_TSM fprintf (logFile, " Range #%d: %ld-%ld (%d)\n", i+1, hiliteRanges->fRange[i].fStart, hiliteRanges->fRange[i].fEnd, hiliteRanges->fRange[i].fHiliteStyle); fflush (logFile); #endif // start and end of range, zero based long bgn = long (hiliteRanges->fRange[i].fStart) / sizeof (UniChar); long end = long (hiliteRanges->fRange[i].fEnd) / sizeof (UniChar); if (bgn >= 0 && end >= 0) { // move the caret if this is requested if (hiliteRanges->fRange[i].fHiliteStyle == kTSMHiliteCaretPosition) caretPos = bgn; else { // determine which style to use int style; switch (hiliteRanges->fRange[i].fHiliteStyle) { case kTSMHiliteRawText: style = INDIC0_MASK; break; case kTSMHiliteSelectedRawText: style = INDIC0_MASK; break; case kTSMHiliteConvertedText: style = INDIC1_MASK; break; case kTSMHiliteSelectedConvertedText: style = INDIC2_MASK; break; default: style = INDIC0_MASK; } // bgn and end are Unicode offsets from the starting pos // use the text buffer to determine the UTF-8 offsets long utf8bgn = data->selStart + UTF8Length (text, bgn); long utf8size = UTF8Length (text + bgn, end - bgn); // set indicators int oldEnd = data->scintilla->WndProc (SCI_GETENDSTYLED, 0, 0); data->scintilla->WndProc (SCI_STARTSTYLING, utf8bgn, INDICS_MASK); data->scintilla->WndProc (SCI_SETSTYLING, utf8size, style & ~1); data->scintilla->WndProc (SCI_STARTSTYLING, oldEnd, 31); } } } } if (noErr == err) { // if the fixed length is == to the new text, we are done if (done) stopInput (data, data->selStart + UTF8Length (text, textLength)); else if (caretPos >= 0) { data->selCur = data->selStart + UTF8Length (text, caretPos); data->scintilla->WndProc (SCI_SETCURRENTPOS, data->selCur, 0); } } delete [] hiliteBuffer; delete [] buffer; return err; } struct MacPoint { short v; short h; }; static OSErr handleTSMOffset2Pos (TSMData* data, EventRef inEvent) { long offset; // get the offfset to convert OSStatus err = ::GetEventParameter (inEvent, kEventParamTextInputSendTextOffset, typeLongInteger, NULL, sizeof (long), NULL, &offset); if (noErr == err) { // where is the caret now? HIPoint where; int line = (int) data->scintilla->WndProc (SCI_LINEFROMPOSITION, data->selCur, 0); where.x = data->scintilla->WndProc (SCI_POINTXFROMPOSITION, 0, data->selCur); where.y = data->scintilla->WndProc (SCI_POINTYFROMPOSITION, 0, data->selCur) + data->scintilla->WndProc (SCI_TEXTHEIGHT, line, 0); // convert to window coords ::HIViewConvertPoint (&where, data->view, NULL); // convert to screen coords Rect global; GetWindowBounds (HIViewGetWindow (data->view), kWindowStructureRgn, &global); MacPoint pt; pt.h = (short) where.x + global.left; pt.v = (short) where.y + global.top; // set the result err = ::SetEventParameter (inEvent, kEventParamTextInputReplyPoint, typeQDPoint, sizeof (MacPoint), &pt); #if LOG_TSM fprintf (logFile, "kEventTextInputOffsetToPos:\n" " Offset: %ld\n" " Pos: %ld:%ld (orig = %ld:%ld)\n", offset, (long) pt.h, (long) pt.v, (long) where.x, (long) where.y); fflush (logFile); #endif } return err; } static OSErr handleTSMPos2Offset (TSMData* data, EventRef inEvent) { MacPoint qdPosition; long offset; short regionClass; // retrieve the global point to convert OSStatus err = ::GetEventParameter (inEvent, kEventParamTextInputSendCurrentPoint, typeQDPoint, NULL, sizeof (MacPoint), NULL, &qdPosition); if (noErr == err) { #if LOG_TSM fprintf (logFile, "kEventTextInputPosToOffset:\n" " Pos: %ld:%ld\n", (long) qdPosition.v, (long) qdPosition.h); fflush (logFile); #endif // convert to local coordinates HIRect rect; rect.origin.x = qdPosition.h; rect.origin.y = qdPosition.v; rect.size.width = rect.size.height = 1; ::HIViewConvertRect (&rect, NULL, data->view); // we always report the position to be within the composition; // coords inside the same pane are clipped to the composition, // and if the position is outside, then we deactivate this instance // this leaves the edit open and active so we can edit multiple panes regionClass = kTSMInsideOfActiveInputArea; // compute the offset (relative value) offset = data->scintilla->WndProc (SCI_POSITIONFROMPOINTCLOSE, (uptr_t) rect.origin.x, (sptr_t) rect.origin.y); if (offset >= 0) { // convert to a UTF-16 offset (Brute Force) char* buf = getTextPortion (data, 0, offset); offset = UCS2Length (buf, offset); delete [] buf; #if LOG_TSM fprintf (logFile, " Offset: %ld (class %ld)\n", offset, (long) regionClass); fflush (logFile); #endif // store the offset err = ::SetEventParameter (inEvent, kEventParamTextInputReplyTextOffset, typeLongInteger, sizeof (long), &offset); if (noErr == err) err = ::SetEventParameter (inEvent, kEventParamTextInputReplyRegionClass, typeShortInteger, sizeof (short), ®ionClass); } else { // not this pane! err = eventNotHandledErr; ExtInput::activate (data->view, false); } } return err; } static OSErr handleTSMGetText (TSMData* data, EventRef inEvent) { char* buf = getTextPortion (data, data->selStart, data->selLength); #if LOG_TSM fprintf (logFile, "kEventTextInputGetSelectedText:\n" " Text: \"%s\"\n", buf); fflush (logFile); #endif OSStatus status = ::SetEventParameter (inEvent, kEventParamTextInputReplyText, typeUTF8Text, data->selLength, buf); delete [] buf; return status; } static pascal OSStatus doHandleTSM (EventHandlerCallRef, EventRef inEvent, void* userData) { TSMData* data = (TSMData*) userData; OSStatus err = eventNotHandledErr; switch (::GetEventKind (inEvent)) { case kEventTextInputUpdateActiveInputArea: // Make sure that input has been started startInput (data); err = handleTSMUpdateActiveInputArea (data, inEvent); break; // case kEventTextInputUnicodeForKeyEvent: // err = handleTSMUnicodeInput (inEvent); // break; case kEventTextInputOffsetToPos: err = handleTSMOffset2Pos (data, inEvent); break; case kEventTextInputPosToOffset: err = handleTSMPos2Offset (data, inEvent); break; case kEventTextInputGetSelectedText: // Make sure that input has been started startInput (data, false); err = handleTSMGetText (data, inEvent); break; } return err; }