/** * Scintilla source code edit control * @file ScintillaCocoa.mm - Cocoa subclass of ScintillaBase * * Written by Mike Lischke * * Loosely based on ScintillaMacOSX.cxx. * Copyright 2003 by Evan Jones * Based on ScintillaGTK.cxx Copyright 1998-2002 by Neil Hodgson * The License.txt file describes the conditions under which this software may be distributed. * * Copyright (c) 2009, 2010 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 #include #import #if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5 #import #endif #import #import #import "ScintillaTypes.h" #import "ScintillaMessages.h" #import "ScintillaStructures.h" #import "Debugging.h" #import "Geometry.h" #import "Platform.h" #import "ScintillaView.h" #import "ScintillaCocoa.h" #import "PlatCocoa.h" using namespace Scintilla; using namespace Scintilla::Internal; NSString *ScintillaRecPboardType = @"com.scintilla.utf16-plain-text.rectangular"; //-------------------------------------------------------------------------------------------------- // Define keyboard shortcuts (equivalents) the Mac way. #define SCI_CMD ( SCI_CTRL) #define SCI_SCMD ( SCI_CMD | SCI_SHIFT) #define SCI_SMETA ( SCI_META | SCI_SHIFT) namespace { constexpr Keys Key(char ch) { return static_cast(ch); } } static const KeyToCommand macMapDefault[] = { // macOS specific {Keys::Down, SCI_CTRL, Message::DocumentEnd}, {Keys::Down, SCI_CSHIFT, Message::DocumentEndExtend}, {Keys::Up, SCI_CTRL, Message::DocumentStart}, {Keys::Up, SCI_CSHIFT, Message::DocumentStartExtend}, {Keys::Left, SCI_CTRL, Message::VCHome}, {Keys::Left, SCI_CSHIFT, Message::VCHomeExtend}, {Keys::Right, SCI_CTRL, Message::LineEnd}, {Keys::Right, SCI_CSHIFT, Message::LineEndExtend}, // Similar to Windows and GTK+ // Where equivalent clashes with macOS standard, use Meta instead {Keys::Down, SCI_NORM, Message::LineDown}, {Keys::Down, SCI_SHIFT, Message::LineDownExtend}, {Keys::Down, SCI_META, Message::LineScrollDown}, {Keys::Down, SCI_ASHIFT, Message::LineDownRectExtend}, {Keys::Up, SCI_NORM, Message::LineUp}, {Keys::Up, SCI_SHIFT, Message::LineUpExtend}, {Keys::Up, SCI_META, Message::LineScrollUp}, {Keys::Up, SCI_ASHIFT, Message::LineUpRectExtend}, {Key('['), SCI_CTRL, Message::ParaUp}, {Key('['), SCI_CSHIFT, Message::ParaUpExtend}, {Key(']'), SCI_CTRL, Message::ParaDown}, {Key(']'), SCI_CSHIFT, Message::ParaDownExtend}, {Keys::Left, SCI_NORM, Message::CharLeft}, {Keys::Left, SCI_SHIFT, Message::CharLeftExtend}, {Keys::Left, SCI_ALT, Message::WordLeft}, {Keys::Left, SCI_META, Message::WordLeft}, {Keys::Left, SCI_SMETA, Message::WordLeftExtend}, {Keys::Left, SCI_ASHIFT, Message::CharLeftRectExtend}, {Keys::Right, SCI_NORM, Message::CharRight}, {Keys::Right, SCI_SHIFT, Message::CharRightExtend}, {Keys::Right, SCI_ALT, Message::WordRight}, {Keys::Right, SCI_META, Message::WordRight}, {Keys::Right, SCI_SMETA, Message::WordRightExtend}, {Keys::Right, SCI_ASHIFT, Message::CharRightRectExtend}, {Key('/'), SCI_CTRL, Message::WordPartLeft}, {Key('/'), SCI_CSHIFT, Message::WordPartLeftExtend}, {Key('\\'), SCI_CTRL, Message::WordPartRight}, {Key('\\'), SCI_CSHIFT, Message::WordPartRightExtend}, {Keys::Home, SCI_NORM, Message::VCHome}, {Keys::Home, SCI_SHIFT, Message::VCHomeExtend}, {Keys::Home, SCI_CTRL, Message::DocumentStart}, {Keys::Home, SCI_CSHIFT, Message::DocumentStartExtend}, {Keys::Home, SCI_ALT, Message::HomeDisplay}, {Keys::Home, SCI_ASHIFT, Message::VCHomeRectExtend}, {Keys::End, SCI_NORM, Message::LineEnd}, {Keys::End, SCI_SHIFT, Message::LineEndExtend}, {Keys::End, SCI_CTRL, Message::DocumentEnd}, {Keys::End, SCI_CSHIFT, Message::DocumentEndExtend}, {Keys::End, SCI_ALT, Message::LineEndDisplay}, {Keys::End, SCI_ASHIFT, Message::LineEndRectExtend}, {Keys::Prior, SCI_NORM, Message::PageUp}, {Keys::Prior, SCI_SHIFT, Message::PageUpExtend}, {Keys::Prior, SCI_ASHIFT, Message::PageUpRectExtend}, {Keys::Next, SCI_NORM, Message::PageDown}, {Keys::Next, SCI_SHIFT, Message::PageDownExtend}, {Keys::Next, SCI_ASHIFT, Message::PageDownRectExtend}, {Keys::Delete, SCI_NORM, Message::Clear}, {Keys::Delete, SCI_SHIFT, Message::Cut}, {Keys::Delete, SCI_CTRL, Message::DelWordRight}, {Keys::Delete, SCI_CSHIFT, Message::DelLineRight}, {Keys::Insert, SCI_NORM, Message::EditToggleOvertype}, {Keys::Insert, SCI_SHIFT, Message::Paste}, {Keys::Insert, SCI_CTRL, Message::Copy}, {Keys::Escape, SCI_NORM, Message::Cancel}, {Keys::Back, SCI_NORM, Message::DeleteBack}, {Keys::Back, SCI_SHIFT, Message::DeleteBack}, {Keys::Back, SCI_CTRL, Message::DelWordLeft}, {Keys::Back, SCI_ALT, Message::DelWordLeft}, {Keys::Back, SCI_CSHIFT, Message::DelLineLeft}, {Key('z'), SCI_CMD, Message::Undo}, {Key('z'), SCI_SCMD, Message::Redo}, {Key('x'), SCI_CMD, Message::Cut}, {Key('c'), SCI_CMD, Message::Copy}, {Key('v'), SCI_CMD, Message::Paste}, {Key('a'), SCI_CMD, Message::SelectAll}, {Keys::Tab, SCI_NORM, Message::Tab}, {Keys::Tab, SCI_SHIFT, Message::BackTab}, {Keys::Return, SCI_NORM, Message::NewLine}, {Keys::Return, SCI_SHIFT, Message::NewLine}, {Keys::Add, SCI_CMD, Message::ZoomIn}, {Keys::Subtract, SCI_CMD, Message::ZoomOut}, {Keys::Divide, SCI_CMD, Message::SetZoom}, {Key('l'), SCI_CMD, Message::LineCut}, {Key('l'), SCI_CSHIFT, Message::LineDelete}, {Key('t'), SCI_CSHIFT, Message::LineCopy}, {Key('t'), SCI_CTRL, Message::LineTranspose}, {Key('d'), SCI_CTRL, Message::SelectionDuplicate}, {Key('u'), SCI_CTRL, Message::LowerCase}, {Key('u'), SCI_CSHIFT, Message::UpperCase}, {Key(0), KeyMod::Norm, static_cast(0)}, }; //-------------------------------------------------------------------------------------------------- #if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5 // Only implement FindHighlightLayer on macOS 10.6+ /** * Class to display the animated gold roundrect used on macOS for matches. */ @interface FindHighlightLayer : CAGradientLayer { @private NSString *sFind; long positionFind; BOOL retaining; CGFloat widthText; CGFloat heightLine; NSString *sFont; CGFloat fontSize; } @property(copy) NSString *sFind; @property(assign) long positionFind; @property(assign) BOOL retaining; @property(assign) CGFloat widthText; @property(assign) CGFloat heightLine; @property(copy) NSString *sFont; @property(assign) CGFloat fontSize; - (void) animateMatch: (CGPoint) ptText bounce: (BOOL) bounce; - (void) hideMatch; @end //-------------------------------------------------------------------------------------------------- @implementation FindHighlightLayer @synthesize sFind, positionFind, retaining, widthText, heightLine, sFont, fontSize; - (id) init { if (self = [super init]) { [self setNeedsDisplayOnBoundsChange: YES]; // A gold to slightly redder gradient to match other applications CGColorRef colGold = CGColorCreateGenericRGB(1.0, 1.0, 0, 1.0); CGColorRef colGoldRed = CGColorCreateGenericRGB(1.0, 0.8, 0, 1.0); self.colors = @[(__bridge id)colGoldRed, (__bridge id)colGold]; CGColorRelease(colGoldRed); CGColorRelease(colGold); CGColorRef colGreyBorder = CGColorCreateGenericGray(0.756f, 0.5f); self.borderColor = colGreyBorder; CGColorRelease(colGreyBorder); self.borderWidth = 1.0; self.cornerRadius = 5.0f; self.shadowRadius = 1.0f; self.shadowOpacity = 0.9f; self.shadowOffset = CGSizeMake(0.0f, -2.0f); self.anchorPoint = CGPointMake(0.5, 0.5); } return self; } const CGFloat paddingHighlightX = 4; const CGFloat paddingHighlightY = 2; - (void) drawInContext: (CGContextRef) context { if (!sFind || !sFont) return; CFStringRef str = (__bridge CFStringRef)(sFind); CFMutableDictionaryRef styleDict = CFDictionaryCreateMutable(kCFAllocatorDefault, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CGColorRef color = CGColorCreateGenericRGB(0.0, 0.0, 0.0, 1.0); CFDictionarySetValue(styleDict, kCTForegroundColorAttributeName, color); CTFontRef fontRef = ::CTFontCreateWithName((CFStringRef)sFont, fontSize, NULL); CFDictionaryAddValue(styleDict, kCTFontAttributeName, fontRef); CFAttributedStringRef attrString = ::CFAttributedStringCreate(NULL, str, styleDict); CTLineRef textLine = ::CTLineCreateWithAttributedString(attrString); // Indent from corner of bounds CGContextSetTextPosition(context, paddingHighlightX, 3 + paddingHighlightY); CTLineDraw(textLine, context); CFRelease(textLine); CFRelease(attrString); CFRelease(fontRef); CGColorRelease(color); CFRelease(styleDict); } - (void) animateMatch: (CGPoint) ptText bounce: (BOOL) bounce { if (!self.sFind || !(self.sFind).length) { [self hideMatch]; return; } CGFloat width = self.widthText + paddingHighlightX * 2; CGFloat height = self.heightLine + paddingHighlightY * 2; CGFloat flipper = self.geometryFlipped ? -1.0 : 1.0; // Adjust for padding ptText.x -= paddingHighlightX; ptText.y += flipper * paddingHighlightY; // Shift point to centre as expanding about centre ptText.x += width / 2.0; ptText.y -= flipper * height / 2.0; [CATransaction begin]; [CATransaction setValue: @0.0f forKey: kCATransactionAnimationDuration]; self.bounds = CGRectMake(0, 0, width, height); self.position = ptText; if (bounce) { // Do not reset visibility when just moving self.hidden = NO; self.opacity = 1.0; } [self setNeedsDisplay]; [CATransaction commit]; if (bounce) { CABasicAnimation *animBounce = [CABasicAnimation animationWithKeyPath: @"transform.scale"]; animBounce.duration = 0.15; animBounce.autoreverses = YES; animBounce.removedOnCompletion = NO; animBounce.fromValue = @1.0f; animBounce.toValue = @1.25f; if (self.retaining) { [self addAnimation: animBounce forKey: @"animateFound"]; } else { CABasicAnimation *animFade = [CABasicAnimation animationWithKeyPath: @"opacity"]; animFade.duration = 0.1; animFade.beginTime = 0.4; animFade.removedOnCompletion = NO; animFade.fromValue = @1.0f; animFade.toValue = @0.0f; CAAnimationGroup *group = [CAAnimationGroup animation]; group.duration = 0.5; group.removedOnCompletion = NO; group.fillMode = kCAFillModeForwards; group.animations = @[animBounce, animFade]; [self addAnimation: group forKey: @"animateFound"]; } } } - (void) hideMatch { self.sFind = @""; self.positionFind = Sci::invalidPosition; self.hidden = YES; } @end #endif //-------------------------------------------------------------------------------------------------- @implementation TimerTarget - (id) init: (void *) target { self = [super init]; if (self != nil) { mTarget = target; // Get the default notification queue for the thread which created the instance (usually the // main thread). We need that later for idle event processing. NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; notificationQueue = [[NSNotificationQueue alloc] initWithNotificationCenter: center]; [center addObserver: self selector: @selector(idleTriggered:) name: @"Idle" object: self]; } return self; } //-------------------------------------------------------------------------------------------------- - (void) dealloc { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center removeObserver: self]; } //-------------------------------------------------------------------------------------------------- /** * Method called by owning ScintillaCocoa object when it is destroyed. */ - (void) ownerDestroyed { mTarget = NULL; notificationQueue = nil; } //-------------------------------------------------------------------------------------------------- /** * Method called by a timer installed by ScintillaCocoa. This two step approach is needed because * a native Obj-C class is required as target for the timer. */ - (void) timerFired: (NSTimer *) timer { if (mTarget) static_cast(mTarget)->TimerFired(timer); } //-------------------------------------------------------------------------------------------------- /** * Another timer callback for the idle timer. */ - (void) idleTimerFired: (NSTimer *) timer { #pragma unused(timer) // Idle timer event. // Post a new idle notification, which gets executed when the run loop is idle. // Since we are coalescing on name and sender there will always be only one actual notification // even for multiple requests. NSNotification *notification = [NSNotification notificationWithName: @"Idle" object: self]; [notificationQueue enqueueNotification: notification postingStyle: NSPostWhenIdle coalesceMask: (NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender) forModes: @[NSDefaultRunLoopMode, NSModalPanelRunLoopMode]]; } //-------------------------------------------------------------------------------------------------- /** * Another step for idle events. The timer (for idle events) simply requests a notification on * idle time. Only when this notification is send we actually call back the editor. */ - (void) idleTriggered: (NSNotification *) notification { #pragma unused(notification) if (mTarget) static_cast(mTarget)->IdleTimerFired(); } @end //----------------- CGContextCurrent --------------------------------------------------------------- CGContextRef Scintilla::Internal::CGContextCurrent() { if (@available(macOS 10.10, *)) { return [NSGraphicsContext currentContext].CGContext; } else { // Use old deprecated API #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" return static_cast([NSGraphicsContext currentContext].graphicsPort); #pragma clang diagnostic pop } } //----------------- ScintillaCocoa ----------------------------------------------------------------- ScintillaCocoa::ScintillaCocoa(ScintillaView *sciView_, SCIContentView *viewContent, SCIMarginView *viewMargin) { vs.marginInside = false; // Don't retain since we're owned by view, which would cause a cycle sciView = sciView_; wMain = (__bridge WindowID)viewContent; wMargin = (__bridge WindowID)viewMargin; timerTarget = [[TimerTarget alloc] init: this]; lastMouseEvent = NULL; delegate = NULL; notifyObj = NULL; notifyProc = NULL; capturedMouse = false; isFirstResponder = false; isActive = NSApp.isActive; enteredSetScrollingSize = false; scrollSpeed = 1; scrollTicks = 2000; observer = NULL; layerFindIndicator = NULL; imeInteraction = IMEInteraction::Inline; std::fill(timers, std::end(timers), nil); Init(); } //-------------------------------------------------------------------------------------------------- ScintillaCocoa::~ScintillaCocoa() { [timerTarget ownerDestroyed]; } //-------------------------------------------------------------------------------------------------- /** * Core initialization of the control. Everything that needs to be set up happens here. */ void ScintillaCocoa::Init() { // Tell Scintilla not to buffer: Quartz buffers drawing for us. WndProc(Message::SetBufferedDraw, 0, 0); // We are working with Unicode exclusively. WndProc(Message::SetCodePage, SC_CP_UTF8, 0); // Add Mac specific key bindings. for (int i = 0; static_cast(macMapDefault[i].key); i++) kmap.AssignCmdKey(macMapDefault[i].key, macMapDefault[i].modifiers, macMapDefault[i].msg); } //-------------------------------------------------------------------------------------------------- /** * We need some clean up. Do it here. */ void ScintillaCocoa::Finalise() { ObserverRemove(); for (size_t tr=static_cast(TickReason::caret); tr<=static_cast(TickReason::platform); tr++) { FineTickerCancel(static_cast(tr)); } ScintillaBase::Finalise(); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::UpdateObserver(CFRunLoopObserverRef /* observer */, CFRunLoopActivity /* activity */, void *info) { ScintillaCocoa *sci = static_cast(info); sci->IdleWork(); } //-------------------------------------------------------------------------------------------------- /** * Add an observer to the run loop to perform styling as high-priority idle task. */ void ScintillaCocoa::ObserverAdd() { if (!observer) { CFRunLoopObserverContext context; context.version = 0; context.info = this; context.retain = NULL; context.release = NULL; context.copyDescription = NULL; CFRunLoopRef mainRunLoop = CFRunLoopGetMain(); observer = CFRunLoopObserverCreate(NULL, kCFRunLoopEntry | kCFRunLoopBeforeWaiting, true, 0, UpdateObserver, &context); CFRunLoopAddObserver(mainRunLoop, observer, kCFRunLoopCommonModes); } } //-------------------------------------------------------------------------------------------------- /** * Remove the run loop observer. */ void ScintillaCocoa::ObserverRemove() { if (observer) { CFRunLoopRef mainRunLoop = CFRunLoopGetMain(); CFRunLoopRemoveObserver(mainRunLoop, observer, kCFRunLoopCommonModes); CFRelease(observer); } observer = NULL; } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::IdleWork() { Editor::IdleWork(); ObserverRemove(); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::QueueIdleWork(WorkItems items, Sci::Position upTo) { Editor::QueueIdleWork(items, upTo); ObserverAdd(); } //-------------------------------------------------------------------------------------------------- /** * Convert a Core Foundation string into a std::string in a particular encoding. */ static std::string EncodedBytesString(CFStringRef cfsRef, CFStringEncoding encoding) { const CFRange rangeAll = {0, CFStringGetLength(cfsRef)}; CFIndex usedLen = 0; CFStringGetBytes(cfsRef, rangeAll, encoding, '?', false, NULL, 0, &usedLen); std::string buffer(usedLen, '\0'); if (usedLen > 0) { CFStringGetBytes(cfsRef, rangeAll, encoding, '?', false, reinterpret_cast(&buffer[0]), usedLen, NULL); } return buffer; } //-------------------------------------------------------------------------------------------------- /** * Create a Core Foundation string from a string. * This is a simple wrapper that specifies common arguments (the default allocator and * false for isExternalRepresentation) and avoids casting since strings in Scintilla * contain char, not UInt8 (unsigned char). */ static CFStringRef CFStringFromString(const char *s, size_t len, CFStringEncoding encoding) { return CFStringCreateWithBytes(kCFAllocatorDefault, reinterpret_cast(s), len, encoding, false); } //-------------------------------------------------------------------------------------------------- /** * Case folders. */ class CaseFolderDBCS : public CaseFolderTable { CFStringEncoding encoding; public: explicit CaseFolderDBCS(CFStringEncoding encoding_) : encoding(encoding_) { } size_t Fold(char *folded, size_t sizeFolded, const char *mixed, size_t lenMixed) override { if ((lenMixed == 1) && (sizeFolded > 0)) { folded[0] = mapping[static_cast(mixed[0])]; return 1; } else { CFStringRef cfsVal = CFStringFromString(mixed, lenMixed, encoding); if (!cfsVal) { folded[0] = '\0'; return 1; } NSString *sMapped = [(__bridge NSString *)cfsVal stringByFoldingWithOptions: NSCaseInsensitiveSearch locale: [NSLocale currentLocale]]; std::string encoded = EncodedBytesString((__bridge CFStringRef)sMapped, encoding); size_t lenMapped = encoded.length(); if (lenMapped < sizeFolded) { memcpy(folded, encoded.c_str(), lenMapped); } else { folded[0] = '\0'; lenMapped = 1; } CFRelease(cfsVal); return lenMapped; } } }; std::unique_ptr ScintillaCocoa::CaseFolderForEncoding() { if (pdoc->dbcsCodePage == SC_CP_UTF8) { return std::make_unique(); } else { CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); if (pdoc->dbcsCodePage == 0) { std::unique_ptr pcf = std::make_unique(); // Only for single byte encodings for (int i=0x80; i<0x100; i++) { char sCharacter[2] = "A"; sCharacter[0] = static_cast(i); CFStringRef cfsVal = CFStringFromString(sCharacter, 1, encoding); if (!cfsVal) continue; NSString *sMapped = [(__bridge NSString *)cfsVal stringByFoldingWithOptions: NSCaseInsensitiveSearch locale: [NSLocale currentLocale]]; std::string encoded = EncodedBytesString((__bridge CFStringRef)sMapped, encoding); if (encoded.length() == 1) { pcf->SetTranslation(sCharacter[0], encoded[0]); } CFRelease(cfsVal); } return pcf; } else { return std::make_unique(encoding); } } } //-------------------------------------------------------------------------------------------------- /** * Case-fold the given string depending on the specified case mapping type. */ std::string ScintillaCocoa::CaseMapString(const std::string &s, CaseMapping caseMapping) { if ((s.size() == 0) || (caseMapping == CaseMapping::same)) return s; if (IsUnicodeMode()) { std::string retMapped(s.length() * maxExpansionCaseConversion, 0); size_t lenMapped = CaseConvertString(&retMapped[0], retMapped.length(), s.c_str(), s.length(), (caseMapping == CaseMapping::upper) ? CaseConversion::upper : CaseConversion::lower); retMapped.resize(lenMapped); return retMapped; } CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); CFStringRef cfsVal = CFStringFromString(s.c_str(), s.length(), encoding); if (!cfsVal) { return s; } NSString *sMapped; switch (caseMapping) { case CaseMapping::upper: sMapped = ((__bridge NSString *)cfsVal).uppercaseString; break; case CaseMapping::lower: sMapped = ((__bridge NSString *)cfsVal).lowercaseString; break; default: sMapped = (__bridge NSString *)cfsVal; } // Back to encoding std::string result = EncodedBytesString((__bridge CFStringRef)sMapped, encoding); CFRelease(cfsVal); return result; } //-------------------------------------------------------------------------------------------------- /** * Cancel all modes, both for base class and any find indicator. */ void ScintillaCocoa::CancelModes() { ScintillaBase::CancelModes(); HideFindIndicator(); } //-------------------------------------------------------------------------------------------------- /** * Helper function to get the scrolling view. */ NSScrollView *ScintillaCocoa::ScrollContainer() const { NSView *container = (__bridge NSView *)(wMain.GetID()); return static_cast(container.superview.superview); } //-------------------------------------------------------------------------------------------------- /** * Helper function to get the inner container which represents the actual "canvas" we work with. */ SCIContentView *ScintillaCocoa::ContentView() { return (__bridge SCIContentView *)(wMain.GetID()); } //-------------------------------------------------------------------------------------------------- /** * Return the top left visible point relative to the origin point of the whole document. */ Scintilla::Internal::Point ScintillaCocoa::GetVisibleOriginInMain() const { NSScrollView *scrollView = ScrollContainer(); NSRect contentRect = scrollView.contentView.bounds; return Point(contentRect.origin.x, contentRect.origin.y); } //-------------------------------------------------------------------------------------------------- /** * Return the size of the client area which has been cached. */ Scintilla::Internal::Point ScintillaCocoa::ClientSize() const { return sizeClient; } //-------------------------------------------------------------------------------------------------- /** * Instead of returning the size of the inner view we have to return the visible part of it * in order to make scrolling working properly. * The returned value is in document coordinates. */ PRectangle ScintillaCocoa::GetClientRectangle() const { NSScrollView *scrollView = ScrollContainer(); return NSRectToPRectangle(scrollView.contentView.bounds); } //-------------------------------------------------------------------------------------------------- /** * Allow for prepared rectangle */ PRectangle ScintillaCocoa::GetClientDrawingRectangle() { #if MAC_OS_X_VERSION_MAX_ALLOWED > 1080 NSView *content = ContentView(); if ([content respondsToSelector: @selector(setPreparedContentRect:)]) { NSRect rcPrepared = content.preparedContentRect; if (!NSIsEmptyRect(rcPrepared)) return NSRectToPRectangle(rcPrepared); } #endif return ScintillaCocoa::GetClientRectangle(); } //-------------------------------------------------------------------------------------------------- /** * Converts the given point from base coordinates to local coordinates and at the same time into * a native Point structure. Base coordinates are used for the top window used in the view hierarchy. * Returned value is in view coordinates. */ Scintilla::Internal::Point ScintillaCocoa::ConvertPoint(NSPoint point) { NSView *container = ContentView(); NSPoint result = [container convertPoint: point fromView: nil]; Scintilla::Internal::Point ptOrigin = GetVisibleOriginInMain(); return Point(result.x - ptOrigin.x, result.y - ptOrigin.y); } //-------------------------------------------------------------------------------------------------- /** * Do not clip like superclass as Cocoa is not reporting all of prepared area. */ void ScintillaCocoa::RedrawRect(PRectangle rc) { if (!rc.Empty()) wMain.InvalidateRectangle(rc); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::DiscardOverdraw() { #if MAC_OS_X_VERSION_MAX_ALLOWED > 1080 // If running on 10.9, reset prepared area to visible area NSView *content = ContentView(); if ([content respondsToSelector: @selector(setPreparedContentRect:)]) { content.preparedContentRect = content.visibleRect; } #endif } //-------------------------------------------------------------------------------------------------- /** * Ensure all of prepared content is also redrawn. */ void ScintillaCocoa::Redraw() { wMargin.InvalidateAll(); DiscardOverdraw(); wMain.InvalidateAll(); } //-------------------------------------------------------------------------------------------------- /** * A function to directly execute code that would usually go the long way via window messages. * However this is a Windows metaphor and not used here, hence we just call our fake * window proc. The given parameters directly reflect the message parameters used on Windows. * * @param ptr The target which is to be called. * @param iMessage A code that indicates which message was sent. * @param wParam One of the two free parameters for the message. Traditionally a word sized parameter * (hence the w prefix). * @param lParam The other of the two free parameters. A signed long. */ sptr_t ScintillaCocoa::DirectFunction(sptr_t ptr, unsigned int iMessage, uptr_t wParam, sptr_t lParam) { ScintillaCocoa *sci = reinterpret_cast(ptr); return sci->WndProc(static_cast(iMessage), wParam, lParam); } //-------------------------------------------------------------------------------------------------- /** * A function to directly execute code that would usually go the long way via window messages. * Similar to DirectFunction but also returns the status. * However this is a Windows metaphor and not used here, hence we just call our fake * window proc. The given parameters directly reflect the message parameters used on Windows. * * @param ptr The target which is to be called. * @param iMessage A code that indicates which message was sent. * @param wParam One of the two free parameters for the message. Traditionally a word sized parameter * (hence the w prefix). * @param lParam The other of the two free parameters. A signed long. * @param pStatus Return the status to the caller. A pointer to an int. */ sptr_t ScintillaCocoa::DirectStatusFunction(sptr_t ptr, unsigned int iMessage, uptr_t wParam, sptr_t lParam, int *pStatus) { ScintillaCocoa *sci = reinterpret_cast(ptr); const sptr_t returnValue = sci->WndProc(static_cast(iMessage), wParam, lParam); *pStatus = static_cast(sci->errorStatus); return returnValue; } //-------------------------------------------------------------------------------------------------- /** * This method is very similar to DirectFunction. On Windows it sends a message (not in the Obj-C sense) * to the target window. Here we simply call our fake window proc. */ sptr_t scintilla_send_message(void *sci, unsigned int iMessage, uptr_t wParam, sptr_t lParam) { ScintillaView *control = (__bridge ScintillaView *)(sci); return [control message: iMessage wParam: wParam lParam: lParam]; } //-------------------------------------------------------------------------------------------------- namespace { /** * The animated find indicator fails with a "bogus layer size" message on macOS 10.13 * and causes drawing failures on macOS 10.12. */ bool SupportAnimatedFind() { return std::floor(NSAppKitVersionNumber) < NSAppKitVersionNumber10_12; } } //-------------------------------------------------------------------------------------------------- /** * That's our fake window procedure. On Windows each window has a dedicated procedure to handle * commands (also used to synchronize UI and background threads), which is not the case in Cocoa. * * Messages handled here are almost solely for special commands of the backend. Everything which * would be system messages on Windows (e.g. for key down, mouse move etc.) are handled by * directly calling appropriate handlers. */ sptr_t ScintillaCocoa::WndProc(Message iMessage, uptr_t wParam, sptr_t lParam) { try { switch (iMessage) { case Message::GetDirectFunction: return reinterpret_cast(DirectFunction); case Message::GetDirectStatusFunction: return reinterpret_cast(DirectStatusFunction); case Message::GetDirectPointer: return reinterpret_cast(this); case Message::SetBidirectional: bidirectional = static_cast(wParam); // Invalidate all cached information including layout. DropGraphics(); InvalidateStyleRedraw(); return 0; case Message::TargetAsUTF8: return TargetAsUTF8(CharPtrFromSPtr(lParam)); case Message::EncodedFromUTF8: return EncodedFromUTF8(ConstCharPtrFromUPtr(wParam), CharPtrFromSPtr(lParam)); case Message::SetIMEInteraction: // Only inline IME supported on Cocoa break; case Message::GrabFocus: [ContentView().window makeFirstResponder: ContentView()]; break; case Message::SetBufferedDraw: // Buffered drawing not supported on Cocoa view.bufferedDraw = false; break; case Message::FindIndicatorShow: if (SupportAnimatedFind()) { ShowFindIndicatorForRange(NSMakeRange(wParam, lParam-wParam), YES); } return 0; case Message::FindIndicatorFlash: if (SupportAnimatedFind()) { ShowFindIndicatorForRange(NSMakeRange(wParam, lParam-wParam), NO); } return 0; case Message::FindIndicatorHide: HideFindIndicator(); return 0; case Message::SetPhasesDraw: { sptr_t r = ScintillaBase::WndProc(iMessage, wParam, lParam); [sciView updateIndicatorIME]; return r; } case Message::GetAccessibility: return static_cast(Accessibility::Enabled); default: sptr_t r = ScintillaBase::WndProc(iMessage, wParam, lParam); return r; } } catch (std::bad_alloc &) { errorStatus = Status::BadAlloc; } catch (...) { errorStatus = Status::Failure; } return 0; } //-------------------------------------------------------------------------------------------------- /** * In Windows lingo this is the handler which handles anything that wasn't handled in the normal * window proc which would usually send the message back to generic window proc that Windows uses. */ sptr_t ScintillaCocoa::DefWndProc(Message, uptr_t, sptr_t) { return 0; } //-------------------------------------------------------------------------------------------------- /** * Handle any ScintillaCocoa-specific ticking or call superclass. */ void ScintillaCocoa::TickFor(TickReason reason) { if (reason == TickReason::platform) { DragScroll(); } else { Editor::TickFor(reason); } } //-------------------------------------------------------------------------------------------------- /** * Is a particular timer currently running? */ bool ScintillaCocoa::FineTickerRunning(TickReason reason) { return timers[static_cast(reason)] != nil; } //-------------------------------------------------------------------------------------------------- /** * Start a fine-grained timer. */ void ScintillaCocoa::FineTickerStart(TickReason reason, int millis, int tolerance) { FineTickerCancel(reason); NSTimer *fineTimer = [NSTimer timerWithTimeInterval: millis / 1000.0 target: timerTarget selector: @selector(timerFired:) userInfo: nil repeats: YES]; if (tolerance && [fineTimer respondsToSelector: @selector(setTolerance:)]) { fineTimer.tolerance = tolerance / 1000.0; } timers[static_cast(reason)] = fineTimer; [NSRunLoop.currentRunLoop addTimer: fineTimer forMode: NSDefaultRunLoopMode]; [NSRunLoop.currentRunLoop addTimer: fineTimer forMode: NSModalPanelRunLoopMode]; } //-------------------------------------------------------------------------------------------------- /** * Cancel a fine-grained timer. */ void ScintillaCocoa::FineTickerCancel(TickReason reason) { const size_t reasonIndex = static_cast(reason); if (timers[reasonIndex]) { [timers[reasonIndex] invalidate]; timers[reasonIndex] = nil; } } //-------------------------------------------------------------------------------------------------- bool ScintillaCocoa::SetIdle(bool on) { if (idler.state != on) { idler.state = on; if (idler.state) { // Scintilla ticks = milliseconds NSTimer *idleTimer = [NSTimer scheduledTimerWithTimeInterval: timer.tickSize / 1000.0 target: timerTarget selector: @selector(idleTimerFired:) userInfo: nil repeats: YES]; [NSRunLoop.currentRunLoop addTimer: idleTimer forMode: NSModalPanelRunLoopMode]; idler.idlerID = (__bridge IdlerID)idleTimer; } else if (idler.idlerID != NULL) { [(__bridge NSTimer *)(idler.idlerID) invalidate]; idler.idlerID = 0; } } return true; } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::CopyToClipboard(const SelectionText &selectedText) { SetPasteboardData([NSPasteboard generalPasteboard], selectedText); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::Copy() { if (!sel.Empty()) { SelectionText selectedText; CopySelectionRange(&selectedText); CopyToClipboard(selectedText); } } //-------------------------------------------------------------------------------------------------- bool ScintillaCocoa::CanPaste() { if (!Editor::CanPaste()) return false; return GetPasteboardData([NSPasteboard generalPasteboard], NULL); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::Paste() { Paste(false); } //-------------------------------------------------------------------------------------------------- /** * Pastes data from the paste board into the editor. */ void ScintillaCocoa::Paste(bool forceRectangular) { SelectionText selectedText; bool ok = GetPasteboardData([NSPasteboard generalPasteboard], &selectedText); if (forceRectangular) selectedText.rectangular = forceRectangular; if (!ok || selectedText.Empty()) // No data or no flavor we support. return; pdoc->BeginUndoAction(); ClearSelection(false); InsertPasteShape(selectedText.Data(), selectedText.Length(), selectedText.rectangular ? PasteShape::rectangular : PasteShape::stream); pdoc->EndUndoAction(); Redraw(); EnsureCaretVisible(); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::CTPaint(void *gc, NSRect rc) { #pragma unused(rc) std::unique_ptr surfaceWindow(Surface::Allocate(Technology::Default)); surfaceWindow->Init(gc, wMain.GetID()); surfaceWindow->SetMode(CurrentSurfaceMode()); ct.PaintCT(surfaceWindow.get()); surfaceWindow->Release(); } @interface CallTipView : NSControl { ScintillaCocoa *sci; } @end @implementation CallTipView - (NSView *) initWithFrame: (NSRect) frame { self = [super initWithFrame: frame]; if (self) { sci = NULL; } return self; } - (BOOL) isFlipped { return YES; } - (void) setSci: (ScintillaCocoa *) sci_ { sci = sci_; } - (void) drawRect: (NSRect) needsDisplayInRect { if (sci) { CGContextRef context = CGContextCurrent(); sci->CTPaint(context, needsDisplayInRect); } } - (void) mouseDown: (NSEvent *) event { if (sci) { sci->CallTipMouseDown(event.locationInWindow); } } // On macOS, only the key view should modify the cursor so the calltip can't. // This view does not become key so resetCursorRects never called. - (void) resetCursorRects { //[super resetCursorRects]; //[self addCursorRect: [self bounds] cursor: [NSCursor arrowCursor]]; } @end void ScintillaCocoa::CallTipMouseDown(NSPoint pt) { NSRect rectBounds = ((__bridge NSView *)(ct.wDraw.GetID())).bounds; Point location(pt.x, rectBounds.size.height - pt.y); ct.MouseClick(location); CallTipClick(); } static bool HeightDifferent(WindowID wCallTip, PRectangle rc) { NSWindow *callTip = (__bridge NSWindow *)wCallTip; CGFloat height = NSHeight(callTip.frame); return height != rc.Height(); } void ScintillaCocoa::CreateCallTipWindow(PRectangle rc) { if (ct.wCallTip.Created() && HeightDifferent(ct.wCallTip.GetID(), rc)) { ct.wCallTip.Destroy(); } if (!ct.wCallTip.Created()) { NSRect ctRect = NSMakeRect(rc.top, rc.bottom, rc.Width(), rc.Height()); NSWindow *callTip = [[NSWindow alloc] initWithContentRect: ctRect styleMask: NSWindowStyleMaskBorderless backing: NSBackingStoreBuffered defer: NO]; [callTip setLevel: NSModalPanelWindowLevel+1]; [callTip setHasShadow: YES]; NSRect ctContent = NSMakeRect(0, 0, rc.Width(), rc.Height()); CallTipView *caption = [[CallTipView alloc] initWithFrame: ctContent]; caption.autoresizingMask = NSViewWidthSizable | NSViewMaxYMargin; [caption setSci: this]; [callTip.contentView addSubview: caption]; [callTip orderFront: caption]; ct.wCallTip = (__bridge_retained WindowID)callTip; ct.wDraw = (__bridge WindowID)caption; } } void ScintillaCocoa::AddToPopUp(const char *label, int cmd, bool enabled) { NSMenuItem *item; ScintillaContextMenu *menu = (__bridge ScintillaContextMenu *)(popup.GetID()); [menu setOwner: this]; [menu setAutoenablesItems: NO]; if (cmd == 0) { item = [NSMenuItem separatorItem]; } else { item = [[NSMenuItem alloc] init]; item.title = @(label); } item.target = menu; item.action = @selector(handleCommand:); item.tag = cmd; item.enabled = enabled; [menu addItem: item]; } // ------------------------------------------------------------------------------------------------- void ScintillaCocoa::ClaimSelection() { // macOS does not have a primary selection. } // ------------------------------------------------------------------------------------------------- /** * Returns the current caret position (which is tracked as an offset into the entire text string) * as a row:column pair. The result is zero-based. */ NSPoint ScintillaCocoa::GetCaretPosition() { const Sci::Line line = static_cast( pdoc->LineFromPosition(sel.RangeMain().caret.Position())); NSPoint result; result.y = line; result.x = sel.RangeMain().caret.Position() - pdoc->LineStart(line); return result; } // ------------------------------------------------------------------------------------------------- std::string ScintillaCocoa::UTF8FromEncoded(std::string_view encoded) const { if (IsUnicodeMode()) { return std::string(encoded); } else { CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); CFStringRef cfsVal = CFStringFromString(encoded.data(), encoded.length(), encoding); std::string utf = EncodedBytesString(cfsVal, kCFStringEncodingUTF8); if (cfsVal) CFRelease(cfsVal); return utf; } } // ------------------------------------------------------------------------------------------------- std::string ScintillaCocoa::EncodedFromUTF8(std::string_view utf8) const { if (IsUnicodeMode()) { return std::string(utf8); } else { CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); CFStringRef cfsVal = CFStringFromString(utf8.data(), utf8.length(), kCFStringEncodingUTF8); const std::string sEncoded = EncodedBytesString(cfsVal, encoding); if (cfsVal) CFRelease(cfsVal); return sEncoded; } } // ------------------------------------------------------------------------------------------------- #pragma mark Drag /** * Triggered by the tick timer on a regular basis to scroll the content during a drag operation. */ void ScintillaCocoa::DragScroll() { if (!posDrag.IsValid()) { scrollSpeed = 1; scrollTicks = 2000; return; } // TODO: does not work for wrapped lines, fix it. Sci::Line line = static_cast(pdoc->LineFromPosition(posDrag.Position())); Sci::Line currentVisibleLine = pcs->DisplayFromDoc(line); Sci::Line lastVisibleLine = std::min(topLine + LinesOnScreen(), pcs->LinesDisplayed()) - 2; if (currentVisibleLine <= topLine && topLine > 0) ScrollTo(topLine - scrollSpeed); else if (currentVisibleLine >= lastVisibleLine) ScrollTo(topLine + scrollSpeed); else { scrollSpeed = 1; scrollTicks = 2000; return; } // TODO: also handle horizontal scrolling. if (scrollSpeed == 1) { scrollTicks -= timer.tickSize; if (scrollTicks <= 0) { scrollSpeed = 5; scrollTicks = 2000; } } } //----------------- DragProviderSource ------------------------------------------------------- @interface DragProviderSource : NSObject { SelectionText selectedText; } @end @implementation DragProviderSource - (id) initWithSelectedText: (const SelectionText *) other { self = [super init]; if (self) { selectedText.Copy(*other); } return self; } - (void) pasteboard: (NSPasteboard *) pasteboard item: (NSPasteboardItem *) item provideDataForType: (NSString *) type { #pragma unused(item) if (selectedText.Length() == 0) return; if (([type compare: NSPasteboardTypeString] != NSOrderedSame) && ([type compare: ScintillaRecPboardType] != NSOrderedSame)) return; CFStringEncoding encoding = EncodingFromCharacterSet(selectedText.codePage == SC_CP_UTF8, selectedText.characterSet); CFStringRef cfsVal = CFStringFromString(selectedText.Data(), selectedText.Length(), encoding); if (!cfsVal) return; if ([type compare: NSPasteboardTypeString] == NSOrderedSame) { [pasteboard setString: (__bridge NSString *)cfsVal forType: NSPasteboardTypeString]; } else if ([type compare: ScintillaRecPboardType] == NSOrderedSame) { // This is specific to scintilla, allows us to drag rectangular selections around the document. if (selectedText.rectangular) [pasteboard setString: (__bridge NSString *)cfsVal forType: ScintillaRecPboardType]; } if (cfsVal) CFRelease(cfsVal); } @end //-------------------------------------------------------------------------------------------------- /** * Called when a drag operation was initiated from within Scintilla. */ void ScintillaCocoa::StartDrag() { if (sel.Empty()) return; inDragDrop = DragDrop::dragging; FineTickerStart(TickReason::platform, timer.tickSize, 0); // Put the data to be dragged on the drag pasteboard. SelectionText selectedText; NSPasteboard *pasteboard = nil; if (@available(macOS 10.13, *)) { pasteboard = [NSPasteboard pasteboardWithName: NSPasteboardNameDrag]; } else { // Use old deprecated name #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" pasteboard = [NSPasteboard pasteboardWithName: NSDragPboard]; #pragma clang diagnostic pop } CopySelectionRange(&selectedText); SetPasteboardData(pasteboard, selectedText); // calculate the bounds of the selection PRectangle client = GetTextRectangle(); Sci::Position selStart = sel.RangeMain().Start().Position(); Sci::Position selEnd = sel.RangeMain().End().Position(); Sci::Line startLine = static_cast(pdoc->LineFromPosition(selStart)); Sci::Line endLine = static_cast(pdoc->LineFromPosition(selEnd)); Point pt; Sci::Position startPos; Sci::Position endPos; Sci::Position ep; PRectangle rcSel; if (startLine==endLine && WndProc(Message::GetWrapMode, 0, 0) != static_cast(Wrap::None)) { // Komodo bug http://bugs.activestate.com/show_bug.cgi?id=87571 // Scintilla bug https://sourceforge.net/tracker/?func=detail&atid=102439&aid=3040200&group_id=2439 // If the width on a wrapped-line selection is negative, // find a better bounding rectangle. Point ptStart, ptEnd; startPos = WndProc(Message::GetLineSelStartPosition, startLine, 0); endPos = WndProc(Message::GetLineSelEndPosition, startLine, 0); // step back a position if we're counting the newline ep = WndProc(Message::GetLineEndPosition, startLine, 0); if (endPos > ep) endPos = ep; ptStart = LocationFromPosition(startPos); ptEnd = LocationFromPosition(endPos); if (ptStart.y == ptEnd.y) { // We're just selecting part of one visible line rcSel.left = ptStart.x; rcSel.right = ptEnd.x < client.right ? ptEnd.x : client.right; } else { // Find the bounding box. startPos = WndProc(Message::PositionFromLine, startLine, 0); rcSel.left = LocationFromPosition(startPos).x; rcSel.right = client.right; } rcSel.top = ptStart.y; rcSel.bottom = ptEnd.y + vs.lineHeight; if (rcSel.bottom > client.bottom) { rcSel.bottom = client.bottom; } } else { rcSel.top = rcSel.bottom = rcSel.right = rcSel.left = -1; for (Sci::Line l = startLine; l <= endLine; l++) { startPos = WndProc(Message::GetLineSelStartPosition, l, 0); endPos = WndProc(Message::GetLineSelEndPosition, l, 0); if (endPos == startPos) continue; // step back a position if we're counting the newline ep = WndProc(Message::GetLineEndPosition, l, 0); if (endPos > ep) endPos = ep; pt = LocationFromPosition(startPos); // top left of line selection if (pt.x < rcSel.left || rcSel.left < 0) rcSel.left = pt.x; if (pt.y < rcSel.top || rcSel.top < 0) rcSel.top = pt.y; pt = LocationFromPosition(endPos); // top right of line selection pt.y += vs.lineHeight; // get to the bottom of the line if (pt.x > rcSel.right || rcSel.right < 0) { if (pt.x > client.right) rcSel.right = client.right; else rcSel.right = pt.x; } if (pt.y > rcSel.bottom || rcSel.bottom < 0) { if (pt.y > client.bottom) rcSel.bottom = client.bottom; else rcSel.bottom = pt.y; } } } // must convert to global coordinates for drag regions, but also save the // image rectangle for further calculations and copy operations // Prepare drag image. NSRect selectionRectangle = PRectangleToNSRect(rcSel); if (NSIsEmptyRect(selectionRectangle)) return; SCIContentView *content = ContentView(); // To get a bitmap of the text we're dragging, we just use Paint on a pixmap surface. SurfaceImpl si; si.SetMode(CurrentSurfaceMode()); std::unique_ptr sw = si.AllocatePixMapImplementation(static_cast(client.Width()), static_cast(client.Height())); const bool lastSelectionVisible = vs.selection.visible; vs.selection.visible = false; PRectangle imageRect = rcSel; paintState = PaintState::painting; paintingAllText = true; CGContextRef gcsw = sw->GetContext(); CGContextTranslateCTM(gcsw, -client.left, -client.top); Paint(sw.get(), client); paintState = PaintState::notPainting; vs.selection.visible = lastSelectionVisible; std::unique_ptr pixmap = si.AllocatePixMapImplementation(static_cast(imageRect.Width()), static_cast(imageRect.Height())); CGContextRef gc = pixmap->GetContext(); // To make Paint() work on a bitmap, we have to flip our coordinates and translate the origin CGContextTranslateCTM(gc, 0, imageRect.Height()); CGContextScaleCTM(gc, 1.0, -1.0); pixmap->CopyImageRectangle(sw.get(), imageRect, PRectangle(0.0f, 0.0f, imageRect.Width(), imageRect.Height())); // XXX TODO: overwrite any part of the image that is not part of the // selection to make it transparent. right now we just use // the full rectangle which may include non-selected text. NSBitmapImageRep *bitmap = NULL; CGImageRef imagePixmap = pixmap->CreateImage(); if (imagePixmap) bitmap = [[NSBitmapImageRep alloc] initWithCGImage: imagePixmap]; CGImageRelease(imagePixmap); NSImage *image = [[NSImage alloc] initWithSize: selectionRectangle.size]; [image addRepresentation: bitmap]; NSImage *dragImage = [[NSImage alloc] initWithSize: selectionRectangle.size]; dragImage.backgroundColor = [NSColor clearColor]; [dragImage lockFocus]; [image drawAtPoint: NSZeroPoint fromRect: NSZeroRect operation: NSCompositingOperationSourceOver fraction: 0.5]; [dragImage unlockFocus]; NSPoint startPoint; startPoint.x = selectionRectangle.origin.x + client.left; startPoint.y = selectionRectangle.origin.y + selectionRectangle.size.height + client.top; NSPasteboardItem *pbItem = [NSPasteboardItem new]; DragProviderSource *dps = [[DragProviderSource alloc] initWithSelectedText: &selectedText]; NSArray *pbTypes = selectedText.rectangular ? @[NSPasteboardTypeString, ScintillaRecPboardType] : @[NSPasteboardTypeString]; [pbItem setDataProvider: dps forTypes: pbTypes]; NSDraggingItem *dragItem = [[NSDraggingItem alloc ]initWithPasteboardWriter: pbItem]; NSScrollView *scrollContainer = ScrollContainer(); NSRect contentRect = scrollContainer.contentView.bounds; NSRect draggingRect = NSOffsetRect(selectionRectangle, contentRect.origin.x, contentRect.origin.y); [dragItem setDraggingFrame: draggingRect contents: dragImage]; NSDraggingSession *dragSession = [content beginDraggingSessionWithItems: @[dragItem] event: lastMouseEvent source: content]; dragSession.animatesToStartingPositionsOnCancelOrFail = YES; dragSession.draggingFormation = NSDraggingFormationNone; } //-------------------------------------------------------------------------------------------------- /** * Called when a drag operation reaches the control which was initiated outside. */ NSDragOperation ScintillaCocoa::DraggingEntered(id info) { FineTickerStart(TickReason::platform, timer.tickSize, 0); return DraggingUpdated(info); } //-------------------------------------------------------------------------------------------------- /** * Called frequently during a drag operation if we are the target. Keep telling the caller * what drag operation we accept and update the drop caret position to indicate the * potential insertion point of the dragged data. */ NSDragOperation ScintillaCocoa::DraggingUpdated(id info) { // Convert the drag location from window coordinates to view coordinates and // from there to a text position to finally set the drag position. Point location = ConvertPoint([info draggingLocation]); SetDragPosition(SPositionFromLocation(location)); NSDragOperation sourceDragMask = [info draggingSourceOperationMask]; if (sourceDragMask == NSDragOperationNone) return sourceDragMask; NSPasteboard *pasteboard = [info draggingPasteboard]; // Return what type of operation we will perform. Prefer move over copy. if ([pasteboard.types containsObject: NSPasteboardTypeString] || [pasteboard.types containsObject: ScintillaRecPboardType]) return (sourceDragMask & NSDragOperationMove) ? NSDragOperationMove : NSDragOperationCopy; if (@available(macOS 10.13, *)) { if ([pasteboard.types containsObject: NSPasteboardTypeFileURL]) return (sourceDragMask & NSDragOperationGeneric); } else { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if ([pasteboard.types containsObject: NSFilenamesPboardType]) return (sourceDragMask & NSDragOperationGeneric); #pragma clang diagnostic pop } return NSDragOperationNone; } //-------------------------------------------------------------------------------------------------- /** * Resets the current drag position as we are no longer the drag target. */ void ScintillaCocoa::DraggingExited(id info) { #pragma unused(info) SetDragPosition(SelectionPosition(Sci::invalidPosition)); FineTickerCancel(TickReason::platform); inDragDrop = DragDrop::none; } //-------------------------------------------------------------------------------------------------- /** * Here is where the real work is done. Insert the text from the pasteboard. */ bool ScintillaCocoa::PerformDragOperation(id info) { NSPasteboard *pasteboard = [info draggingPasteboard]; if (@available(macOS 10.13, *)) { // NSPasteboardTypeFileURL is available for macOS 10.13+, provides NSURLs if ([pasteboard.types containsObject: NSPasteboardTypeFileURL]) { NSArray *files = [pasteboard readObjectsForClasses:@[NSURL.class] options:nil]; for (NSURL *uri in files) { NotifyURIDropped([uri path].UTF8String); } return true; } } else { // Use deprecated NSFilenamesPboardType, provides NSStrings #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if ([pasteboard.types containsObject: NSFilenamesPboardType]) { NSArray *files = [pasteboard propertyListForType: NSFilenamesPboardType]; for (NSString *uri in files) { NotifyURIDropped(uri.UTF8String); } } #pragma clang diagnostic pop } SelectionText text; GetPasteboardData(pasteboard, &text); if (text.Length() > 0) { NSDragOperation operation = [info draggingSourceOperationMask]; bool moving = (operation & NSDragOperationMove) != 0; DropAt(posDrag, text.Data(), text.Length(), moving, text.rectangular); }; return true; } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::SetPasteboardData(NSPasteboard *board, const SelectionText &selectedText) { if (selectedText.Length() == 0) return; CFStringEncoding encoding = EncodingFromCharacterSet(selectedText.codePage == SC_CP_UTF8, selectedText.characterSet); CFStringRef cfsVal = CFStringFromString(selectedText.Data(), selectedText.Length(), encoding); if (!cfsVal) return; NSArray *pbTypes = selectedText.rectangular ? @[NSPasteboardTypeString, ScintillaRecPboardType] : @[NSPasteboardTypeString]; [board declareTypes: pbTypes owner: nil]; if (selectedText.rectangular) { // This is specific to scintilla, allows us to drag rectangular selections around the document. [board setString: (__bridge NSString *)cfsVal forType: ScintillaRecPboardType]; } [board setString: (__bridge NSString *)cfsVal forType: NSPasteboardTypeString]; if (cfsVal) CFRelease(cfsVal); } //-------------------------------------------------------------------------------------------------- /** * Helper method to retrieve the best fitting alternative from the general pasteboard. */ bool ScintillaCocoa::GetPasteboardData(NSPasteboard *board, SelectionText *selectedText) { NSArray *supportedTypes = @[ScintillaRecPboardType, NSPasteboardTypeString]; NSString *bestType = [board availableTypeFromArray: supportedTypes]; NSString *data = [board stringForType: bestType]; if (data != nil) { if (selectedText != nil) { CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); CFRange rangeAll = {0, static_cast(data.length)}; CFIndex usedLen = 0; CFStringGetBytes((CFStringRef)data, rangeAll, encoding, '?', false, NULL, 0, &usedLen); std::vector buffer(usedLen); CFStringGetBytes((CFStringRef)data, rangeAll, encoding, '?', false, buffer.data(), usedLen, NULL); bool rectangular = bestType == ScintillaRecPboardType; std::string dest(reinterpret_cast(buffer.data()), usedLen); selectedText->Copy(dest, pdoc->dbcsCodePage, vs.styles[StyleDefault].characterSet, rectangular, false); } return true; } return false; } //-------------------------------------------------------------------------------------------------- // Returns the target converted to UTF8. // Return the length in bytes. Sci::Position ScintillaCocoa::TargetAsUTF8(char *text) const { const Sci::Position targetLength = targetRange.Length(); if (IsUnicodeMode()) { if (text) pdoc->GetCharRange(text, targetRange.start.Position(), targetLength); } else { // Need to convert const CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); const std::string s = RangeText(targetRange.start.Position(), targetRange.end.Position()); CFStringRef cfsVal = CFStringFromString(s.c_str(), s.length(), encoding); if (!cfsVal) { return 0; } const std::string tmputf = EncodedBytesString(cfsVal, kCFStringEncodingUTF8); if (text) memcpy(text, tmputf.c_str(), tmputf.length()); CFRelease(cfsVal); return tmputf.length(); } return targetLength; } //-------------------------------------------------------------------------------------------------- // Returns the text in the range converted to an NSString. NSString *ScintillaCocoa::RangeTextAsString(NSRange rangePositions) const { const std::string text = RangeText(rangePositions.location, NSMaxRange(rangePositions)); if (IsUnicodeMode()) { return @(text.c_str()); } else { // Need to convert const CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); CFStringRef cfsVal = CFStringFromString(text.c_str(), text.length(), encoding); return (__bridge NSString *)cfsVal; } } //-------------------------------------------------------------------------------------------------- // Return character range of a line. NSRange ScintillaCocoa::RangeForVisibleLine(NSInteger lineVisible) { const Range posRangeLine = RangeDisplayLine(static_cast(lineVisible)); return CharactersFromPositions(NSMakeRange(posRangeLine.First(), posRangeLine.Last() - posRangeLine.First())); } //-------------------------------------------------------------------------------------------------- // Returns visible line number of a text position in characters. NSInteger ScintillaCocoa::VisibleLineForIndex(NSInteger index) { const NSRange rangePosition = PositionsFromCharacters(NSMakeRange(index, 0)); const Sci::Line lineVisible = DisplayFromPosition(rangePosition.location); return lineVisible; } //-------------------------------------------------------------------------------------------------- // Returns a rectangle that frames the range for use by the VoiceOver cursor. NSRect ScintillaCocoa::FrameForRange(NSRange rangeCharacters) { const NSRange posRange = PositionsFromCharacters(rangeCharacters); NSUInteger rangeEnd = NSMaxRange(posRange); const bool endsWithLineEnd = rangeCharacters.length && (pdoc->GetColumn(rangeEnd) == 0); Point ptStart = LocationFromPosition(posRange.location); Point ptEnd = LocationFromPosition(rangeEnd, PointEnd::endEither); NSRect rect = NSMakeRect(ptStart.x, ptStart.y, ptEnd.x - ptStart.x, ptEnd.y - ptStart.y); rect.size.width += 2; // Shows the last character better if (endsWithLineEnd) { // Add a block to the right to indicate a line end is selected rect.size.width += 20; } rect.size.height += vs.lineHeight; // Adjust for margin and scroll rect.origin.x = rect.origin.x - vs.textStart + vs.fixedColumnWidth; return rect; } //-------------------------------------------------------------------------------------------------- // Returns a rectangle that frames the range for use by the VoiceOver cursor. NSRect ScintillaCocoa::GetBounds() const { return PRectangleToNSRect(GetClientRectangle()); } //-------------------------------------------------------------------------------------------------- // Translates a UTF8 string into the document encoding. // Return the length of the result in bytes. Sci::Position ScintillaCocoa::EncodedFromUTF8(const char *utf8, char *encoded) const { const size_t inputLength = (lengthForEncode >= 0) ? lengthForEncode : strlen(utf8); if (IsUnicodeMode()) { if (encoded) memcpy(encoded, utf8, inputLength); return inputLength; } else { // Need to convert const CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); CFStringRef cfsVal = CFStringFromString(utf8, inputLength, kCFStringEncodingUTF8); const std::string sEncoded = EncodedBytesString(cfsVal, encoding); if (encoded) memcpy(encoded, sEncoded.c_str(), sEncoded.length()); CFRelease(cfsVal); return sEncoded.length(); } } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::SetMouseCapture(bool on) { capturedMouse = on; } //-------------------------------------------------------------------------------------------------- bool ScintillaCocoa::HaveMouseCapture() { return capturedMouse; } //-------------------------------------------------------------------------------------------------- /** * Synchronously paint a rectangle of the window. */ bool ScintillaCocoa::SyncPaint(void *gc, PRectangle rc) { paintState = PaintState::painting; rcPaint = rc; PRectangle rcText = GetTextRectangle(); paintingAllText = rcPaint.Contains(rcText); std::unique_ptr sw(Surface::Allocate(Technology::Default)); CGContextSetAllowsAntialiasing((CGContextRef)gc, vs.extraFontFlag != FontQuality::QualityNonAntialiased); CGContextSetAllowsFontSmoothing((CGContextRef)gc, vs.extraFontFlag == FontQuality::QualityLcdOptimized); CGContextSetAllowsFontSubpixelPositioning((CGContextRef)gc, vs.extraFontFlag == FontQuality::QualityDefault || vs.extraFontFlag == FontQuality::QualityLcdOptimized); sw->Init(gc, wMain.GetID()); Paint(sw.get(), rc); const bool succeeded = paintState != PaintState::abandoned; sw->Release(); paintState = PaintState::notPainting; if (!succeeded) { NSView *marginView = (__bridge NSView *)(wMargin.GetID()); [marginView setNeedsDisplay: YES]; } return succeeded; } //-------------------------------------------------------------------------------------------------- /** * Paint the margin into the SCIMarginView space. */ void ScintillaCocoa::PaintMargin(NSRect aRect) { CGContextRef gc = CGContextCurrent(); PRectangle rc = NSRectToPRectangle(aRect); rcPaint = rc; std::unique_ptr sw(Surface::Allocate(Technology::Default)); if (sw) { CGContextSetAllowsAntialiasing(gc, vs.extraFontFlag != FontQuality::QualityNonAntialiased); CGContextSetAllowsFontSmoothing(gc, vs.extraFontFlag == FontQuality::QualityLcdOptimized); CGContextSetAllowsFontSubpixelPositioning(gc, vs.extraFontFlag == FontQuality::QualityDefault || vs.extraFontFlag == FontQuality::QualityLcdOptimized); sw->Init(gc, wMargin.GetID()); PaintSelMargin(sw.get(), rc); sw->Release(); } } //-------------------------------------------------------------------------------------------------- /** * Prepare for drawing. * * @param rect The area that will be drawn, given in the sender's coordinate system. */ void ScintillaCocoa::WillDraw(NSRect rect) { RefreshStyleData(); PRectangle rcWillDraw = NSRectToPRectangle(rect); const Sci::Position posAfterArea = PositionAfterArea(rcWillDraw); const Sci::Position posAfterMax = PositionAfterMaxStyling(posAfterArea, true); pdoc->StyleToAdjustingLineDuration(posAfterMax); StartIdleStyling(posAfterMax < posAfterArea); NotifyUpdateUI(); if (WrapLines(WrapScope::wsVisible)) { // Wrap may have reduced number of lines so more lines may need to be styled const Sci::Position posAfterAreaWrapped = PositionAfterArea(rcWillDraw); pdoc->EnsureStyledTo(posAfterAreaWrapped); // The wrapping process has changed the height of some lines so redraw all. Redraw(); } } //-------------------------------------------------------------------------------------------------- /** * ScrollText is empty because scrolling is handled by the NSScrollView. */ void ScintillaCocoa::ScrollText(Sci::Line) { } //-------------------------------------------------------------------------------------------------- /** * Modifies the vertical scroll position to make the current top line show up as such. */ void ScintillaCocoa::SetVerticalScrollPos() { NSScrollView *scrollView = ScrollContainer(); if (scrollView) { NSClipView *clipView = scrollView.contentView; NSRect contentRect = clipView.bounds; [clipView scrollToPoint: NSMakePoint(contentRect.origin.x, topLine * vs.lineHeight)]; [scrollView reflectScrolledClipView: clipView]; } } //-------------------------------------------------------------------------------------------------- /** * Modifies the horizontal scroll position to match xOffset. */ void ScintillaCocoa::SetHorizontalScrollPos() { PRectangle textRect = GetTextRectangle(); int maxXOffset = scrollWidth - static_cast(textRect.Width()); if (maxXOffset < 0) maxXOffset = 0; if (xOffset > maxXOffset) xOffset = maxXOffset; NSScrollView *scrollView = ScrollContainer(); if (scrollView) { NSClipView *clipView = scrollView.contentView; NSRect contentRect = clipView.bounds; [clipView scrollToPoint: NSMakePoint(xOffset, contentRect.origin.y)]; [scrollView reflectScrolledClipView: clipView]; } MoveFindIndicatorWithBounce(NO); } //-------------------------------------------------------------------------------------------------- /** * Used to adjust both scrollers to reflect the current scroll range and position in the editor. * Arguments no longer used as NSScrollView handles details of scroll bar sizes. * * @param nMax Number of lines in the editor. * @param nPage Number of lines per scroll page. * @return True if there was a change, otherwise false. */ bool ScintillaCocoa::ModifyScrollBars(Sci::Line nMax, Sci::Line nPage) { #pragma unused(nMax, nPage) return SetScrollingSize(); } //-------------------------------------------------------------------------------------------------- /** * Adjust both scrollers to reflect the current scroll ranges and position in the editor. * Called when size changes or when scroll ranges change. */ bool ScintillaCocoa::SetScrollingSize() { bool changes = false; SCIContentView *inner = ContentView(); if (!enteredSetScrollingSize) { enteredSetScrollingSize = true; NSScrollView *scrollView = ScrollContainer(); const NSRect clipRect = scrollView.contentView.bounds; CGFloat docHeight = pcs->LinesDisplayed() * vs.lineHeight; if (!endAtLastLine) docHeight += (int(scrollView.bounds.size.height / vs.lineHeight)-3) * vs.lineHeight; // Allow extra space so that last scroll position places whole line at top const int clipExtra = int(clipRect.size.height) % vs.lineHeight; docHeight += clipExtra; // Ensure all of clipRect covered by Scintilla drawing if (docHeight < clipRect.size.height) docHeight = clipRect.size.height; const bool showHorizontalScroll = horizontalScrollBarVisible && !Wrapping(); const CGFloat docWidth = Wrapping() ? clipRect.size.width : scrollWidth; const NSRect contentRect = NSMakeRect(0, 0, docWidth, docHeight); changes = !CGSizeEqualToSize(contentRect.size, inner.frame.size); if (changes) { inner.frame = contentRect; } scrollView.hasVerticalScroller = verticalScrollBarVisible; scrollView.hasHorizontalScroller = showHorizontalScroll; SetVerticalScrollPos(); enteredSetScrollingSize = false; } [sciView setMarginWidth: vs.fixedColumnWidth]; return changes; } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::Resize() { SetScrollingSize(); const PRectangle rcClient = GetClientRectangle(); sizeClient = Point(rcClient.Width(), rcClient.Height()); ChangeSize(); } //-------------------------------------------------------------------------------------------------- /** * Update fields to match scroll position after receiving a notification that the user has scrolled. */ void ScintillaCocoa::UpdateForScroll() { Point ptOrigin = GetVisibleOriginInMain(); xOffset = static_cast(ptOrigin.x); Sci::Line newTop = std::min(static_cast(ptOrigin.y / vs.lineHeight), MaxScrollPos()); SetTopLine(newTop); } //-------------------------------------------------------------------------------------------------- /** * Register a delegate that will be called for notifications and commands. * This provides similar functionality to RegisterNotifyCallback but in an * Objective C way. * * @param delegate_ A pointer to an object that implements ScintillaNotificationProtocol. */ void ScintillaCocoa::SetDelegate(id delegate_) { delegate = delegate_; } //-------------------------------------------------------------------------------------------------- /** * Used to register a callback function for a given window. This is used to emulate the way * Windows notifies other controls (mainly up in the view hierarchy) about certain events. * * @param windowid A handle to a window. That value is generic and can be anything. It is passed * through to the callback. * @param callback The callback function to be used for future notifications. If NULL then no * notifications will be sent anymore. */ void ScintillaCocoa::RegisterNotifyCallback(intptr_t windowid, SciNotifyFunc callback) { notifyObj = windowid; notifyProc = callback; } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::NotifyChange() { if (notifyProc != NULL) notifyProc(notifyObj, WM_COMMAND, Platform::LongFromTwoShorts(static_cast(GetCtrlID()), static_cast(FocusChange::Change)), (uintptr_t) this); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::NotifyFocus(bool focus) { if (commandEvents && notifyProc) notifyProc(notifyObj, WM_COMMAND, Platform::LongFromTwoShorts(static_cast(GetCtrlID()), static_cast((focus ? FocusChange::Setfocus : FocusChange::Killfocus))), (uintptr_t) this); Editor::NotifyFocus(focus); } //-------------------------------------------------------------------------------------------------- /** * Used to send a notification (as WM_NOTIFY call) to the procedure, which has been set by the call * to RegisterNotifyCallback (so it is not necessarily the parent window). * * @param scn The notification to send. */ void ScintillaCocoa::NotifyParent(NotificationData scn) { scn.nmhdr.hwndFrom = (void *) this; scn.nmhdr.idFrom = GetCtrlID(); if (notifyProc != NULL) notifyProc(notifyObj, WM_NOTIFY, GetCtrlID(), (uintptr_t) &scn); if (delegate) [delegate notification: reinterpret_cast(&scn)]; if (scn.nmhdr.code == Notification::UpdateUI) { NSView *content = ContentView(); if (FlagSet(scn.updated, Update::Content)) { NSAccessibilityPostNotification(content, NSAccessibilityValueChangedNotification); } if (FlagSet(scn.updated, Update::Selection)) { NSAccessibilityPostNotification(content, NSAccessibilitySelectedTextChangedNotification); } } } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::NotifyURIDropped(const char *uri) { NotificationData scn; scn.nmhdr.code = Notification::URIDropped; scn.text = uri; NotifyParent(scn); } //-------------------------------------------------------------------------------------------------- bool ScintillaCocoa::HasSelection() { return !sel.Empty(); } //-------------------------------------------------------------------------------------------------- bool ScintillaCocoa::CanUndo() { return pdoc->CanUndo(); } //-------------------------------------------------------------------------------------------------- bool ScintillaCocoa::CanRedo() { return pdoc->CanRedo(); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::TimerFired(NSTimer *timer) { for (size_t tr=static_cast(TickReason::caret); tr<=static_cast(TickReason::platform); tr++) { if (timers[tr] == timer) { TickFor(static_cast(tr)); } } } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::IdleTimerFired() { bool more = Idle(); if (!more) SetIdle(false); } //-------------------------------------------------------------------------------------------------- /** * Main entry point for drawing the control. * * @param rect The area to paint, given in the sender's coordinate system. * @param gc The context we can use to paint. */ bool ScintillaCocoa::Draw(NSRect rect, CGContextRef gc) { return SyncPaint(gc, NSRectToPRectangle(rect)); } //-------------------------------------------------------------------------------------------------- /** * Helper function to translate macOS key codes to Scintilla key codes. */ static inline Keys KeyTranslate(UniChar unicodeChar, NSEventModifierFlags modifierFlags) { switch (unicodeChar) { case NSDownArrowFunctionKey: return Keys::Down; case NSUpArrowFunctionKey: return Keys::Up; case NSLeftArrowFunctionKey: return Keys::Left; case NSRightArrowFunctionKey: return Keys::Right; case NSHomeFunctionKey: return Keys::Home; case NSEndFunctionKey: return Keys::End; case NSPageUpFunctionKey: return Keys::Prior; case NSPageDownFunctionKey: return Keys::Next; case NSDeleteFunctionKey: return Keys::Delete; case NSInsertFunctionKey: return Keys::Insert; case '\n': case 3: return Keys::Return; case 27: return Keys::Escape; case '+': if (modifierFlags & NSEventModifierFlagNumericPad) return Keys::Add; else return static_cast(unicodeChar); case '-': if (modifierFlags & NSEventModifierFlagNumericPad) return Keys::Subtract; else return static_cast(unicodeChar); case '/': if (modifierFlags & NSEventModifierFlagNumericPad) return Keys::Divide; else return static_cast(unicodeChar); case 127: return Keys::Back; case '\t': case 25: // Shift tab, return to unmodified tab and handle that via modifiers. return Keys::Tab; default: return static_cast(unicodeChar); } } //-------------------------------------------------------------------------------------------------- /** * Translate NSEvent modifier flags into SCI_* modifier flags. * * @param modifiers An integer bit set of NSSEvent modifier flags. * @return A set of SCI_* modifier flags. */ static KeyMod TranslateModifierFlags(NSUInteger modifiers) { // Signal Control as SCI_META return (((modifiers & NSEventModifierFlagShift) != 0) ? KeyMod::Shift : KeyMod::Norm) | (((modifiers & NSEventModifierFlagCommand) != 0) ? KeyMod::Ctrl : KeyMod::Norm) | (((modifiers & NSEventModifierFlagOption) != 0) ? KeyMod::Alt : KeyMod::Norm) | (((modifiers & NSEventModifierFlagControl) != 0) ? KeyMod::Meta : KeyMod::Norm); } //-------------------------------------------------------------------------------------------------- /** * Main keyboard input handling method. It is called for any key down event, including function keys, * numeric keypad input and whatnot. * * @param event The event instance associated with the key down event. * @return True if the input was handled, false otherwise. */ bool ScintillaCocoa::KeyboardInput(NSEvent *event) { // For now filter out function keys. NSString *input = event.charactersIgnoringModifiers; bool handled = false; // Handle each entry individually. Usually we only have one entry anyway. for (size_t i = 0; i < input.length; i++) { const UniChar originalKey = [input characterAtIndex: i]; // Some Unicode extended Latin characters overlap the Keys enumeration so treat them // only as and not as command keys. if (originalKey >= static_cast(Keys::Down) && originalKey <= static_cast(Keys::Menu)) continue; NSEventModifierFlags modifierFlags = event.modifierFlags; Keys key = KeyTranslate(originalKey, modifierFlags); bool consumed = false; // Consumed as command? if (KeyDownWithModifiers(key, TranslateModifierFlags(modifierFlags), &consumed)) handled = true; if (consumed) handled = true; } return handled; } //-------------------------------------------------------------------------------------------------- /** * Used to insert already processed text provided by the Cocoa text input system. */ ptrdiff_t ScintillaCocoa::InsertText(NSString *input, CharacterSource charSource) { if ([input length] == 0) { return 0; } // There may be multiple characters in input so loop over them if (IsUnicodeMode()) { // There may be non-BMP characters as 2 elements in NSString so // convert to UTF-8 and use UTF8BytesOfLead. std::string encoded = EncodedBytesString((__bridge CFStringRef)input, kCFStringEncodingUTF8); std::string_view sv = encoded; while (sv.length()) { const unsigned char leadByte = sv[0]; const unsigned int bytesInCharacter = UTF8BytesOfLead[leadByte]; InsertCharacter(sv.substr(0, bytesInCharacter), charSource); sv.remove_prefix(bytesInCharacter); } return encoded.length(); } else { const CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); ptrdiff_t lengthInserted = 0; for (NSInteger i = 0; i < [input length]; i++) { NSString *character = [input substringWithRange:NSMakeRange(i, 1)]; std::string encoded = EncodedBytesString((__bridge CFStringRef)character, encoding); lengthInserted += encoded.length(); InsertCharacter(encoded, charSource); } return lengthInserted; } } //-------------------------------------------------------------------------------------------------- /** * Convert from a range of characters to a range of bytes. */ NSRange ScintillaCocoa::PositionsFromCharacters(NSRange rangeCharacters) const { Sci::Position start = pdoc->GetRelativePositionUTF16(0, rangeCharacters.location); if (start == Sci::invalidPosition) start = pdoc->Length(); Sci::Position end = pdoc->GetRelativePositionUTF16(start, rangeCharacters.length); if (end == Sci::invalidPosition) end = pdoc->Length(); return NSMakeRange(start, end - start); } //-------------------------------------------------------------------------------------------------- /** * Convert from a range of characters from a range of bytes. */ NSRange ScintillaCocoa::CharactersFromPositions(NSRange rangePositions) const { const Sci::Position start = pdoc->CountUTF16(0, rangePositions.location); const Sci::Position len = pdoc->CountUTF16(rangePositions.location, NSMaxRange(rangePositions)); return NSMakeRange(start, len); } //-------------------------------------------------------------------------------------------------- /** * Used to ensure that only one selection is active for input composition as composition * does not support multi-typing. */ void ScintillaCocoa::SelectOnlyMainSelection() { sel.SetSelection(sel.RangeMain()); Redraw(); } //-------------------------------------------------------------------------------------------------- /** * Convert virtual space before selection into real space. */ void ScintillaCocoa::ConvertSelectionVirtualSpace() { ClearBeforeTentativeStart(); } //-------------------------------------------------------------------------------------------------- /** * 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->DecorationSetCurrentIndicator(static_cast(IndicatorNumbers::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. */ void ScintillaCocoa::SetDocPointer(Document *document) { // Drop input composition. NSTextInputContext *inctxt = [NSTextInputContext currentInputContext]; [inctxt discardMarkedText]; SCIContentView *inner = ContentView(); [inner unmarkText]; Editor::SetDocPointer(document); } //-------------------------------------------------------------------------------------------------- /** * Convert NSEvent timestamp NSTimeInterval into unsigned int milliseconds wanted by Editor methods. */ namespace { unsigned int TimeOfEvent(NSEvent *event) { return static_cast(std::lround(event.timestamp * 1000.0) % 2000000000); } } //-------------------------------------------------------------------------------------------------- /** * Called by the owning view when the mouse pointer enters the control. */ void ScintillaCocoa::MouseEntered(NSEvent *event) { if (!HaveMouseCapture()) { WndProc(Message::SetCursor, (long int)CursorShape::Normal, 0); // Mouse location is given in screen coordinates and might also be outside of our bounds. Point location = ConvertPoint(event.locationInWindow); ButtonMoveWithModifiers(location, TimeOfEvent(event), TranslateModifierFlags(event.modifierFlags)); } } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::MouseExited(NSEvent * /* event */) { // Nothing to do here. } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::MouseDown(NSEvent *event) { Point location = ConvertPoint(event.locationInWindow); ButtonDownWithModifiers(location, TimeOfEvent(event), TranslateModifierFlags(event.modifierFlags)); } void ScintillaCocoa::RightMouseDown(NSEvent *event) { Point location = ConvertPoint(event.locationInWindow); RightButtonDownWithModifiers(location, TimeOfEvent(event), TranslateModifierFlags(event.modifierFlags)); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::MouseMove(NSEvent *event) { lastMouseEvent = event; ButtonMoveWithModifiers(ConvertPoint(event.locationInWindow), TimeOfEvent(event), TranslateModifierFlags(event.modifierFlags)); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::MouseUp(NSEvent *event) { ButtonUpWithModifiers(ConvertPoint(event.locationInWindow), TimeOfEvent(event), TranslateModifierFlags(event.modifierFlags)); } //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::MouseWheel(NSEvent *event) { bool command = (event.modifierFlags & NSEventModifierFlagCommand) != 0; int dY = 0; // In order to make scrolling with larger offset smoother we scroll less lines the larger the // delta value is. if (event.deltaY < 0) dY = -static_cast(sqrt(-10.0 * event.deltaY)); else dY = static_cast(sqrt(10.0 * event.deltaY)); if (command) { // Zoom! We play with the font sizes in the styles. // Number of steps/line is ignored, we just care if sizing up or down. if (dY > 0.5) KeyCommand(Message::ZoomIn); else if (dY < -0.5) KeyCommand(Message::ZoomOut); } else { } } //-------------------------------------------------------------------------------------------------- // Helper methods for NSResponder actions. void ScintillaCocoa::SelectAll() { Editor::SelectAll(); } void ScintillaCocoa::DeleteBackward() { KeyDownWithModifiers(Keys::Back, KeyMod::Norm, nil); } void ScintillaCocoa::Cut() { Editor::Cut(); } void ScintillaCocoa::Undo() { Editor::Undo(); } void ScintillaCocoa::Redo() { Editor::Redo(); } //-------------------------------------------------------------------------------------------------- bool ScintillaCocoa::ShouldDisplayPopupOnMargin() { return displayPopupMenu == PopUp::All; } bool ScintillaCocoa::ShouldDisplayPopupOnText() { return displayPopupMenu == PopUp::All || displayPopupMenu == PopUp::Text; } /** * Creates and returns a popup menu, which is then displayed by the Cocoa framework. */ NSMenu *ScintillaCocoa::CreateContextMenu(NSEvent * /* event */) { // Call ScintillaBase to create the context menu. ContextMenu(Point(0, 0)); return (__bridge NSMenu *)(popup.GetID()); } //-------------------------------------------------------------------------------------------------- /** * An intermediate function to forward context menu commands from the menu action handler to * scintilla. */ void ScintillaCocoa::HandleCommand(NSInteger command) { Command(static_cast(command)); } //-------------------------------------------------------------------------------------------------- /** * Update 'isFirstResponder' and possibly change focus state. */ void ScintillaCocoa::SetFirstResponder(bool isFirstResponder_) { isFirstResponder = isFirstResponder_; SetFocusActiveState(); } //-------------------------------------------------------------------------------------------------- /** * Update 'isActive' and possibly change focus state. */ void ScintillaCocoa::ActiveStateChanged(bool isActive_) { isActive = isActive_; SetFocusActiveState(); } //-------------------------------------------------------------------------------------------------- /** * Update 'hasFocus' based on first responder and active status. */ void ScintillaCocoa::SetFocusActiveState() { SetFocusState(isActive && isFirstResponder); } //-------------------------------------------------------------------------------------------------- namespace { /** * Convert from an NSColor into a ColourRGBA */ ColourRGBA ColourFromNSColor(NSColor *value) { return ColourRGBA(static_cast(value.redComponent * componentMaximum), static_cast(value.greenComponent * componentMaximum), static_cast(value.blueComponent * componentMaximum), static_cast(value.alphaComponent * componentMaximum)); } } //-------------------------------------------------------------------------------------------------- /** * Update ViewStyle::elementBaseColours to match system preferences. */ void ScintillaCocoa::UpdateBaseElements() { NSView *content = ContentView(); NSAppearance *saved = [NSAppearance currentAppearance]; [NSAppearance setCurrentAppearance:content.effectiveAppearance]; bool changed = false; if (@available(macOS 10.14, *)) { NSColorSpace *colorSpace = [NSColorSpace genericRGBColorSpace]; NSColor *textBack = [NSColor.textBackgroundColor colorUsingColorSpace: colorSpace]; NSColor *noFocusBack = [NSColor.unemphasizedSelectedTextBackgroundColor colorUsingColorSpace: colorSpace]; if (vs.selection.layer == Layer::Base) { NSColor *selBack = [NSColor.selectedTextBackgroundColor colorUsingColorSpace: colorSpace]; // Additional selection: blend with text background to make weaker version. NSColor *modified = [selBack blendedColorWithFraction:0.5 ofColor:textBack]; changed = vs.SetElementBase(Element::SelectionBack, ColourFromNSColor(selBack)); changed = vs.SetElementBase(Element::SelectionAdditionalBack, ColourFromNSColor(modified)) || changed; changed = vs.SetElementBase(Element::SelectionInactiveBack, ColourFromNSColor(noFocusBack)) || changed; } else { // Less translucent colour used in dark mode as otherwise less visible const int alpha = textBack.brightnessComponent > 0.5 ? 0x40 : 0x60; // Make a translucent colour that approximates selectedTextBackgroundColor NSColor *accent = [NSColor.controlAccentColor colorUsingColorSpace: colorSpace]; const ColourRGBA colourAccent = ColourFromNSColor(accent); changed = vs.SetElementBase(Element::SelectionBack, ColourRGBA(colourAccent, alpha)); changed = vs.SetElementBase(Element::SelectionAdditionalBack, ColourRGBA(colourAccent, alpha/2)) || changed; changed = vs.SetElementBase(Element::SelectionInactiveBack, ColourRGBA(ColourFromNSColor(noFocusBack), alpha)) || changed; } } if (changed) { Redraw(); } [NSAppearance setCurrentAppearance:saved]; } //-------------------------------------------------------------------------------------------------- /** * When the window is about to move, the calltip and autcoimpletion stay in the same spot, * so cancel them. */ void ScintillaCocoa::WindowWillMove() { AutoCompleteCancel(); ct.CallTipCancel(); } // If building with old SDK, need to define version number for 10.8 #ifndef NSAppKitVersionNumber10_8 #define NSAppKitVersionNumber10_8 1187 #endif //-------------------------------------------------------------------------------------------------- void ScintillaCocoa::ShowFindIndicatorForRange(NSRange charRange, BOOL retaining) { #if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5 NSView *content = ContentView(); if (!layerFindIndicator) { layerFindIndicator = [[FindHighlightLayer alloc] init]; [content setWantsLayer: YES]; layerFindIndicator.geometryFlipped = content.layer.geometryFlipped; if (std::floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_8) { // Content layer is unflipped on 10.9, but the indicator shows wrong unless flipped layerFindIndicator.geometryFlipped = YES; } [content.layer addSublayer: layerFindIndicator]; } [layerFindIndicator removeAnimationForKey: @"animateFound"]; if (charRange.length) { CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(), vs.styles[StyleDefault].characterSet); std::vector buffer(charRange.length); pdoc->GetCharRange(&buffer[0], charRange.location, charRange.length); CFStringRef cfsFind = CFStringFromString(&buffer[0], charRange.length, encoding); layerFindIndicator.sFind = (__bridge NSString *)cfsFind; if (cfsFind) CFRelease(cfsFind); layerFindIndicator.retaining = retaining; layerFindIndicator.positionFind = charRange.location; // Message::GetStyleAt reports a signed byte but want an unsigned to index into styles const char styleByte = static_cast(WndProc(Message::GetStyleAt, charRange.location, 0)); const long style = static_cast(styleByte); std::vector bufferFontName(WndProc(Message::StyleGetFont, style, 0) + 1); WndProc(Message::StyleGetFont, style, (sptr_t)&bufferFontName[0]); layerFindIndicator.sFont = @(&bufferFontName[0]); layerFindIndicator.fontSize = WndProc(Message::StyleGetSizeFractional, style, 0) / (float)FontSizeMultiplier; layerFindIndicator.widthText = WndProc(Message::PointXFromPosition, 0, charRange.location + charRange.length) - WndProc(Message::PointXFromPosition, 0, charRange.location); layerFindIndicator.heightLine = WndProc(Message::TextHeight, 0, 0); MoveFindIndicatorWithBounce(YES); } else { [layerFindIndicator hideMatch]; } #endif } void ScintillaCocoa::MoveFindIndicatorWithBounce(BOOL bounce) { #if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5 if (layerFindIndicator) { CGPoint ptText = CGPointMake( WndProc(Message::PointXFromPosition, 0, layerFindIndicator.positionFind), WndProc(Message::PointYFromPosition, 0, layerFindIndicator.positionFind)); ptText.x = ptText.x - vs.fixedColumnWidth + xOffset; ptText.y += topLine * vs.lineHeight; if (!layerFindIndicator.geometryFlipped) { NSView *content = ContentView(); ptText.y = content.bounds.size.height - ptText.y; } [layerFindIndicator animateMatch: ptText bounce: bounce]; } #endif } void ScintillaCocoa::HideFindIndicator() { #if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5 if (layerFindIndicator) { [layerFindIndicator hideMatch]; } #endif }