You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notepad-plus-plus/scintilla/cocoa/ScintillaCocoa.mm

2775 lines
95 KiB

/**
* Scintilla source code edit control
* @file ScintillaCocoa.mm - Cocoa subclass of ScintillaBase
*
* Written by Mike Lischke <mlischke@sun.com>
*
* Loosely based on ScintillaMacOSX.cxx.
* Copyright 2003 by Evan Jones <ejones@uwaterloo.ca>
* Based on ScintillaGTK.cxx Copyright 1998-2002 by Neil Hodgson <neilh@scintilla.org>
* 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 <cmath>
#include <string_view>
#include <vector>
#include <optional>
#import <Cocoa/Cocoa.h>
#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
#import <QuartzCore/CAGradientLayer.h>
#endif
#import <QuartzCore/CAAnimation.h>
#import <QuartzCore/CATransaction.h>
#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<Keys>(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<Message>(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<ScintillaCocoa *>(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<ScintillaCocoa *>(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<CGContextRef>([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<int>(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<size_t>(TickReason::caret); tr<=static_cast<size_t>(TickReason::platform); tr++) {
FineTickerCancel(static_cast<TickReason>(tr));
}
ScintillaBase::Finalise();
}
//--------------------------------------------------------------------------------------------------
void ScintillaCocoa::UpdateObserver(CFRunLoopObserverRef /* observer */, CFRunLoopActivity /* activity */, void *info) {
ScintillaCocoa *sci = static_cast<ScintillaCocoa *>(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<UInt8 *>(&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<const UInt8 *>(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<unsigned char>(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<CaseFolder> ScintillaCocoa::CaseFolderForEncoding() {
if (pdoc->dbcsCodePage == SC_CP_UTF8) {
return std::make_unique<CaseFolderUnicode>();
} else {
CFStringEncoding encoding = EncodingFromCharacterSet(IsUnicodeMode(),
vs.styles[StyleDefault].characterSet);
if (pdoc->dbcsCodePage == 0) {
std::unique_ptr<CaseFolderTable> pcf = std::make_unique<CaseFolderTable>();
// Only for single byte encodings
for (int i=0x80; i<0x100; i++) {
char sCharacter[2] = "A";
sCharacter[0] = static_cast<char>(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<CaseFolderDBCS>(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<NSScrollView *>(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<ScintillaCocoa *>(ptr);
return sci->WndProc(static_cast<Message>(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<ScintillaCocoa *>(ptr);
const sptr_t returnValue = sci->WndProc(static_cast<Message>(iMessage), wParam, lParam);
*pStatus = static_cast<int>(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<sptr_t>(DirectFunction);
case Message::GetDirectStatusFunction:
return reinterpret_cast<sptr_t>(DirectStatusFunction);
case Message::GetDirectPointer:
return reinterpret_cast<sptr_t>(this);
case Message::SetBidirectional:
bidirectional = static_cast<Bidirectional>(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<sptr_t>(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<size_t>(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<size_t>(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<size_t>(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<Surface> 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<Sci::Line>(
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<Sci::Line>(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 <NSPasteboardItemDataProvider> {
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<Sci::Line>(pdoc->LineFromPosition(selStart));
Sci::Line endLine = static_cast<Sci::Line>(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<sptr_t>(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<SurfaceImpl> sw = si.AllocatePixMapImplementation(static_cast<int>(client.Width()), static_cast<int>(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<SurfaceImpl> pixmap = si.AllocatePixMapImplementation(static_cast<int>(imageRect.Width()),
static_cast<int>(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 <NSDraggingInfo> 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 <NSDraggingInfo> 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 <NSDraggingInfo> 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 <NSDraggingInfo> 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<CFIndex>(data.length)};
CFIndex usedLen = 0;
CFStringGetBytes((CFStringRef)data, rangeAll, encoding, '?',
false, NULL, 0, &usedLen);
std::vector<UInt8> buffer(usedLen);
CFStringGetBytes((CFStringRef)data, rangeAll, encoding, '?',
false, buffer.data(), usedLen, NULL);
bool rectangular = bestType == ScintillaRecPboardType;
std::string dest(reinterpret_cast<const char *>(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<Sci::Line>(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<Surface> 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<Surface> 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<int>(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<int>(ptOrigin.x);
Sci::Line newTop = std::min(static_cast<Sci::Line>(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<ScintillaNotificationProtocol> 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<short>(GetCtrlID()), static_cast<short>(FocusChange::Change)),
(uintptr_t) this);
}
//--------------------------------------------------------------------------------------------------
void ScintillaCocoa::NotifyFocus(bool focus) {
if (commandEvents && notifyProc)
notifyProc(notifyObj, WM_COMMAND, Platform::LongFromTwoShorts(static_cast<short>(GetCtrlID()),
static_cast<short>((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<SCNotification *>(&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<size_t>(TickReason::caret); tr<=static_cast<size_t>(TickReason::platform); tr++) {
if (timers[tr] == timer) {
TickFor(static_cast<TickReason>(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<Keys>(unicodeChar);
case '-':
if (modifierFlags & NSEventModifierFlagNumericPad)
return Keys::Subtract;
else
return static_cast<Keys>(unicodeChar);
case '/':
if (modifierFlags & NSEventModifierFlagNumericPad)
return Keys::Divide;
else
return static_cast<Keys>(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<Keys>(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<UniChar>(Keys::Down) && originalKey <= static_cast<UniChar>(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<int>(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<unsigned int>(event.timestamp * 1000);
}
}
//--------------------------------------------------------------------------------------------------
/**
* 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<int>(sqrt(-10.0 * event.deltaY));
else
dY = static_cast<int>(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<int>(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<unsigned int>(value.redComponent * componentMaximum),
static_cast<unsigned int>(value.greenComponent * componentMaximum),
static_cast<unsigned int>(value.blueComponent * componentMaximum),
static_cast<unsigned int>(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<char> 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<char>(WndProc(Message::GetStyleAt, charRange.location, 0));
const long style = static_cast<unsigned char>(styleByte);
std::vector<char> 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
}