diff options
-rw-r--r-- | cocoa/ScintillaCocoa.h | 7 | ||||
-rw-r--r-- | cocoa/ScintillaCocoa.mm | 109 | ||||
-rw-r--r-- | cocoa/ScintillaView.h | 2 | ||||
-rw-r--r-- | cocoa/ScintillaView.mm | 241 |
4 files changed, 230 insertions, 129 deletions
diff --git a/cocoa/ScintillaCocoa.h b/cocoa/ScintillaCocoa.h index 45d04e564..0941ac174 100644 --- a/cocoa/ScintillaCocoa.h +++ b/cocoa/ScintillaCocoa.h @@ -197,7 +197,14 @@ public: virtual void IdleWork(); virtual void QueueIdleWork(WorkNeeded::workItems items, int upTo); int InsertText(NSString* input); + NSRange PositionsFromCharacters(NSRange range) const; + NSRange CharactersFromPositions(NSRange range) const; void SelectOnlyMainSelection(); + void ConvertSelectionVirtualSpace(); + bool ClearAllSelections(); + void CompositionStart(); + void CompositionCommit(); + void CompositionUndo(); virtual void SetDocPointer(Document *document); bool KeyboardInput(NSEvent* event); diff --git a/cocoa/ScintillaCocoa.mm b/cocoa/ScintillaCocoa.mm index f248f3bb0..ced7be3d6 100644 --- a/cocoa/ScintillaCocoa.mm +++ b/cocoa/ScintillaCocoa.mm @@ -2102,22 +2102,42 @@ bool ScintillaCocoa::KeyboardInput(NSEvent* event) int ScintillaCocoa::InsertText(NSString* input) { CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), - vs.styles[STYLE_DEFAULT].characterSet); - CFRange rangeAll = {0, static_cast<CFIndex>([input length])}; - CFIndex usedLen = 0; - CFStringGetBytes((CFStringRef)input, rangeAll, encoding, '?', - false, NULL, 0, &usedLen); - - if (usedLen > 0) + vs.styles[STYLE_DEFAULT].characterSet); + std::string encoded = EncodedBytesString((CFStringRef)input, encoding); + + if (encoded.length() > 0) { - std::vector<UInt8> buffer(usedLen); + AddCharUTF((char*) encoded.c_str(), static_cast<unsigned int>(encoded.length()), false); + } + return static_cast<int>(encoded.length()); +} - CFStringGetBytes((CFStringRef)input, rangeAll, encoding, '?', - false, buffer.data(),usedLen, NULL); +//-------------------------------------------------------------------------------------------------- - AddCharUTF((char*) buffer.data(), static_cast<unsigned int>(usedLen), false); - } - return static_cast<int>(usedLen); +/** + * Convert from a range of characters to a range of bytes. + */ +NSRange ScintillaCocoa::PositionsFromCharacters(NSRange range) const +{ + long start = pdoc->GetRelativePositionUTF16(0, range.location); + if (start == INVALID_POSITION) + start = pdoc->Length(); + long end = pdoc->GetRelativePositionUTF16(start, range.length); + if (end == INVALID_POSITION) + end = pdoc->Length(); + return NSMakeRange(start, end - start); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Convert from a range of characters from a range of bytes. + */ +NSRange ScintillaCocoa::CharactersFromPositions(NSRange range) const +{ + const long start = pdoc->CountUTF16(0, range.location); + const long len = pdoc->CountUTF16(range.location, NSMaxRange(range)); + return NSMakeRange(start, len); } //-------------------------------------------------------------------------------------------------- @@ -2125,17 +2145,72 @@ int ScintillaCocoa::InsertText(NSString* input) /** * Used to ensure that only one selection is active for input composition as composition * does not support multi-typing. - * Also drop virtual space as that is not supported by composition. */ void ScintillaCocoa::SelectOnlyMainSelection() { - SelectionRange mainSel = sel.RangeMain(); - mainSel.ClearVirtualSpace(); - sel.SetSelection(mainSel); + sel.SetSelection(sel.RangeMain()); Redraw(); } //-------------------------------------------------------------------------------------------------- + +/** + * Convert virtual space before selection into real space. + */ +void ScintillaCocoa::ConvertSelectionVirtualSpace() +{ + FillVirtualSpace(); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Erase all selected text and return whether the selection is now empty. + * The selection may not be empty if the selection contained protected text. + */ +bool ScintillaCocoa::ClearAllSelections() +{ + ClearSelection(true); + return sel.Empty(); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Start composing for IME. + */ +void ScintillaCocoa::CompositionStart() +{ + if (!sel.Empty()) + { + NSLog(@"Selection not empty when starting composition"); + } + pdoc->TentativeStart(); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Commit the IME text. + */ +void ScintillaCocoa::CompositionCommit() +{ + pdoc->TentativeCommit(); + pdoc->decorations.SetCurrentIndicator(INDIC_IME); + pdoc->DecorationFillRange(0, 0, pdoc->Length()); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Remove the IME text. + */ +void ScintillaCocoa::CompositionUndo() +{ + pdoc->TentativeUndo(); +} + +//-------------------------------------------------------------------------------------------------- /** * When switching documents discard any incomplete character composition state as otherwise tries to * act on the new document. diff --git a/cocoa/ScintillaView.h b/cocoa/ScintillaView.h index 102bf3b60..e328646a5 100644 --- a/cocoa/ScintillaView.h +++ b/cocoa/ScintillaView.h @@ -79,12 +79,10 @@ extern NSString *const SCIUpdateUINotification; // Set when we are in composition mode and partial input is displayed. NSRange mMarkedTextRange; - BOOL undoCollectionWasActive; } @property (nonatomic, assign) ScintillaView* owner; -- (void) removeMarkedText; - (void) setCursor: (int) cursor; - (BOOL) canUndo; diff --git a/cocoa/ScintillaView.mm b/cocoa/ScintillaView.mm index 43ceb8b48..d5f658755 100644 --- a/cocoa/ScintillaView.mm +++ b/cocoa/ScintillaView.mm @@ -19,9 +19,6 @@ using namespace Scintilla; static NSCursor* reverseArrowCursor; static NSCursor* waitCursor; -// The scintilla indicator used for keyboard input. -#define INPUT_INDICATOR INDIC_MAX - 1 - NSString *const SCIUpdateUINotification = @"SCIUpdateUI"; /** @@ -347,14 +344,52 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange { - return nil; + const NSRange posRange = mOwner.backend->PositionsFromCharacters(aRange); + // The backend validated aRange and may have removed characters beyond the end of the document. + const NSRange charRange = mOwner.backend->CharactersFromPositions(posRange); + if (!NSEqualRanges(aRange, charRange)) + { + *actualRange = charRange; + } + + [mOwner message: SCI_SETTARGETRANGE wParam: posRange.location lParam: NSMaxRange(posRange)]; + std::string text([mOwner message: SCI_TARGETASUTF8] + 1, 0); + [mOwner message: SCI_TARGETASUTF8 wParam: 0 lParam: reinterpret_cast<sptr_t>(&text[0])]; + NSString *result = [NSString stringWithUTF8String: text.c_str()]; + NSMutableAttributedString *asResult = [[[NSMutableAttributedString alloc] initWithString:result] autorelease]; + + const NSRange rangeAS = NSMakeRange(0, [asResult length]); + const long style = [mOwner message: SCI_GETSTYLEAT wParam:posRange.location]; + std::string fontName([mOwner message: SCI_STYLEGETFONT wParam:style lParam:0] + 1, 0); + [mOwner message: SCI_STYLEGETFONT wParam:style lParam:(sptr_t)&fontName[0]]; + const CGFloat fontSize = [mOwner message: SCI_STYLEGETSIZEFRACTIONAL wParam:style] / 100.0f; + NSString *sFontName = [NSString stringWithUTF8String: fontName.c_str()]; + NSFont *font = [NSFont fontWithName:sFontName size:fontSize]; + [asResult addAttribute:NSFontAttributeName value:font range:rangeAS]; + + return asResult; } //-------------------------------------------------------------------------------------------------- - (NSUInteger) characterIndexForPoint: (NSPoint) point { - return NSNotFound; + const NSRect rectPoint = {point, NSZeroSize}; + const NSRect rectInWindow = [self.window convertRectFromScreen:rectPoint]; + const NSRect rectLocal = [[[self superview] superview] convertRect:rectInWindow fromView:nil]; + + const long position = [mOwner message: SCI_CHARPOSITIONFROMPOINT + wParam: rectLocal.origin.x + lParam: rectLocal.origin.y]; + if (position == INVALID_POSITION) + { + return NSNotFound; + } + else + { + const NSRange index = mOwner.backend->CharactersFromPositions(NSMakeRange(position, 0)); + return index.location; + } } //-------------------------------------------------------------------------------------------------- @@ -369,32 +404,19 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) - (NSRect) firstRectForCharacterRange: (NSRange) aRange actualRange: (NSRangePointer) actualRange { + const NSRange posRange = mOwner.backend->PositionsFromCharacters(aRange); + NSRect rect; - rect.origin.x = [ScintillaView directCall: mOwner - message: SCI_POINTXFROMPOSITION - wParam: 0 - lParam: aRange.location]; - rect.origin.y = [ScintillaView directCall: mOwner - message: SCI_POINTYFROMPOSITION - wParam: 0 - lParam: aRange.location]; - NSUInteger rangeEnd = aRange.location + aRange.length; - rect.size.width = [ScintillaView directCall: mOwner - message: SCI_POINTXFROMPOSITION - wParam: 0 - lParam: rangeEnd] - rect.origin.x; - rect.size.height = [ScintillaView directCall: mOwner - message: SCI_POINTYFROMPOSITION - wParam: 0 - lParam: rangeEnd] - rect.origin.y; - rect.size.height += [ScintillaView directCall: mOwner - message: SCI_TEXTHEIGHT - wParam: 0 - lParam: 0]; - rect = [[[self superview] superview] convertRect:rect toView:nil]; - rect = [self.window convertRectToScreen:rect]; - - return rect; + rect.origin.x = [mOwner message: SCI_POINTXFROMPOSITION wParam: 0 lParam: posRange.location]; + rect.origin.y = [mOwner message: SCI_POINTYFROMPOSITION wParam: 0 lParam: posRange.location]; + const NSUInteger rangeEnd = NSMaxRange(posRange); + rect.size.width = [mOwner message: SCI_POINTXFROMPOSITION wParam: 0 lParam: rangeEnd] - rect.origin.x; + rect.size.height = [mOwner message: SCI_POINTYFROMPOSITION wParam: 0 lParam: rangeEnd] - rect.origin.y; + rect.size.height += [mOwner message: SCI_TEXTHEIGHT wParam: 0 lParam: 0]; + const NSRect rectInWindow = [[[self superview] superview] convertRect:rect toView:nil]; + const NSRect rectScreen = [self.window convertRectToScreen:rectInWindow]; + + return rectScreen; } //-------------------------------------------------------------------------------------------------- @@ -419,14 +441,27 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) } // Remove any previously marked text first. - [self removeMarkedText]; + mOwner.backend->CompositionUndo(); + if (mMarkedTextRange.location != NSNotFound) + { + const NSRange posRangeMark = mOwner.backend->PositionsFromCharacters(mMarkedTextRange); + [mOwner message: SCI_SETEMPTYSELECTION wParam: posRangeMark.location]; + } + mMarkedTextRange = NSMakeRange(NSNotFound, 0); if (replacementRange.location == (NSNotFound-1)) // This occurs when the accent popup is visible and menu selected. // Its replacing a non-existent position so do nothing. return; - [mOwner deleteRange: replacementRange]; + if (replacementRange.location != NSNotFound) + { + const NSRange posRangeReplacement = mOwner.backend->PositionsFromCharacters(replacementRange); + [mOwner message: SCI_DELETERANGE + wParam: posRangeReplacement.location + lParam: posRangeReplacement.length]; + [mOwner message: SCI_SETEMPTYSELECTION wParam: posRangeReplacement.location]; + } NSString* newText = @""; if ([aString isKindOfClass:[NSString class]]) @@ -448,9 +483,10 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) - (NSRange) selectedRange { - long begin = [mOwner getGeneralProperty: SCI_GETSELECTIONSTART parameter: 0]; - long end = [mOwner getGeneralProperty: SCI_GETSELECTIONEND parameter: 0]; - return NSMakeRange(begin, end - begin); + const long positionBegin = [mOwner message: SCI_GETSELECTIONSTART]; + const long positionEnd = [mOwner message: SCI_GETSELECTIONEND]; + NSRange posRangeSel = NSMakeRange(positionBegin, positionEnd-positionBegin); + return mOwner.backend->CharactersFromPositions(posRangeSel); } //-------------------------------------------------------------------------------------------------- @@ -473,86 +509,85 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) if ([aString isKindOfClass:[NSAttributedString class]]) newText = (NSString*) [aString string]; - long currentPosition; - // Replace marked text if there is one. if (mMarkedTextRange.length > 0) { + mOwner.backend->CompositionUndo(); if (replacementRange.location != NSNotFound) { // This situation makes no sense and has not occurred in practice. - // Should the marked range remain marked in addition to the new text - // or should it be removed first? NSLog(@"Can not handle a replacement range when there is also a marked range"); } - - [mOwner deleteRange: mMarkedTextRange]; - currentPosition = mMarkedTextRange.location; + else + { + replacementRange = mMarkedTextRange; + const NSRange posRangeMark = mOwner.backend->PositionsFromCharacters(mMarkedTextRange); + [mOwner message: SCI_SETEMPTYSELECTION wParam: posRangeMark.location]; + } } else { // Must perform deletion before entering composition mode or else // both document and undo history will not contain the deleted text // leading to an inaccurate and unusable undo history. + + // Convert selection virtual space into real space + mOwner.backend->ConvertSelectionVirtualSpace(); if (replacementRange.location != NSNotFound) { - [mOwner deleteRange: replacementRange]; - currentPosition = replacementRange.location; + const NSRange posRangeReplacement = mOwner.backend->PositionsFromCharacters(replacementRange); + [mOwner message: SCI_DELETERANGE + wParam: posRangeReplacement.location + lParam: posRangeReplacement.length]; } else // No marked or replacement range, so replace selection { - // Ensure only a single selection - mOwner.backend->SelectOnlyMainSelection(); - - NSRange selectionRangeCurrent = [self selectedRange]; - if (selectionRangeCurrent.length > 0) - { - [mOwner deleteRange: selectionRangeCurrent]; + if (!mOwner.backend->ScintillaCocoa::ClearAllSelections()) { + // Some of the selection is protected so can not perform composition here + return; } - currentPosition = selectionRangeCurrent.location; + // Ensure only a single selection. + mOwner.backend->SelectOnlyMainSelection(); + replacementRange = [self selectedRange]; } - - // Switching into composition so remember if collecting undo. - undoCollectionWasActive = [mOwner getGeneralProperty: SCI_GETUNDOCOLLECTION] != 0; - - // Keep Scintilla from collecting undo actions for the composition task. - [mOwner setGeneralProperty: SCI_SETUNDOCOLLECTION value: 0]; } - [mOwner message: SCI_SETEMPTYSELECTION wParam: currentPosition]; - // Note: Scintilla internally works almost always with bytes instead chars, so we need to take - // this into account when determining selection ranges and such. - int lengthInserted = mOwner.backend->InsertText(newText); + // To support IME input to multiple selections, the following code would + // need to insert newText at each selection, mark each piece of new text and then + // select range relative to each insertion. - if (lengthInserted > 0) + if ([newText length]) { - mMarkedTextRange = NSMakeRange(currentPosition, lengthInserted); + // Switching into composition. + mOwner.backend->CompositionStart(); + + NSRange posRangeCurrent = mOwner.backend->PositionsFromCharacters(NSMakeRange(replacementRange.location, 0)); + // Note: Scintilla internally works almost always with bytes instead chars, so we need to take + // this into account when determining selection ranges and such. + int lengthInserted = mOwner.backend->InsertText(newText); + posRangeCurrent.length = lengthInserted; + mMarkedTextRange = mOwner.backend->CharactersFromPositions(posRangeCurrent); // Mark the just inserted text. Keep the marked range for later reset. - [mOwner setGeneralProperty: SCI_SETINDICATORCURRENT value: INPUT_INDICATOR]; + [mOwner setGeneralProperty: SCI_SETINDICATORCURRENT value: INDIC_IME]; [mOwner setGeneralProperty: SCI_INDICATORFILLRANGE - parameter: mMarkedTextRange.location - value: mMarkedTextRange.length]; + parameter: posRangeCurrent.location + value: posRangeCurrent.length]; } else { mMarkedTextRange = NSMakeRange(NSNotFound, 0); // Re-enable undo action collection if composition ended (indicated by an empty mark string). - if (undoCollectionWasActive) - [mOwner setGeneralProperty: SCI_SETUNDOCOLLECTION value: range.length == 0]; + mOwner.backend->CompositionCommit(); } // Select the part which is indicated in the given range. It does not scroll the caret into view. if (range.length > 0) { // range is in characters so convert to bytes for selection. - long rangeStart = currentPosition; - for (size_t characterInComposition=0; characterInComposition<range.location; characterInComposition++) - rangeStart = [mOwner getGeneralProperty: SCI_POSITIONAFTER parameter: rangeStart]; - long rangeEnd = rangeStart; - for (size_t characterInRange=0; characterInRange<range.length; characterInRange++) - rangeEnd = [mOwner getGeneralProperty: SCI_POSITIONAFTER parameter: rangeEnd]; - [mOwner setGeneralProperty: SCI_SETSELECTION parameter: rangeEnd value: rangeStart]; + range.location += replacementRange.location; + NSRange posRangeSelect = mOwner.backend->PositionsFromCharacters(range); + [mOwner setGeneralProperty: SCI_SETSELECTION parameter: NSMaxRange(posRangeSelect) value: posRangeSelect.location]; } } @@ -562,35 +597,8 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) { if (mMarkedTextRange.length > 0) { - [mOwner setGeneralProperty: SCI_SETINDICATORCURRENT value: INPUT_INDICATOR]; - [mOwner setGeneralProperty: SCI_INDICATORCLEARRANGE - parameter: mMarkedTextRange.location - value: mMarkedTextRange.length]; - mMarkedTextRange = NSMakeRange(NSNotFound, 0); - - // Reenable undo action collection, after we are done with text composition. - if (undoCollectionWasActive) - [mOwner setGeneralProperty: SCI_SETUNDOCOLLECTION value: 1]; - } -} - -//-------------------------------------------------------------------------------------------------- - -/** - * Removes any currently marked text. - */ -- (void) removeMarkedText -{ - if (mMarkedTextRange.length > 0) - { - // We have already marked text. Replace that. - [mOwner deleteRange: mMarkedTextRange]; - + mOwner.backend->CompositionCommit(); mMarkedTextRange = NSMakeRange(NSNotFound, 0); - - // Reenable undo action collection, after we are done with text composition. - if (undoCollectionWasActive) - [mOwner setGeneralProperty: SCI_SETUNDOCOLLECTION value: 1]; } } @@ -831,12 +839,24 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) - (void) paste: (id) sender { #pragma unused(sender) + if (mMarkedTextRange.location != NSNotFound) + { + [[NSTextInputContext currentInputContext] discardMarkedText]; + mOwner.backend->CompositionCommit(); + mMarkedTextRange = NSMakeRange(NSNotFound, 0); + } mOwner.backend->Paste(); } - (void) undo: (id) sender { #pragma unused(sender) + if (mMarkedTextRange.location != NSNotFound) + { + [[NSTextInputContext currentInputContext] discardMarkedText]; + mOwner.backend->CompositionCommit(); + mMarkedTextRange = NSMakeRange(NSNotFound, 0); + } mOwner.backend->Undo(); } @@ -848,7 +868,7 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) - (BOOL) canUndo { - return mOwner.backend->CanUndo(); + return mOwner.backend->CanUndo() && (mMarkedTextRange.location == NSNotFound); } - (BOOL) canRedo @@ -1141,10 +1161,10 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) // Setup a special indicator used in the editor to provide visual feedback for // input composition, depending on language, keyboard etc. - [self setColorProperty: SCI_INDICSETFORE parameter: INPUT_INDICATOR fromHTML: @"#FF0000"]; - [self setGeneralProperty: SCI_INDICSETUNDER parameter: INPUT_INDICATOR value: 1]; - [self setGeneralProperty: SCI_INDICSETSTYLE parameter: INPUT_INDICATOR value: INDIC_PLAIN]; - [self setGeneralProperty: SCI_INDICSETALPHA parameter: INPUT_INDICATOR value: 100]; + [self setColorProperty: SCI_INDICSETFORE parameter: INDIC_IME fromHTML: @"#FF0000"]; + [self setGeneralProperty: SCI_INDICSETUNDER parameter: INDIC_IME value: 1]; + [self setGeneralProperty: SCI_INDICSETSTYLE parameter: INDIC_IME value: INDIC_PLAIN]; + [self setGeneralProperty: SCI_INDICSETALPHA parameter: INDIC_IME value: 100]; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self @@ -1318,7 +1338,8 @@ static NSCursor *cursorFromEnum(Window::Cursor cursor) { if (aRange.length > 0) { - [self message: SCI_DELETERANGE wParam: aRange.location lParam: aRange.length]; + NSRange posRange = mBackend->PositionsFromCharacters(aRange); + [self message: SCI_DELETERANGE wParam: posRange.location lParam: posRange.length]; } } |