/** * Implementation of the native Cocoa View that serves as container for the scintilla parts. * @file ScintillaView.mm * * Created by Mike Lischke. * * Copyright 2011, 2013, Oracle and/or its affiliates. All rights reserved. * Copyright 2009, 2011 Sun Microsystems, Inc. All rights reserved. * This file is dual licensed under LGPL v2.1 and the Scintilla license (http://www.scintilla.org/License.txt). */ #include #include #include #import "Platform.h" #import "ScintillaView.h" #import "ScintillaCocoa.h" using namespace Scintilla; // Add backend property to ScintillaView as a private category. // Specified here as backend accessed by SCIMarginView and SCIContentView. @interface ScintillaView() @property(nonatomic, readonly) Scintilla::ScintillaCocoa *backend; @end // Two additional cursors we need, which aren't provided by Cocoa. static NSCursor *reverseArrowCursor; static NSCursor *waitCursor; NSString *const SCIUpdateUINotification = @"SCIUpdateUI"; /** * Provide an NSCursor object that matches the Window::Cursor enumeration. */ static NSCursor *cursorFromEnum(Window::Cursor cursor) { switch (cursor) { case Window::cursorText: return [NSCursor IBeamCursor]; case Window::cursorArrow: return [NSCursor arrowCursor]; case Window::cursorWait: return waitCursor; case Window::cursorHoriz: return [NSCursor resizeLeftRightCursor]; case Window::cursorVert: return [NSCursor resizeUpDownCursor]; case Window::cursorReverseArrow: return reverseArrowCursor; case Window::cursorUp: default: return [NSCursor arrowCursor]; } } @implementation SCIScrollView - (void) tile { [super tile]; #if defined(MAC_OS_X_VERSION_10_14) if (std::floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_13) { NSRect frame = self.contentView.frame; frame.origin.x = self.verticalRulerView.requiredThickness; frame.size.width -= frame.origin.x; self.contentView.frame = frame; } #endif } @end // Add marginWidth and owner properties as a private category. @interface SCIMarginView() @property(assign) int marginWidth; @property(nonatomic, weak) ScintillaView *owner; @end @implementation SCIMarginView { int marginWidth; ScintillaView *__weak owner; NSMutableArray *currentCursors; } @synthesize marginWidth, owner; - (instancetype) initWithScrollView: (NSScrollView *) aScrollView { self = [super initWithScrollView: aScrollView orientation: NSVerticalRuler]; if (self != nil) { owner = nil; marginWidth = 20; currentCursors = [NSMutableArray arrayWithCapacity: 0]; for (size_t i=0; i<=SC_MAX_MARGIN; i++) { [currentCursors addObject: reverseArrowCursor]; } self.clientView = aScrollView.documentView; if ([self respondsToSelector: @selector(setAccessibilityLabel:)]) self.accessibilityLabel = @"Scintilla Margin"; } return self; } - (void) setFrame: (NSRect) frame { super.frame = frame; [self.window invalidateCursorRectsForView: self]; } - (CGFloat) requiredThickness { return marginWidth; } - (void) drawHashMarksAndLabelsInRect: (NSRect) aRect { if (owner) { NSRect contentRect = self.scrollView.contentView.bounds; NSRect marginRect = self.bounds; // Ensure paint to bottom of view to avoid glitches if (marginRect.size.height > contentRect.size.height) { // Legacy scroll bar mode leaves a poorly painted corner aRect = marginRect; } owner.backend->PaintMargin(aRect); } } /** * Called by the framework if it wants to show a context menu for the margin. */ - (NSMenu *) menuForEvent: (NSEvent *) theEvent { NSMenu *menu = [owner menuForEvent: theEvent]; if (menu) { return menu; } else if (owner.backend->ShouldDisplayPopupOnMargin()) { return owner.backend->CreateContextMenu(theEvent); } else { return nil; } } - (void) mouseDown: (NSEvent *) theEvent { NSClipView *textView = self.scrollView.contentView; [textView.window makeFirstResponder: textView]; owner.backend->MouseDown(theEvent); } - (void) rightMouseDown: (NSEvent *) theEvent { [NSMenu popUpContextMenu: [self menuForEvent: theEvent] withEvent: theEvent forView: self]; owner.backend->RightMouseDown(theEvent); } - (void) mouseDragged: (NSEvent *) theEvent { owner.backend->MouseMove(theEvent); } - (void) mouseMoved: (NSEvent *) theEvent { owner.backend->MouseMove(theEvent); } - (void) mouseUp: (NSEvent *) theEvent { owner.backend->MouseUp(theEvent); } // Not a simple button so return failure - (BOOL) accessibilityPerformPress { return NO; } /** * This method is called to give us the opportunity to define our mouse sensitive rectangle. */ - (void) resetCursorRects { [super resetCursorRects]; int x = 0; NSRect marginRect = self.bounds; size_t co = currentCursors.count; for (size_t i=0; iWndProc(SCI_GETMARGINCURSORN, i, 0); long width =owner.backend->WndProc(SCI_GETMARGINWIDTHN, i, 0); NSCursor *cc = cursorFromEnum(static_cast(cursType)); currentCursors[i] = cc; marginRect.origin.x = x; marginRect.size.width = width; [self addCursorRect: marginRect cursor: cc]; [cc setOnMouseEntered: YES]; x += width; } } @end // Add owner property as a private category. @interface SCIContentView() @property(nonatomic, weak) ScintillaView *owner; @end @implementation SCIContentView { ScintillaView *__weak mOwner; NSCursor *mCurrentCursor; NSTrackingArea *trackingArea; // Set when we are in composition mode and partial input is displayed. NSRange mMarkedTextRange; } @synthesize owner = mOwner; //-------------------------------------------------------------------------------------------------- - (NSView *) initWithFrame: (NSRect) frame { self = [super initWithFrame: frame]; if (self != nil) { // Some initialization for our view. mCurrentCursor = [NSCursor arrowCursor]; trackingArea = nil; mMarkedTextRange = NSMakeRange(NSNotFound, 0); [self registerForDraggedTypes: @[NSStringPboardType, ScintillaRecPboardType, NSFilenamesPboardType]]; // Set up accessibility in the text role if ([self respondsToSelector: @selector(setAccessibilityElement:)]) { self.accessibilityElement = TRUE; self.accessibilityEnabled = TRUE; self.accessibilityLabel = NSLocalizedString(@"Scintilla", nil); // No real localization self.accessibilityRoleDescription = @"source code editor"; self.accessibilityRole = NSAccessibilityTextAreaRole; self.accessibilityIdentifier = @"Scintilla"; } } return self; } //-------------------------------------------------------------------------------------------------- /** * When the view is resized or scrolled we need to update our tracking area. */ - (void) updateTrackingAreas { if (trackingArea) { [self removeTrackingArea: trackingArea]; } int opts = (NSTrackingActiveAlways | NSTrackingInVisibleRect | NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved); trackingArea = [[NSTrackingArea alloc] initWithRect: self.bounds options: opts owner: self userInfo: nil]; [self addTrackingArea: trackingArea]; [super updateTrackingAreas]; } //-------------------------------------------------------------------------------------------------- /** * When the view is resized we need to let the backend know. */ - (void) setFrame: (NSRect) frame { super.frame = frame; mOwner.backend->Resize(); } //-------------------------------------------------------------------------------------------------- /** * Called by the backend if a new cursor must be set for the view. */ - (void) setCursor: (int) cursor { Window::Cursor eCursor = (Window::Cursor)cursor; mCurrentCursor = cursorFromEnum(eCursor); // Trigger recreation of the cursor rectangle(s). [self.window invalidateCursorRectsForView: self]; [mOwner updateMarginCursors]; } //-------------------------------------------------------------------------------------------------- /** * This method is called to give us the opportunity to define our mouse sensitive rectangle. */ - (void) resetCursorRects { [super resetCursorRects]; // We only have one cursor rect: our bounds. const NSRect visibleBounds = mOwner.backend->GetBounds(); [self addCursorRect: visibleBounds cursor: mCurrentCursor]; [mCurrentCursor setOnMouseEntered: YES]; } //-------------------------------------------------------------------------------------------------- /** * Called before repainting. */ - (void) viewWillDraw { if (!mOwner) { [super viewWillDraw]; return; } const NSRect *rects; NSInteger nRects = 0; [self getRectsBeingDrawn: &rects count: &nRects]; if (nRects > 0) { NSRect rectUnion = rects[0]; for (int i=0; iWillDraw(rectUnion); } [super viewWillDraw]; } //-------------------------------------------------------------------------------------------------- /** * Called before responsive scrolling overdraw. */ - (void) prepareContentInRect: (NSRect) rect { if (mOwner) mOwner.backend->WillDraw(rect); #if MAC_OS_X_VERSION_MAX_ALLOWED > 1080 [super prepareContentInRect: rect]; #endif } //-------------------------------------------------------------------------------------------------- /** * Gets called by the runtime when the view needs repainting. */ - (void) drawRect: (NSRect) rect { CGContextRef context = (CGContextRef) [NSGraphicsContext currentContext].graphicsPort; if (!mOwner.backend->Draw(rect, context)) { dispatch_async(dispatch_get_main_queue(), ^ { [self setNeedsDisplay: YES]; }); } } //-------------------------------------------------------------------------------------------------- /** * Windows uses a client coordinate system where the upper left corner is the origin in a window * (and so does Scintilla). We have to adjust for that. However by returning YES here, we are * already done with that. * Note that because of returning YES here most coordinates we use now (e.g. for painting, * invalidating rectangles etc.) are given with +Y pointing down! */ - (BOOL) isFlipped { return YES; } //-------------------------------------------------------------------------------------------------- - (BOOL) isOpaque { return YES; } //-------------------------------------------------------------------------------------------------- /** * Implement the "click through" behavior by telling the caller we accept the first mouse event too. */ - (BOOL) acceptsFirstMouse: (NSEvent *) theEvent { #pragma unused(theEvent) return YES; } //-------------------------------------------------------------------------------------------------- /** * Make this view accepting events as first responder. */ - (BOOL) acceptsFirstResponder { return YES; } //-------------------------------------------------------------------------------------------------- /** * Called by the framework if it wants to show a context menu for the editor. */ - (NSMenu *) menuForEvent: (NSEvent *) theEvent { NSMenu *menu = [mOwner menuForEvent: theEvent]; if (menu) { return menu; } else if (mOwner.backend->ShouldDisplayPopupOnText()) { return mOwner.backend->CreateContextMenu(theEvent); } else { return nil; } } //-------------------------------------------------------------------------------------------------- // Adoption of NSTextInputClient protocol. - (NSAttributedString *) attributedSubstringForProposedRange: (NSRange) aRange actualRange: (NSRangePointer) actualRange { const NSInteger lengthCharacters = self.accessibilityNumberOfCharacters; if (aRange.location > lengthCharacters) { 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], 0); [mOwner message: SCI_TARGETASUTF8 wParam: 0 lParam: reinterpret_cast(&text[0])]; text = FixInvalidUTF8(text); NSString *result = @(text.c_str()); NSMutableAttributedString *asResult = [[NSMutableAttributedString alloc] initWithString: result]; const NSRange rangeAS = NSMakeRange(0, asResult.length); // SCI_GETSTYLEAT reports a signed byte but want an unsigned to index into styles const char styleByte = static_cast([mOwner message: SCI_GETSTYLEAT wParam: posRange.location]); const long style = static_cast(styleByte); std::string fontName([mOwner message: SCI_STYLEGETFONT wParam: style lParam: 0], 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 = @(fontName.c_str()); NSFont *font = [NSFont fontWithName: sFontName size: fontSize]; if (font) { [asResult addAttribute: NSFontAttributeName value: font range: rangeAS]; } return asResult; } //-------------------------------------------------------------------------------------------------- - (NSUInteger) characterIndexForPoint: (NSPoint) point { 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; } } //-------------------------------------------------------------------------------------------------- - (void) doCommandBySelector: (SEL) selector { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if ([self respondsToSelector: selector]) [self performSelector: selector withObject: nil]; #pragma clang diagnostic pop } //-------------------------------------------------------------------------------------------------- - (NSRect) firstRectForCharacterRange: (NSRange) aRange actualRange: (NSRangePointer) actualRange { #pragma unused(actualRange) const NSRange posRange = mOwner.backend->PositionsFromCharacters(aRange); NSRect 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; } //-------------------------------------------------------------------------------------------------- - (BOOL) hasMarkedText { return mMarkedTextRange.length > 0; } //-------------------------------------------------------------------------------------------------- /** * General text input. Used to insert new text at the current input position, replacing the current * selection if there is any. * First removes the replacementRange. */ - (void) insertText: (id) aString replacementRange: (NSRange) replacementRange { if ((mMarkedTextRange.location != NSNotFound) && (replacementRange.location != NSNotFound)) { NSLog(@"Trying to insertText when there is both a marked range and a replacement range"); } // Remove any previously marked text first. 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; 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]]) newText = (NSString *) aString; else if ([aString isKindOfClass: [NSAttributedString class]]) newText = (NSString *) [aString string]; mOwner.backend->InsertText(newText, EditModel::CharacterSource::directInput); } //-------------------------------------------------------------------------------------------------- - (NSRange) markedRange { return mMarkedTextRange; } //-------------------------------------------------------------------------------------------------- - (NSRange) selectedRange { const NSRange posRangeSel = [mOwner selectedRangePositions]; if (posRangeSel.length == 0) { NSTextInputContext *tic = [NSTextInputContext currentInputContext]; // Chinese input causes malloc crash when empty selection returned with actual // position so return NSNotFound. // If this is applied to European input, it stops the accented character // chooser from appearing. // May need to add more input source names. if ([tic.selectedKeyboardInputSource isEqualToString: @"com.apple.inputmethod.TCIM.Cangjie"]) { return NSMakeRange(NSNotFound, 0); } } return mOwner.backend->CharactersFromPositions(posRangeSel); } //-------------------------------------------------------------------------------------------------- /** * Called by the input manager to set text which might be combined with further input to form * the final text (e.g. composition of ^ and a to รข). * * @param aString The text to insert, either what has been marked already or what is selected already * or simply added at the current insertion point. Depending on what is available. * @param range The range of the new text to select (given relative to the insertion point of the new text). * @param replacementRange The range to remove before insertion. */ - (void) setMarkedText: (id) aString selectedRange: (NSRange) range replacementRange: (NSRange) replacementRange { NSString *newText = @""; if ([aString isKindOfClass: [NSString class]]) newText = (NSString *) aString; else if ([aString isKindOfClass: [NSAttributedString class]]) newText = (NSString *) [aString string]; // 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. NSLog(@"Can not handle a replacement range when there is also a marked range"); } 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) { 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 if (!mOwner.backend->ScintillaCocoa::ClearAllSelections()) { // Some of the selection is protected so can not perform composition here return; } // Ensure only a single selection. mOwner.backend->SelectOnlyMainSelection(); const NSRange posRangeSel = [mOwner selectedRangePositions]; replacementRange = mOwner.backend->CharactersFromPositions(posRangeSel); } } // 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 (newText.length) { // 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. ptrdiff_t lengthInserted = mOwner.backend->InsertText(newText, EditModel::CharacterSource::tentativeInput); 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: INDICATOR_IME]; [mOwner setGeneralProperty: SCI_INDICATORFILLRANGE 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). 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. range.location += replacementRange.location; NSRange posRangeSelect = mOwner.backend->PositionsFromCharacters(range); [mOwner setGeneralProperty: SCI_SETSELECTION parameter: NSMaxRange(posRangeSelect) value: posRangeSelect.location]; } } //-------------------------------------------------------------------------------------------------- - (void) unmarkText { if (mMarkedTextRange.length > 0) { mOwner.backend->CompositionCommit(); mMarkedTextRange = NSMakeRange(NSNotFound, 0); } } //-------------------------------------------------------------------------------------------------- - (NSArray *) validAttributesForMarkedText { return @[]; } // End of the NSTextInputClient protocol adoption. //-------------------------------------------------------------------------------------------------- /** * Generic input method. It is used to pass on keyboard input to Scintilla. The control itself only * handles shortcuts. The input is then forwarded to the Cocoa text input system, which in turn does * its own input handling (character composition via NSTextInputClient protocol): */ - (void) keyDown: (NSEvent *) theEvent { bool handled = false; if (mMarkedTextRange.length == 0) handled = mOwner.backend->KeyboardInput(theEvent); if (!handled) { NSArray *events = @[theEvent]; [self interpretKeyEvents: events]; } } //-------------------------------------------------------------------------------------------------- - (void) mouseDown: (NSEvent *) theEvent { mOwner.backend->MouseDown(theEvent); } //-------------------------------------------------------------------------------------------------- - (void) mouseDragged: (NSEvent *) theEvent { mOwner.backend->MouseMove(theEvent); } //-------------------------------------------------------------------------------------------------- - (void) mouseUp: (NSEvent *) theEvent { mOwner.backend->MouseUp(theEvent); } //-------------------------------------------------------------------------------------------------- - (void) mouseMoved: (NSEvent *) theEvent { mOwner.backend->MouseMove(theEvent); } //-------------------------------------------------------------------------------------------------- - (void) mouseEntered: (NSEvent *) theEvent { mOwner.backend->MouseEntered(theEvent); } //-------------------------------------------------------------------------------------------------- - (void) mouseExited: (NSEvent *) theEvent { mOwner.backend->MouseExited(theEvent); } //-------------------------------------------------------------------------------------------------- /** * Implementing scrollWheel makes scrolling work better even if just * calling super. * Mouse wheel with command key may magnify text if enabled. * Pinch gestures and key commands can also be used for magnification. */ - (void) scrollWheel: (NSEvent *) theEvent { #ifdef SCROLL_WHEEL_MAGNIFICATION if (([theEvent modifierFlags] & NSEventModifierFlagCommand) != 0) { mOwner.backend->MouseWheel(theEvent); return; } #endif [super scrollWheel: theEvent]; } //-------------------------------------------------------------------------------------------------- /** * Ensure scrolling is aligned to whole lines instead of starting part-way through a line */ - (NSRect) adjustScroll: (NSRect) proposedVisibleRect { if (!mOwner) return proposedVisibleRect; NSRect rc = proposedVisibleRect; // Snap to lines NSRect contentRect = self.bounds; if ((rc.origin.y > 0) && (NSMaxY(rc) < contentRect.size.height)) { // Only snap for positions inside the document - allow outside // for overshoot. long lineHeight = mOwner.backend->WndProc(SCI_TEXTHEIGHT, 0, 0); rc.origin.y = std::round(static_cast(rc.origin.y) / lineHeight) * lineHeight; } // Snap to whole points - on retina displays this avoids visual debris // when scrolling horizontally. if ((rc.origin.x > 0) && (NSMaxX(rc) < contentRect.size.width)) { // Only snap for positions inside the document - allow outside // for overshoot. rc.origin.x = std::round(static_cast(rc.origin.x)); } return rc; } //-------------------------------------------------------------------------------------------------- /** * The editor is getting the foreground control (the one getting the input focus). */ - (BOOL) becomeFirstResponder { mOwner.backend->WndProc(SCI_SETFOCUS, 1, 0); return YES; } //-------------------------------------------------------------------------------------------------- /** * The editor is losing the input focus. */ - (BOOL) resignFirstResponder { mOwner.backend->WndProc(SCI_SETFOCUS, 0, 0); return YES; } //-------------------------------------------------------------------------------------------------- /** * Implement NSDraggingSource. */ - (NSDragOperation) draggingSession: (NSDraggingSession *) session sourceOperationMaskForDraggingContext: (NSDraggingContext) context { #pragma unused(session) switch (context) { case NSDraggingContextOutsideApplication: return NSDragOperationCopy | NSDragOperationMove | NSDragOperationDelete; case NSDraggingContextWithinApplication: default: return NSDragOperationCopy | NSDragOperationMove | NSDragOperationDelete; } } - (void) draggingSession: (NSDraggingSession *) session movedToPoint: (NSPoint) screenPoint { #pragma unused(session, screenPoint) } - (void) draggingSession: (NSDraggingSession *) session endedAtPoint: (NSPoint) screenPoint operation: (NSDragOperation) operation { #pragma unused(session, screenPoint) if (operation == NSDragOperationDelete) { mOwner.backend->WndProc(SCI_CLEAR, 0, 0); } } /** * Implement NSDraggingDestination. */ //-------------------------------------------------------------------------------------------------- /** * Called when an external drag operation enters the view. */ - (NSDragOperation) draggingEntered: (id ) sender { return mOwner.backend->DraggingEntered(sender); } //-------------------------------------------------------------------------------------------------- /** * Called frequently during an external drag operation if we are the target. */ - (NSDragOperation) draggingUpdated: (id ) sender { return mOwner.backend->DraggingUpdated(sender); } //-------------------------------------------------------------------------------------------------- /** * Drag image left the view. Clean up if necessary. */ - (void) draggingExited: (id ) sender { mOwner.backend->DraggingExited(sender); } //-------------------------------------------------------------------------------------------------- - (BOOL) prepareForDragOperation: (id ) sender { #pragma unused(sender) return YES; } //-------------------------------------------------------------------------------------------------- - (BOOL) performDragOperation: (id ) sender { return mOwner.backend->PerformDragOperation(sender); } //-------------------------------------------------------------------------------------------------- /** * Drag operation is done. Notify editor. */ - (void) concludeDragOperation: (id ) sender { // Clean up is the same as if we are no longer the drag target. mOwner.backend->DraggingExited(sender); } //-------------------------------------------------------------------------------------------------- // NSResponder actions. - (void) selectAll: (id) sender { #pragma unused(sender) mOwner.backend->SelectAll(); } - (void) deleteBackward: (id) sender { #pragma unused(sender) mOwner.backend->DeleteBackward(); } - (void) cut: (id) sender { #pragma unused(sender) mOwner.backend->Cut(); } - (void) copy: (id) sender { #pragma unused(sender) mOwner.backend->Copy(); } - (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(); } - (void) redo: (id) sender { #pragma unused(sender) mOwner.backend->Redo(); } - (BOOL) canUndo { return mOwner.backend->CanUndo() && (mMarkedTextRange.location == NSNotFound); } - (BOOL) canRedo { return mOwner.backend->CanRedo(); } - (BOOL) validateUserInterfaceItem: (id ) anItem { SEL action = anItem.action; if (action==@selector(undo:)) { return [self canUndo]; } else if (action==@selector(redo:)) { return [self canRedo]; } else if (action==@selector(cut:) || action==@selector(copy:) || action==@selector(clear:)) { return mOwner.backend->HasSelection(); } else if (action==@selector(paste:)) { return mOwner.backend->CanPaste(); } return YES; } - (void) clear: (id) sender { [self deleteBackward: sender]; } - (BOOL) isEditable { return mOwner.backend->WndProc(SCI_GETREADONLY, 0, 0) == 0; } #pragma mark - NSAccessibility //-------------------------------------------------------------------------------------------------- // Adoption of NSAccessibility protocol. // NSAccessibility wants to pass ranges in UTF-16 code units, not bytes (like Scintilla) // or characters. // Needs more testing with non-ASCII and non-BMP text. // Needs to take account of folding and wraping. //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Text of the whole document as a string. */ - (id) accessibilityValue { const sptr_t length = [mOwner message: SCI_GETLENGTH]; return mOwner.backend->RangeTextAsString(NSMakeRange(0, length)); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Line of the caret. */ - (NSInteger) accessibilityInsertionPointLineNumber { const Sci::Position caret = [mOwner message: SCI_GETCURRENTPOS]; const NSRange rangeCharactersCaret = mOwner.backend->CharactersFromPositions(NSMakeRange(caret, 0)); return mOwner.backend->VisibleLineForIndex(rangeCharactersCaret.location); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Not implemented and not called by VoiceOver. */ - (NSRange) accessibilityRangeForPosition: (NSPoint) point { return NSMakeRange(0, 0); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Number of characters in the whole document. */ - (NSInteger) accessibilityNumberOfCharacters { sptr_t length = [mOwner message: SCI_GETLENGTH]; const NSRange posRange = mOwner.backend->CharactersFromPositions(NSMakeRange(length, 0)); return posRange.location; } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : The selection text as a string. */ - (NSString *) accessibilitySelectedText { const sptr_t positionBegin = [mOwner message: SCI_GETSELECTIONSTART]; const sptr_t positionEnd = [mOwner message: SCI_GETSELECTIONEND]; const NSRange posRangeSel = NSMakeRange(positionBegin, positionEnd-positionBegin); return mOwner.backend->RangeTextAsString(posRangeSel); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : The character range of the main selection. */ - (NSRange) accessibilitySelectedTextRange { const sptr_t positionBegin = [mOwner message: SCI_GETSELECTIONSTART]; const sptr_t positionEnd = [mOwner message: SCI_GETSELECTIONEND]; const NSRange posRangeSel = NSMakeRange(positionBegin, positionEnd-positionBegin); return mOwner.backend->CharactersFromPositions(posRangeSel); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : The setter for accessibilitySelectedTextRange. * This method is the only setter required for reasonable VoiceOver behaviour. */ - (void) setAccessibilitySelectedTextRange: (NSRange) range { NSRange rangePositions = mOwner.backend->PositionsFromCharacters(range); [mOwner message: SCI_SETSELECTION wParam: rangePositions.location lParam: NSMaxRange(rangePositions)]; } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Range of the glyph at a character index. * Currently doesn't try to handle composite characters. */ - (NSRange) accessibilityRangeForIndex: (NSInteger) index { sptr_t length = [mOwner message: SCI_GETLENGTH]; const NSRange rangeLength = mOwner.backend->CharactersFromPositions(NSMakeRange(length, 0)); NSRange rangePositions = NSMakeRange(length, 0); if (index < rangeLength.location) { rangePositions = mOwner.backend->PositionsFromCharacters(NSMakeRange(index, 1)); } return mOwner.backend->CharactersFromPositions(rangePositions); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : All the text ranges. * Currently only returns the main selection. */ - (NSArray *) accessibilitySelectedTextRanges { const sptr_t positionBegin = [mOwner message: SCI_GETSELECTIONSTART]; const sptr_t positionEnd = [mOwner message: SCI_GETSELECTIONEND]; const NSRange posRangeSel = NSMakeRange(positionBegin, positionEnd-positionBegin); NSRange rangeCharacters = mOwner.backend->CharactersFromPositions(posRangeSel); NSValue *valueRange = [NSValue valueWithRange: (NSRange)rangeCharacters]; return @[valueRange]; } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Character range currently visible. */ - (NSRange) accessibilityVisibleCharacterRange { const sptr_t lineTopVisible = [mOwner message: SCI_GETFIRSTVISIBLELINE]; const sptr_t lineTop = [mOwner message: SCI_DOCLINEFROMVISIBLE wParam: lineTopVisible]; const sptr_t lineEndVisible = lineTopVisible + [mOwner message: SCI_LINESONSCREEN] - 1; const sptr_t lineEnd = [mOwner message: SCI_DOCLINEFROMVISIBLE wParam: lineEndVisible]; const sptr_t posStartView = [mOwner message: SCI_POSITIONFROMLINE wParam: lineTop]; const sptr_t posEndView = [mOwner message: SCI_GETLINEENDPOSITION wParam: lineEnd]; const NSRange posRangeSel = NSMakeRange(posStartView, posEndView-posStartView); return mOwner.backend->CharactersFromPositions(posRangeSel); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Character range of a line. */ - (NSRange) accessibilityRangeForLine: (NSInteger) line { return mOwner.backend->RangeForVisibleLine(line); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Line number of a text position in characters. */ - (NSInteger) accessibilityLineForIndex: (NSInteger) index { return mOwner.backend->VisibleLineForIndex(index); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : A rectangle that covers a range which will be shown as the * VoiceOver cursor. * Producing a nice rectangle is a little tricky particularly when including new * lines. Needs to improve the case where parts of two lines are included. */ - (NSRect) accessibilityFrameForRange: (NSRange) range { const NSRect rectInView = mOwner.backend->FrameForRange(range); const NSRect rectInWindow = [self.superview.superview convertRect: rectInView toView: nil]; return [self.window convertRectToScreen: rectInWindow]; } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : A range of text as a string. */ - (NSString *) accessibilityStringForRange: (NSRange) range { const NSRange posRange = mOwner.backend->PositionsFromCharacters(range); return mOwner.backend->RangeTextAsString(posRange); } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : A range of text as an attributed string. * Currently no attributes are set. */ - (NSAttributedString *) accessibilityAttributedStringForRange: (NSRange) range { const NSRange posRange = mOwner.backend->PositionsFromCharacters(range); NSString *result = mOwner.backend->RangeTextAsString(posRange); return [[NSMutableAttributedString alloc] initWithString: result]; } //-------------------------------------------------------------------------------------------------- /** * NSAccessibility : Show the context menu at the caret. */ - (BOOL) accessibilityPerformShowMenu { const sptr_t caret = [mOwner message: SCI_GETCURRENTPOS]; NSRect rect; rect.origin.x = [mOwner message: SCI_POINTXFROMPOSITION wParam: 0 lParam: caret]; rect.origin.y = [mOwner message: SCI_POINTYFROMPOSITION wParam: 0 lParam: caret]; rect.origin.y += [mOwner message: SCI_TEXTHEIGHT wParam: 0 lParam: 0]; rect.size.width = 1.0; rect.size.height = 1.0; NSRect rectInWindow = [self.superview.superview convertRect: rect toView: nil]; NSPoint pt = rectInWindow.origin; NSEvent *event = [NSEvent mouseEventWithType: NSEventTypeRightMouseDown location: pt modifierFlags: 0 timestamp: 0 windowNumber: self.window.windowNumber context: nil eventNumber: 0 clickCount: 1 pressure: 0.0]; NSMenu *menu = mOwner.backend->CreateContextMenu(event); [NSMenu popUpContextMenu: menu withEvent: event forView: self]; return YES; } //-------------------------------------------------------------------------------------------------- @end //-------------------------------------------------------------------------------------------------- @implementation ScintillaView { // The back end is kind of a controller and model in one. // It uses the content view for display. Scintilla::ScintillaCocoa *mBackend; // This is the actual content to which the backend renders itself. SCIContentView *mContent; NSScrollView *scrollView; SCIMarginView *marginView; CGFloat zoomDelta; // Area to display additional controls (e.g. zoom info, caret position, status info). NSView *mInfoBar; BOOL mInfoBarAtTop; id __unsafe_unretained mDelegate; } @synthesize backend = mBackend; @synthesize delegate = mDelegate; @synthesize scrollView; /** * ScintillaView is a composite control made from an NSView and an embedded NSView that is * used as canvas for the output (by the backend, using its CGContext), plus other elements * (scrollers, info bar). */ //-------------------------------------------------------------------------------------------------- /** * Initialize custom cursor. */ + (void) initialize { if (self == [ScintillaView class]) { NSBundle *bundle = [NSBundle bundleForClass: [ScintillaView class]]; NSString *path = [bundle pathForResource: @"mac_cursor_busy" ofType: @"tiff" inDirectory: nil]; NSImage *image = [[NSImage alloc] initWithContentsOfFile: path]; waitCursor = [[NSCursor alloc] initWithImage: image hotSpot: NSMakePoint(2, 2)]; path = [bundle pathForResource: @"mac_cursor_flipped" ofType: @"tiff" inDirectory: nil]; image = [[NSImage alloc] initWithContentsOfFile: path]; reverseArrowCursor = [[NSCursor alloc] initWithImage: image hotSpot: NSMakePoint(15, 2)]; } } //-------------------------------------------------------------------------------------------------- /** * Specify the SCIContentView class. Can be overridden in a subclass to provide an SCIContentView subclass. */ + (Class) contentViewClass { return [SCIContentView class]; } //-------------------------------------------------------------------------------------------------- /** * Receives zoom messages, for example when a "pinch zoom" is performed on the trackpad. */ - (void) magnifyWithEvent: (NSEvent *) event { #if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5 zoomDelta += event.magnification * 10.0; if (std::abs(zoomDelta)>=1.0) { long zoomFactor = static_cast([self getGeneralProperty: SCI_GETZOOM] + zoomDelta); [self setGeneralProperty: SCI_SETZOOM parameter: zoomFactor value: 0]; zoomDelta = 0.0; } #endif } - (void) beginGestureWithEvent: (NSEvent *) event { // Scintilla is only interested in this event as the starft of a zoom #pragma unused(event) zoomDelta = 0.0; } //-------------------------------------------------------------------------------------------------- /** * Sends a new notification of the given type to the default notification center. */ - (void) sendNotification: (NSString *) notificationName { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center postNotificationName: notificationName object: self]; } //-------------------------------------------------------------------------------------------------- /** * Called by a connected component (usually the info bar) if something changed there. * * @param type The type of the notification. * @param message Carries the new status message if the type is a status message change. * @param location Carries the new location (e.g. caret) if the type is a caret change or similar type. * @param value Carries the new zoom value if the type is a zoom change. */ - (void) notify: (NotificationType) type message: (NSString *) message location: (NSPoint) location value: (float) value { // These parameters are just to conform to the protocol #pragma unused(message) #pragma unused(location) switch (type) { case IBNZoomChanged: { // Compute point increase/decrease based on default font size. long fontSize = [self getGeneralProperty: SCI_STYLEGETSIZE parameter: STYLE_DEFAULT]; int zoom = (int)(fontSize * (value - 1)); [self setGeneralProperty: SCI_SETZOOM value: zoom]; break; } default: break; }; } //-------------------------------------------------------------------------------------------------- - (void) setCallback: (id ) callback { // Not used. Only here to satisfy protocol. #pragma unused(callback) } //-------------------------------------------------------------------------------------------------- /** * Prevents drawing of the inner view to avoid flickering when doing many visual updates * (like clearing all marks and setting new ones etc.). */ - (void) suspendDrawing: (BOOL) suspend { if (suspend) [self.window disableFlushWindow]; else [self.window enableFlushWindow]; } //-------------------------------------------------------------------------------------------------- /** * Method receives notifications from Scintilla (e.g. for handling clicks on the * folder margin or changes in the editor). * A delegate can be set to receive all notifications. If set no handling takes place here, except * for action pertaining to internal stuff (like the info bar). */ - (void) notification: (SCNotification *) scn { // Parent notification. Details are passed as SCNotification structure. if (mDelegate != nil) { [mDelegate notification: scn]; if (scn->nmhdr.code != SCN_ZOOM && scn->nmhdr.code != SCN_UPDATEUI) return; } switch (scn->nmhdr.code) { case SCN_MARGINCLICK: { if (scn->margin == 2) { // Click on the folder margin. Toggle the current line if possible. long line = [self getGeneralProperty: SCI_LINEFROMPOSITION parameter: scn->position]; [self setGeneralProperty: SCI_TOGGLEFOLD value: line]; } break; }; case SCN_MODIFIED: { // Decide depending on the modification type what to do. // There can be more than one modification carried by one notification. if (scn->modificationType & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT)) [self sendNotification: NSTextDidChangeNotification]; break; } case SCN_ZOOM: { // A zoom change happened. Notify info bar if there is one. float zoom = [self getGeneralProperty: SCI_GETZOOM parameter: 0]; long fontSize = [self getGeneralProperty: SCI_STYLEGETSIZE parameter: STYLE_DEFAULT]; float factor = (zoom / fontSize) + 1; [mInfoBar notify: IBNZoomChanged message: nil location: NSZeroPoint value: factor]; break; } case SCN_UPDATEUI: { // Triggered whenever changes in the UI state need to be reflected. // These can be: caret changes, selection changes etc. NSPoint caretPosition = mBackend->GetCaretPosition(); [mInfoBar notify: IBNCaretChanged message: nil location: caretPosition value: 0]; [self sendNotification: SCIUpdateUINotification]; if (scn->updated & (SC_UPDATE_SELECTION | SC_UPDATE_CONTENT)) { [self sendNotification: NSTextViewDidChangeSelectionNotification]; } break; } case SCN_FOCUSOUT: [self sendNotification: NSTextDidEndEditingNotification]; break; case SCN_FOCUSIN: // Nothing to do for now. break; } } //-------------------------------------------------------------------------------------------------- /** * Setup a special indicator used in the editor to provide visual feedback for * input composition, depending on language, keyboard etc. */ - (void) updateIndicatorIME { [self setColorProperty: SCI_INDICSETFORE parameter: INDICATOR_IME fromHTML: @"#FF0000"]; const bool drawInBackground = [self message: SCI_GETPHASESDRAW] != 0; [self setGeneralProperty: SCI_INDICSETUNDER parameter: INDICATOR_IME value: drawInBackground]; [self setGeneralProperty: SCI_INDICSETSTYLE parameter: INDICATOR_IME value: INDIC_PLAIN]; [self setGeneralProperty: SCI_INDICSETALPHA parameter: INDICATOR_IME value: 100]; } //-------------------------------------------------------------------------------------------------- /** * Initialization of the view. Used to setup a few other things we need. */ - (instancetype) initWithFrame: (NSRect) frame { self = [super initWithFrame: frame]; if (self) { mContent = [[[[self class] contentViewClass] alloc] initWithFrame: NSZeroRect]; mContent.owner = self; // Initialize the scrollers but don't show them yet. // Pick an arbitrary size, just to make NSScroller selecting the proper scroller direction // (horizontal or vertical). NSRect scrollerRect = NSMakeRect(0, 0, 100, 10); scrollView = (NSScrollView *)[[SCIScrollView alloc] initWithFrame: scrollerRect]; #if defined(MAC_OS_X_VERSION_10_14) // Let SCIScrollView account for other subviews such as vertical ruler by turning off // automaticallyAdjustsContentInsets. if (std::floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_13) { scrollView.contentView.automaticallyAdjustsContentInsets = NO; scrollView.contentView.contentInsets = NSEdgeInsetsMake(0., 0., 0., 0.); } #endif scrollView.documentView = mContent; [scrollView setHasVerticalScroller: YES]; [scrollView setHasHorizontalScroller: YES]; scrollView.autoresizingMask = NSViewWidthSizable|NSViewHeightSizable; //[scrollView setScrollerStyle:NSScrollerStyleLegacy]; //[scrollView setScrollerKnobStyle:NSScrollerKnobStyleDark]; //[scrollView setHorizontalScrollElasticity:NSScrollElasticityNone]; [self addSubview: scrollView]; marginView = [[SCIMarginView alloc] initWithScrollView: scrollView]; marginView.owner = self; marginView.ruleThickness = marginView.requiredThickness; scrollView.verticalRulerView = marginView; [scrollView setHasHorizontalRuler: NO]; [scrollView setHasVerticalRuler: YES]; [scrollView setRulersVisible: YES]; mBackend = new ScintillaCocoa(self, mContent, marginView); // Establish a connection from the back end to this container so we can handle situations // which require our attention. mBackend->SetDelegate(self); [self updateIndicatorIME]; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver: self selector: @selector(applicationDidResignActive:) name: NSApplicationDidResignActiveNotification object: nil]; [center addObserver: self selector: @selector(applicationDidBecomeActive:) name: NSApplicationDidBecomeActiveNotification object: nil]; [center addObserver: self selector: @selector(windowWillMove:) name: NSWindowWillMoveNotification object: self.window]; [scrollView.contentView setPostsBoundsChangedNotifications: YES]; [center addObserver: self selector: @selector(scrollerAction:) name: NSViewBoundsDidChangeNotification object: scrollView.contentView]; } return self; } //-------------------------------------------------------------------------------------------------- - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver: self]; mBackend->Finalise(); delete mBackend; mBackend = NULL; mContent.owner = nil; [marginView setClientView: nil]; [scrollView removeFromSuperview]; } //-------------------------------------------------------------------------------------------------- - (void) applicationDidResignActive: (NSNotification *) note { #pragma unused(note) mBackend->ActiveStateChanged(false); } //-------------------------------------------------------------------------------------------------- - (void) applicationDidBecomeActive: (NSNotification *) note { #pragma unused(note) mBackend->ActiveStateChanged(true); } //-------------------------------------------------------------------------------------------------- - (void) windowWillMove: (NSNotification *) note { #pragma unused(note) mBackend->WindowWillMove(); } //-------------------------------------------------------------------------------------------------- - (void) viewDidMoveToWindow { [super viewDidMoveToWindow]; [self positionSubViews]; // Enable also mouse move events for our window (and so this view). [self.window setAcceptsMouseMovedEvents: YES]; } //-------------------------------------------------------------------------------------------------- /** * Used to position and size the parts of the editor (content, scrollers, info bar). */ - (void) positionSubViews { CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize: NSControlSizeRegular scrollerStyle: NSScrollerStyleLegacy]; NSSize size = self.frame.size; NSRect barFrame = {{0, size.height - scrollerWidth}, {size.width, scrollerWidth}}; BOOL infoBarVisible = mInfoBar != nil && !mInfoBar.hidden; // Horizontal offset of the content. Almost always 0 unless the vertical scroller // is on the left side. CGFloat contentX = 0; NSRect scrollRect = {{contentX, 0}, {size.width, size.height}}; // Info bar frame. if (infoBarVisible) { scrollRect.size.height -= scrollerWidth; // Initial value already is as if the bar is at top. if (!mInfoBarAtTop) { scrollRect.origin.y += scrollerWidth; barFrame.origin.y = 0; } } if (!NSEqualRects(scrollView.frame, scrollRect)) { scrollView.frame = scrollRect; } if (infoBarVisible) mInfoBar.frame = barFrame; } //-------------------------------------------------------------------------------------------------- /** * Set the width of the margin. */ - (void) setMarginWidth: (int) width { if (marginView.ruleThickness != width) { marginView.marginWidth = width; marginView.ruleThickness = marginView.requiredThickness; } } //-------------------------------------------------------------------------------------------------- /** * Triggered by one of the scrollers when it gets manipulated by the user. Notify the backend * about the change. */ - (void) scrollerAction: (id) sender { #pragma unused(sender) mBackend->UpdateForScroll(); } //-------------------------------------------------------------------------------------------------- /** * Used to reposition our content depending on the size of the view. */ - (void) setFrame: (NSRect) newFrame { NSRect previousFrame = self.frame; super.frame = newFrame; [self positionSubViews]; if (!NSEqualRects(previousFrame, newFrame)) { mBackend->Resize(); } } //-------------------------------------------------------------------------------------------------- /** * Getter for the currently selected text in raw form (no formatting information included). * If there is no text available an empty string is returned. */ - (NSString *) selectedString { NSString *result = @""; const long length = mBackend->WndProc(SCI_GETSELTEXT, 0, 0); if (length > 0) { std::string buffer(length + 1, '\0'); try { mBackend->WndProc(SCI_GETSELTEXT, length + 1, (sptr_t) &buffer[0]); result = @(buffer.c_str()); } catch (...) { } } return result; } //-------------------------------------------------------------------------------------------------- /** * Delete a range from the document. */ - (void) deleteRange: (NSRange) aRange { if (aRange.length > 0) { NSRange posRange = mBackend->PositionsFromCharacters(aRange); [self message: SCI_DELETERANGE wParam: posRange.location lParam: posRange.length]; } } //-------------------------------------------------------------------------------------------------- /** * Getter for the current text in raw form (no formatting information included). * If there is no text available an empty string is returned. */ - (NSString *) string { NSString *result = @""; const long length = mBackend->WndProc(SCI_GETLENGTH, 0, 0); if (length > 0) { std::string buffer(length + 1, '\0'); try { mBackend->WndProc(SCI_GETTEXT, length + 1, (sptr_t) &buffer[0]); result = @(buffer.c_str()); } catch (...) { } } return result; } //-------------------------------------------------------------------------------------------------- /** * Setter for the current text (no formatting included). */ - (void) setString: (NSString *) aString { const char *text = aString.UTF8String; mBackend->WndProc(SCI_SETTEXT, 0, (long) text); } //-------------------------------------------------------------------------------------------------- - (void) insertString: (NSString *) aString atOffset: (int) offset { const char *text = aString.UTF8String; mBackend->WndProc(SCI_ADDTEXT, offset, (long) text); } //-------------------------------------------------------------------------------------------------- - (void) setEditable: (BOOL) editable { mBackend->WndProc(SCI_SETREADONLY, editable ? 0 : 1, 0); } //-------------------------------------------------------------------------------------------------- - (BOOL) isEditable { return mBackend->WndProc(SCI_GETREADONLY, 0, 0) == 0; } //-------------------------------------------------------------------------------------------------- - (SCIContentView *) content { return mContent; } //-------------------------------------------------------------------------------------------------- - (void) updateMarginCursors { [self.window invalidateCursorRectsForView: marginView]; } //-------------------------------------------------------------------------------------------------- /** * Direct call into the backend to allow uninterpreted access to it. The values to be passed in and * the result heavily depend on the message that is used for the call. Refer to the Scintilla * documentation to learn what can be used here. */ + (sptr_t) directCall: (ScintillaView *) sender message: (unsigned int) message wParam: (uptr_t) wParam lParam: (sptr_t) lParam { return ScintillaCocoa::DirectFunction( reinterpret_cast(sender->mBackend), message, wParam, lParam); } - (sptr_t) message: (unsigned int) message wParam: (uptr_t) wParam lParam: (sptr_t) lParam { return mBackend->WndProc(message, wParam, lParam); } - (sptr_t) message: (unsigned int) message wParam: (uptr_t) wParam { return mBackend->WndProc(message, wParam, 0); } - (sptr_t) message: (unsigned int) message { return mBackend->WndProc(message, 0, 0); } //-------------------------------------------------------------------------------------------------- /** * This is a helper method to set properties in the backend, with native parameters. * * @param property Main property like SCI_STYLESETFORE for which a value is to be set. * @param parameter Additional info for this property like a parameter or index. * @param value The actual value. It depends on the property what this parameter means. */ - (void) setGeneralProperty: (int) property parameter: (long) parameter value: (long) value { mBackend->WndProc(property, parameter, value); } //-------------------------------------------------------------------------------------------------- /** * A simplified version for setting properties which only require one parameter. * * @param property Main property like SCI_STYLESETFORE for which a value is to be set. * @param value The actual value. It depends on the property what this parameter means. */ - (void) setGeneralProperty: (int) property value: (long) value { mBackend->WndProc(property, value, 0); } //-------------------------------------------------------------------------------------------------- /** * This is a helper method to get a property in the backend, with native parameters. * * @param property Main property like SCI_STYLESETFORE for which a value is to get. * @param parameter Additional info for this property like a parameter or index. * @param extra Yet another parameter if needed. * @result A generic value which must be interpreted depending on the property queried. */ - (long) getGeneralProperty: (int) property parameter: (long) parameter extra: (long) extra { return mBackend->WndProc(property, parameter, extra); } //-------------------------------------------------------------------------------------------------- /** * Convenience function to avoid unneeded extra parameter. */ - (long) getGeneralProperty: (int) property parameter: (long) parameter { return mBackend->WndProc(property, parameter, 0); } //-------------------------------------------------------------------------------------------------- /** * Convenience function to avoid unneeded parameters. */ - (long) getGeneralProperty: (int) property { return mBackend->WndProc(property, 0, 0); } //-------------------------------------------------------------------------------------------------- /** * Use this variant if you have to pass in a reference to something (e.g. a text range). */ - (long) getGeneralProperty: (int) property ref: (const void *) ref { return mBackend->WndProc(property, 0, (sptr_t) ref); } //-------------------------------------------------------------------------------------------------- /** * Specialized property setter for colors. */ - (void) setColorProperty: (int) property parameter: (long) parameter value: (NSColor *) value { if (value.colorSpaceName != NSDeviceRGBColorSpace) value = [value colorUsingColorSpaceName: NSDeviceRGBColorSpace]; long red = static_cast(value.redComponent * 255); long green = static_cast(value.greenComponent * 255); long blue = static_cast(value.blueComponent * 255); long color = (blue << 16) + (green << 8) + red; mBackend->WndProc(property, parameter, color); } //-------------------------------------------------------------------------------------------------- /** * Another color property setting, which allows to specify the color as string like in HTML * documents (i.e. with leading # and either 3 hex digits or 6). */ - (void) setColorProperty: (int) property parameter: (long) parameter fromHTML: (NSString *) fromHTML { if (fromHTML.length > 3 && [fromHTML characterAtIndex: 0] == '#') { bool longVersion = fromHTML.length > 6; int index = 1; char value[3] = {0, 0, 0}; value[0] = static_cast([fromHTML characterAtIndex: index++]); if (longVersion) value[1] = static_cast([fromHTML characterAtIndex: index++]); else value[1] = value[0]; unsigned rawRed; [[NSScanner scannerWithString: @(value)] scanHexInt: &rawRed]; value[0] = static_cast([fromHTML characterAtIndex: index++]); if (longVersion) value[1] = static_cast([fromHTML characterAtIndex: index++]); else value[1] = value[0]; unsigned rawGreen; [[NSScanner scannerWithString: @(value)] scanHexInt: &rawGreen]; value[0] = static_cast([fromHTML characterAtIndex: index++]); if (longVersion) value[1] = static_cast([fromHTML characterAtIndex: index++]); else value[1] = value[0]; unsigned rawBlue; [[NSScanner scannerWithString: @(value)] scanHexInt: &rawBlue]; long color = (rawBlue << 16) + (rawGreen << 8) + rawRed; mBackend->WndProc(property, parameter, color); } } //-------------------------------------------------------------------------------------------------- /** * Specialized property getter for colors. */ - (NSColor *) getColorProperty: (int) property parameter: (long) parameter { long color = mBackend->WndProc(property, parameter, 0); CGFloat red = (color & 0xFF) / 255.0; CGFloat green = ((color >> 8) & 0xFF) / 255.0; CGFloat blue = ((color >> 16) & 0xFF) / 255.0; NSColor *result = [NSColor colorWithDeviceRed: red green: green blue: blue alpha: 1]; return result; } //-------------------------------------------------------------------------------------------------- /** * Specialized property setter for references (pointers, addresses). */ - (void) setReferenceProperty: (int) property parameter: (long) parameter value: (const void *) value { mBackend->WndProc(property, parameter, (sptr_t) value); } //-------------------------------------------------------------------------------------------------- /** * Specialized property getter for references (pointers, addresses). */ - (const void *) getReferenceProperty: (int) property parameter: (long) parameter { return (const void *) mBackend->WndProc(property, parameter, 0); } //-------------------------------------------------------------------------------------------------- /** * Specialized property setter for string values. */ - (void) setStringProperty: (int) property parameter: (long) parameter value: (NSString *) value { const char *rawValue = value.UTF8String; mBackend->WndProc(property, parameter, (sptr_t) rawValue); } //-------------------------------------------------------------------------------------------------- /** * Specialized property getter for string values. */ - (NSString *) getStringProperty: (int) property parameter: (long) parameter { const char *rawValue = (const char *) mBackend->WndProc(property, parameter, 0); return @(rawValue); } //-------------------------------------------------------------------------------------------------- /** * Specialized property setter for lexer properties, which are commonly passed as strings. */ - (void) setLexerProperty: (NSString *) name value: (NSString *) value { const char *rawName = name.UTF8String; const char *rawValue = value.UTF8String; mBackend->WndProc(SCI_SETPROPERTY, (sptr_t) rawName, (sptr_t) rawValue); } //-------------------------------------------------------------------------------------------------- /** * Specialized property getter for references (pointers, addresses). */ - (NSString *) getLexerProperty: (NSString *) name { const char *rawName = name.UTF8String; const char *result = (const char *) mBackend->WndProc(SCI_SETPROPERTY, (sptr_t) rawName, 0); return @(result); } //-------------------------------------------------------------------------------------------------- /** * Sets the notification callback */ - (void) registerNotifyCallback: (intptr_t) windowid value: (SciNotifyFunc) callback { mBackend->RegisterNotifyCallback(windowid, callback); } //-------------------------------------------------------------------------------------------------- /** * Sets the new control which is displayed as info bar at the top or bottom of the editor. * Set newBar to nil if you want to hide the bar again. * The info bar's height is set to the height of the scrollbar. */ - (void) setInfoBar: (NSView *) newBar top: (BOOL) top { if (mInfoBar != newBar) { [mInfoBar removeFromSuperview]; mInfoBar = newBar; mInfoBarAtTop = top; if (mInfoBar != nil) { [self addSubview: mInfoBar]; [mInfoBar setCallback: self]; } [self positionSubViews]; } } //-------------------------------------------------------------------------------------------------- /** * Sets the edit's info bar status message. This call only has an effect if there is an info bar. */ - (void) setStatusText: (NSString *) text { if (mInfoBar != nil) [mInfoBar notify: IBNStatusChanged message: text location: NSZeroPoint value: 0]; } //-------------------------------------------------------------------------------------------------- - (NSRange) selectedRange { return [mContent selectedRange]; } //-------------------------------------------------------------------------------------------------- /** * Return the main selection as an NSRange of positions (not characters). * Unlike selectedRange, this can return empty ranges inside the document. */ - (NSRange) selectedRangePositions { const sptr_t positionBegin = [self message: SCI_GETSELECTIONSTART]; const sptr_t positionEnd = [self message: SCI_GETSELECTIONEND]; return NSMakeRange(positionBegin, positionEnd-positionBegin); } //-------------------------------------------------------------------------------------------------- - (void) insertText: (id) aString { if ([aString isKindOfClass: [NSString class]]) mBackend->InsertText(aString, EditModel::CharacterSource::directInput); else if ([aString isKindOfClass: [NSAttributedString class]]) mBackend->InsertText([aString string], EditModel::CharacterSource::directInput); } //-------------------------------------------------------------------------------------------------- /** * For backwards compatibility. */ - (BOOL) findAndHighlightText: (NSString *) searchText matchCase: (BOOL) matchCase wholeWord: (BOOL) wholeWord scrollTo: (BOOL) scrollTo wrap: (BOOL) wrap { return [self findAndHighlightText: searchText matchCase: matchCase wholeWord: wholeWord scrollTo: scrollTo wrap: wrap backwards: NO]; } //-------------------------------------------------------------------------------------------------- /** * Searches and marks the first occurrence of the given text and optionally scrolls it into view. * * @result YES if something was found, NO otherwise. */ - (BOOL) findAndHighlightText: (NSString *) searchText matchCase: (BOOL) matchCase wholeWord: (BOOL) wholeWord scrollTo: (BOOL) scrollTo wrap: (BOOL) wrap backwards: (BOOL) backwards { int searchFlags= 0; if (matchCase) searchFlags |= SCFIND_MATCHCASE; if (wholeWord) searchFlags |= SCFIND_WHOLEWORD; long selectionStart = [self getGeneralProperty: SCI_GETSELECTIONSTART parameter: 0]; long selectionEnd = [self getGeneralProperty: SCI_GETSELECTIONEND parameter: 0]; // Sets the start point for the coming search to the beginning of the current selection. // For forward searches we have therefore to set the selection start to the current selection end // for proper incremental search. This does not harm as we either get a new selection if something // is found or the previous selection is restored. if (!backwards) [self getGeneralProperty: SCI_SETSELECTIONSTART parameter: selectionEnd]; [self setGeneralProperty: SCI_SEARCHANCHOR value: 0]; sptr_t result; const char *textToSearch = searchText.UTF8String; // The following call will also set the selection if something was found. if (backwards) { result = [ScintillaView directCall: self message: SCI_SEARCHPREV wParam: searchFlags lParam: (sptr_t) textToSearch]; if (result < 0 && wrap) { // Try again from the end of the document if nothing could be found so far and // wrapped search is set. [self getGeneralProperty: SCI_SETSELECTIONSTART parameter: [self getGeneralProperty: SCI_GETTEXTLENGTH parameter: 0]]; [self setGeneralProperty: SCI_SEARCHANCHOR value: 0]; result = [ScintillaView directCall: self message: SCI_SEARCHNEXT wParam: searchFlags lParam: (sptr_t) textToSearch]; } } else { result = [ScintillaView directCall: self message: SCI_SEARCHNEXT wParam: searchFlags lParam: (sptr_t) textToSearch]; if (result < 0 && wrap) { // Try again from the start of the document if nothing could be found so far and // wrapped search is set. [self getGeneralProperty: SCI_SETSELECTIONSTART parameter: 0]; [self setGeneralProperty: SCI_SEARCHANCHOR value: 0]; result = [ScintillaView directCall: self message: SCI_SEARCHNEXT wParam: searchFlags lParam: (sptr_t) textToSearch]; } } if (result >= 0) { if (scrollTo) [self setGeneralProperty: SCI_SCROLLCARET value: 0]; } else { // Restore the former selection if we did not find anything. [self setGeneralProperty: SCI_SETSELECTIONSTART value: selectionStart]; [self setGeneralProperty: SCI_SETSELECTIONEND value: selectionEnd]; } return (result >= 0) ? YES : NO; } //-------------------------------------------------------------------------------------------------- /** * Searches the given text and replaces * * @result Number of entries replaced, 0 if none. */ - (int) findAndReplaceText: (NSString *) searchText byText: (NSString *) newText matchCase: (BOOL) matchCase wholeWord: (BOOL) wholeWord doAll: (BOOL) doAll { // The current position is where we start searching for single occurrences. Otherwise we start at // the beginning of the document. long startPosition; if (doAll) startPosition = 0; // Start at the beginning of the text if we replace all occurrences. else // For a single replacement we start at the current caret position. startPosition = [self getGeneralProperty: SCI_GETCURRENTPOS]; long endPosition = [self getGeneralProperty: SCI_GETTEXTLENGTH]; int searchFlags= 0; if (matchCase) searchFlags |= SCFIND_MATCHCASE; if (wholeWord) searchFlags |= SCFIND_WHOLEWORD; [self setGeneralProperty: SCI_SETSEARCHFLAGS value: searchFlags]; [self setGeneralProperty: SCI_SETTARGETSTART value: startPosition]; [self setGeneralProperty: SCI_SETTARGETEND value: endPosition]; const char *textToSearch = searchText.UTF8String; long sourceLength = strlen(textToSearch); // Length in bytes. const char *replacement = newText.UTF8String; long targetLength = strlen(replacement); // Length in bytes. sptr_t result; int replaceCount = 0; if (doAll) { while (true) { result = [ScintillaView directCall: self message: SCI_SEARCHINTARGET wParam: sourceLength lParam: (sptr_t) textToSearch]; if (result < 0) break; replaceCount++; [ScintillaView directCall: self message: SCI_REPLACETARGET wParam: targetLength lParam: (sptr_t) replacement]; // The replacement changes the target range to the replaced text. Continue after that till the end. // The text length might be changed by the replacement so make sure the target end is the actual // text end. [self setGeneralProperty: SCI_SETTARGETSTART value: [self getGeneralProperty: SCI_GETTARGETEND]]; [self setGeneralProperty: SCI_SETTARGETEND value: [self getGeneralProperty: SCI_GETTEXTLENGTH]]; } } else { result = [ScintillaView directCall: self message: SCI_SEARCHINTARGET wParam: sourceLength lParam: (sptr_t) textToSearch]; replaceCount = (result < 0) ? 0 : 1; if (replaceCount > 0) { [ScintillaView directCall: self message: SCI_REPLACETARGET wParam: targetLength lParam: (sptr_t) replacement]; // For a single replace we set the new selection to the replaced text. [self setGeneralProperty: SCI_SETSELECTIONSTART value: [self getGeneralProperty: SCI_GETTARGETSTART]]; [self setGeneralProperty: SCI_SETSELECTIONEND value: [self getGeneralProperty: SCI_GETTARGETEND]]; } } return replaceCount; } //-------------------------------------------------------------------------------------------------- - (void) setFontName: (NSString *) font size: (int) size bold: (BOOL) bold italic: (BOOL) italic { for (int i = 0; i < 128; i++) { [self setGeneralProperty: SCI_STYLESETFONT parameter: i value: (sptr_t)font.UTF8String]; [self setGeneralProperty: SCI_STYLESETSIZE parameter: i value: size]; [self setGeneralProperty: SCI_STYLESETBOLD parameter: i value: bold]; [self setGeneralProperty: SCI_STYLESETITALIC parameter: i value: italic]; } } //-------------------------------------------------------------------------------------------------- @end