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.
2380 lines
73 KiB
2380 lines
73 KiB
/** |
|
* Scintilla source code edit control |
|
* @file PlatCocoa.mm - implementation of platform facilities on macOS/Cocoa |
|
* |
|
* Written by Mike Lischke |
|
* Based on PlatMacOSX.cxx |
|
* Based on work by Evan Jones (c) 2002 <ejones@uwaterloo.ca> |
|
* Based on PlatGTK.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 2009 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 <cstddef> |
|
#include <cstdlib> |
|
#include <cassert> |
|
#include <cstring> |
|
#include <cstdio> |
|
#include <cmath> |
|
|
|
#include <stdexcept> |
|
#include <string_view> |
|
#include <vector> |
|
#include <map> |
|
#include <optional> |
|
#include <functional> |
|
#include <memory> |
|
#include <numeric> |
|
|
|
#import <Foundation/NSGeometry.h> |
|
|
|
#import "ScintillaTypes.h" |
|
#import "ScintillaMessages.h" |
|
#import "ScintillaStructures.h" |
|
|
|
#import "Debugging.h" |
|
#import "Geometry.h" |
|
#import "Platform.h" |
|
|
|
#include "XPM.h" |
|
#include "UniConversion.h" |
|
|
|
#import "ScintillaView.h" |
|
#import "ScintillaCocoa.h" |
|
#import "PlatCocoa.h" |
|
|
|
using namespace Scintilla; |
|
using namespace Scintilla::Internal; |
|
|
|
extern sptr_t scintilla_send_message(void *sci, unsigned int iMessage, uptr_t wParam, sptr_t lParam); |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Converts a Point as used by Scintilla to a Quartz-style CGPoint. |
|
*/ |
|
inline CGPoint CGPointFromPoint(Scintilla::Internal::Point pt) { |
|
return CGPointMake(pt.x, pt.y); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Converts a PRectangle as used by Scintilla to standard Obj-C NSRect structure . |
|
*/ |
|
NSRect PRectangleToNSRect(const PRectangle &rc) { |
|
return NSMakeRect(rc.left, rc.top, rc.Width(), rc.Height()); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Converts an NSRect as used by the system to a native Scintilla rectangle. |
|
*/ |
|
PRectangle NSRectToPRectangle(const NSRect &rc) { |
|
return PRectangle(rc.origin.x, rc.origin.y, NSMaxX(rc), NSMaxY(rc)); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Converts a PRectangle as used by Scintilla to a Quartz-style rectangle. |
|
*/ |
|
inline CGRect PRectangleToCGRect(PRectangle &rc) { |
|
return CGRectMake(rc.left, rc.top, rc.Width(), rc.Height()); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Converts a PRectangle as used by Scintilla to a Quartz-style rectangle. |
|
* Result is inset by strokeWidth / 2 so stroking does not go outside the rectangle. |
|
*/ |
|
inline CGRect CGRectFromPRectangleInset(PRectangle rc, XYPOSITION strokeWidth) { |
|
const XYPOSITION halfStroke = strokeWidth / 2.0f; |
|
const CGRect rect = PRectangleToCGRect(rc); |
|
return CGRectInset(rect, halfStroke, halfStroke); |
|
} |
|
|
|
//----------------- FontQuartz --------------------------------------------------------------------- |
|
|
|
class FontQuartz : public Font { |
|
public: |
|
std::unique_ptr<QuartzTextStyle> style; |
|
FontQuartz(const FontParameters &fp) { |
|
style = std::make_unique<QuartzTextStyle>(); |
|
// Create the font with attributes |
|
QuartzFont font(fp.faceName, strlen(fp.faceName), fp.size, fp.weight, fp.italic); |
|
CTFontRef fontRef = font.getFontID(); |
|
style->setFontRef(fontRef, fp.characterSet); |
|
} |
|
FontQuartz(const QuartzTextStyle *style_) { |
|
style = std::make_unique<QuartzTextStyle>(style_); |
|
} |
|
}; |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
static QuartzTextStyle *TextStyleFromFont(const Font *f) noexcept { |
|
if (f) { |
|
const FontQuartz *pfq = dynamic_cast<const FontQuartz *>(f); |
|
if (pfq) { |
|
return pfq->style.get(); |
|
} |
|
} |
|
return nullptr; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Creates a CTFontRef with the given properties. |
|
*/ |
|
std::shared_ptr<Font> Font::Allocate(const FontParameters &fp) { |
|
return std::make_shared<FontQuartz>(fp); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
// Bidirectional text support for Arabic and Hebrew. |
|
|
|
namespace { |
|
|
|
CFIndex IndexFromPosition(std::string_view text, size_t position) { |
|
const std::string_view textUptoPosition = text.substr(0, position); |
|
return UTF16Length(textUptoPosition); |
|
} |
|
|
|
// Handling representations and tabs |
|
|
|
struct Blob { |
|
XYPOSITION width; |
|
Blob(XYPOSITION width_) : width(width_) { |
|
} |
|
}; |
|
|
|
static void BlobDealloc(void *refCon) { |
|
Blob *blob = static_cast<Blob *>(refCon); |
|
delete blob; |
|
} |
|
|
|
static CGFloat BlobGetWidth(void *refCon) { |
|
Blob *blob = static_cast<Blob *>(refCon); |
|
return blob->width; |
|
} |
|
|
|
class ScreenLineLayout : public IScreenLineLayout { |
|
CTLineRef line = NULL; |
|
const std::string text; |
|
public: |
|
ScreenLineLayout(const IScreenLine *screenLine); |
|
~ScreenLineLayout(); |
|
// IScreenLineLayout implementation |
|
size_t PositionFromX(XYPOSITION xDistance, bool charPosition) override; |
|
XYPOSITION XFromPosition(size_t caretPosition) override; |
|
std::vector<Interval> FindRangeIntervals(size_t start, size_t end) override; |
|
}; |
|
|
|
ScreenLineLayout::ScreenLineLayout(const IScreenLine *screenLine) : text(screenLine->Text()) { |
|
const UInt8 *puiBuffer = reinterpret_cast<const UInt8 *>(text.data()); |
|
std::string_view sv = text; |
|
|
|
// Start with an empty mutable attributed string and add each character to it. |
|
CFMutableAttributedStringRef mas = CFAttributedStringCreateMutable(NULL, 0); |
|
|
|
for (size_t bp=0; bp<text.length();) { |
|
const unsigned char uch = text[bp]; |
|
const int utf8Status = UTF8Classify(sv); |
|
const unsigned int byteCount = utf8Status & UTF8MaskWidth; |
|
XYPOSITION repWidth = screenLine->RepresentationWidth(bp); |
|
if (uch == '\t') { |
|
// Find the size up to the tab |
|
NSMutableAttributedString *nas = (__bridge NSMutableAttributedString *)mas; |
|
const NSSize sizeUpTo = [nas size]; |
|
const XYPOSITION nextTab = screenLine->TabPositionAfter(sizeUpTo.width); |
|
repWidth = nextTab - sizeUpTo.width; |
|
} |
|
CFAttributedStringRef as = NULL; |
|
if (repWidth > 0.0f) { |
|
CTRunDelegateCallbacks callbacks = { |
|
.version = kCTRunDelegateVersion1, |
|
.dealloc = BlobDealloc, |
|
.getWidth = BlobGetWidth |
|
}; |
|
CTRunDelegateRef runDelegate = CTRunDelegateCreate(&callbacks, new Blob(repWidth)); |
|
NSMutableAttributedString *masBlob = [[NSMutableAttributedString alloc] initWithString:@"X"]; |
|
NSRange rangeX = NSMakeRange(0, 1); |
|
[masBlob addAttribute: (NSString *)kCTRunDelegateAttributeName value: (__bridge id)runDelegate range:rangeX]; |
|
CFRelease(runDelegate); |
|
as = (CFAttributedStringRef)CFBridgingRetain(masBlob); |
|
} else { |
|
CFStringRef piece = CFStringCreateWithBytes(NULL, |
|
&puiBuffer[bp], |
|
byteCount, |
|
kCFStringEncodingUTF8, |
|
false); |
|
const QuartzTextStyle *qts = TextStyleFromFont(screenLine->FontOfPosition(bp)); |
|
CFMutableDictionaryRef pieceAttributes = qts->getCTStyle(); |
|
as = CFAttributedStringCreate(NULL, piece, pieceAttributes); |
|
CFRelease(piece); |
|
} |
|
CFAttributedStringReplaceAttributedString(mas, |
|
CFRangeMake(CFAttributedStringGetLength(mas), 0), |
|
as); |
|
bp += byteCount; |
|
sv.remove_prefix(byteCount); |
|
CFRelease(as); |
|
} |
|
|
|
line = CTLineCreateWithAttributedString(mas); |
|
CFRelease(mas); |
|
} |
|
|
|
ScreenLineLayout::~ScreenLineLayout() { |
|
CFRelease(line); |
|
} |
|
|
|
size_t ScreenLineLayout::PositionFromX(XYPOSITION xDistance, bool charPosition) { |
|
if (!line) { |
|
return 0; |
|
} |
|
const CGPoint ptDistance = CGPointMake(xDistance, 0); |
|
const CFIndex offset = CTLineGetStringIndexForPosition(line, ptDistance); |
|
if (offset == kCFNotFound) { |
|
return 0; |
|
} |
|
// Convert back to UTF-8 positions |
|
return UTF8PositionFromUTF16Position(text, offset); |
|
} |
|
|
|
XYPOSITION ScreenLineLayout::XFromPosition(size_t caretPosition) { |
|
if (!line) { |
|
return 0.0; |
|
} |
|
// Convert from UTF-8 position |
|
const CFIndex caretIndex = IndexFromPosition(text, caretPosition); |
|
|
|
const CGFloat distance = CTLineGetOffsetForStringIndex(line, caretIndex, nullptr); |
|
return distance; |
|
} |
|
|
|
void AddToIntervalVector(std::vector<Interval> &vi, XYPOSITION left, XYPOSITION right) { |
|
const Interval interval = {left, right}; |
|
if (vi.empty()) { |
|
vi.push_back(interval); |
|
} else { |
|
Interval &last = vi.back(); |
|
if (std::abs(last.right-interval.left) < 0.01) { |
|
// If new left is very close to previous right then extend last item |
|
last.right = interval.right; |
|
} else { |
|
vi.push_back(interval); |
|
} |
|
} |
|
} |
|
|
|
std::vector<Interval> ScreenLineLayout::FindRangeIntervals(size_t start, size_t end) { |
|
if (!line) { |
|
return {}; |
|
} |
|
|
|
std::vector<Interval> ret; |
|
|
|
// Convert from UTF-8 position |
|
const CFIndex startIndex = IndexFromPosition(text, start); |
|
const CFIndex endIndex = IndexFromPosition(text, end); |
|
|
|
CFArrayRef runs = CTLineGetGlyphRuns(line); |
|
const CFIndex runCount = CFArrayGetCount(runs); |
|
for (CFIndex run=0; run<runCount; run++) { |
|
CTRunRef aRun = static_cast<CTRunRef>(CFArrayGetValueAtIndex(runs, run)); |
|
const CFIndex glyphCount = CTRunGetGlyphCount(aRun); |
|
const CFRange rangeAll = CFRangeMake(0, glyphCount); |
|
std::vector<CFIndex> indices(glyphCount); |
|
CTRunGetStringIndices(aRun, rangeAll, indices.data()); |
|
std::vector<CGPoint> positions(glyphCount); |
|
CTRunGetPositions(aRun, rangeAll, positions.data()); |
|
std::vector<CGSize> advances(glyphCount); |
|
CTRunGetAdvances(aRun, rangeAll, advances.data()); |
|
for (CFIndex glyph=0; glyph<glyphCount; glyph++) { |
|
const CFIndex glyphIndex = indices[glyph]; |
|
const XYPOSITION xPosition = positions[glyph].x; |
|
const XYPOSITION width = advances[glyph].width; |
|
if ((glyphIndex >= startIndex) && (glyphIndex < endIndex)) { |
|
AddToIntervalVector(ret, xPosition, xPosition + width); |
|
} |
|
} |
|
} |
|
return ret; |
|
} |
|
|
|
// Helper for SurfaceImpl::MeasureWidths that examines the glyph runs in a layout |
|
|
|
void GetPositions(CTLineRef line, std::vector<CGFloat> &positions) { |
|
|
|
// Find the advances of the text |
|
std::vector<CGFloat> lineAdvances(positions.size()); |
|
CFArrayRef runs = CTLineGetGlyphRuns(line); |
|
const CFIndex runCount = CFArrayGetCount(runs); |
|
for (CFIndex run=0; run<runCount; run++) { |
|
CTRunRef aRun = static_cast<CTRunRef>(CFArrayGetValueAtIndex(runs, run)); |
|
const CFIndex glyphCount = CTRunGetGlyphCount(aRun); |
|
const CFRange rangeAll = CFRangeMake(0, glyphCount); |
|
std::vector<CFIndex> indices(glyphCount); |
|
CTRunGetStringIndices(aRun, rangeAll, indices.data()); |
|
std::vector<CGSize> advances(glyphCount); |
|
CTRunGetAdvances(aRun, rangeAll, advances.data()); |
|
for (CFIndex glyph=0; glyph<glyphCount; glyph++) { |
|
const CFIndex glyphIndex = indices[glyph]; |
|
if (glyphIndex >= positions.size()) { |
|
return; |
|
} |
|
lineAdvances[glyphIndex] = advances[glyph].width; |
|
} |
|
} |
|
|
|
// Accumulate advances into positions |
|
std::partial_sum(lineAdvances.begin(), lineAdvances.end(), |
|
positions.begin(), std::plus<CGFloat>()); |
|
} |
|
|
|
const Supports SupportsCocoa[] = { |
|
Supports::LineDrawsFinal, |
|
Supports::PixelDivisions, |
|
Supports::FractionalStrokeWidth, |
|
Supports::TranslucentStroke, |
|
Supports::PixelModification, |
|
Supports::ThreadSafeMeasureWidths, |
|
}; |
|
|
|
} |
|
|
|
//----------------- SurfaceImpl -------------------------------------------------------------------- |
|
|
|
SurfaceImpl::SurfaceImpl() { |
|
gc = NULL; |
|
|
|
bitmapData.reset(); // Release will try and delete bitmapData if != nullptr |
|
bitmapWidth = 0; |
|
bitmapHeight = 0; |
|
|
|
Release(); |
|
} |
|
|
|
SurfaceImpl::SurfaceImpl(const SurfaceImpl *surface, int width, int height) { |
|
|
|
// Create a new bitmap context, along with the RAM for the bitmap itself |
|
bitmapWidth = width; |
|
bitmapHeight = height; |
|
|
|
const int bitmapBytesPerRow = (width * BYTES_PER_PIXEL); |
|
const int bitmapByteCount = (bitmapBytesPerRow * height); |
|
|
|
// Create an RGB color space. |
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); |
|
if (colorSpace == NULL) |
|
return; |
|
|
|
// Create the bitmap. |
|
bitmapData.reset(new uint8_t[bitmapByteCount]); |
|
// create the context |
|
gc = CGBitmapContextCreate(bitmapData.get(), |
|
width, |
|
height, |
|
BITS_PER_COMPONENT, |
|
bitmapBytesPerRow, |
|
colorSpace, |
|
kCGImageAlphaPremultipliedLast); |
|
|
|
if (gc == NULL) { |
|
// the context couldn't be created for some reason, |
|
// and we have no use for the bitmap without the context |
|
bitmapData.reset(); |
|
} |
|
|
|
// the context retains the color space, so we can release it |
|
CGColorSpaceRelease(colorSpace); |
|
|
|
if (gc && bitmapData) { |
|
// "Erase" to white. |
|
CGContextClearRect(gc, CGRectMake(0, 0, width, height)); |
|
CGContextSetRGBFillColor(gc, 1.0, 1.0, 1.0, 1.0); |
|
CGContextFillRect(gc, CGRectMake(0, 0, width, height)); |
|
} |
|
|
|
mode = surface->mode; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
SurfaceImpl::~SurfaceImpl() { |
|
Clear(); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
bool SurfaceImpl::UnicodeMode() const noexcept { |
|
return mode.codePage == SC_CP_UTF8; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::Clear() { |
|
if (bitmapData) { |
|
bitmapData.reset(); |
|
// We only "own" the graphics context if we are a bitmap context |
|
if (gc) |
|
CGContextRelease(gc); |
|
} |
|
gc = NULL; |
|
|
|
bitmapWidth = 0; |
|
bitmapHeight = 0; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::Release() noexcept { |
|
Clear(); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
bool SurfaceImpl::Initialised() { |
|
// We are initalised if the graphics context is not null |
|
return gc != NULL;// || port != NULL; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::Init(WindowID) { |
|
// To be able to draw, the surface must get a CGContext handle. We save the graphics port, |
|
// then acquire/release the context on an as-need basis (see above). |
|
// XXX Docs on QDBeginCGContext are light, a better way to do this would be good. |
|
// AFAIK we should not hold onto a context retrieved this way, thus the need for |
|
// acquire/release of the context. |
|
|
|
Release(); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::Init(SurfaceID sid, WindowID) { |
|
Release(); |
|
gc = static_cast<CGContextRef>(sid); |
|
CGContextSetLineWidth(gc, 1.0); |
|
} |
|
|
|
std::unique_ptr<Surface> SurfaceImpl::AllocatePixMap(int width, int height) { |
|
return std::make_unique<SurfaceImpl>(this, width, height); |
|
} |
|
|
|
std::unique_ptr<SurfaceImpl> SurfaceImpl::AllocatePixMapImplementation(int width, int height) { |
|
return std::make_unique<SurfaceImpl>(this, width, height); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::SetMode(SurfaceMode mode_) { |
|
mode = mode_; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
int SurfaceImpl::SupportsFeature(Supports feature) noexcept { |
|
for (const Supports f : SupportsCocoa) { |
|
if (f == feature) |
|
return 1; |
|
} |
|
return 0; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::FillColour(ColourRGBA fill) { |
|
// Set the Fill color to match |
|
CGContextSetRGBFillColor(gc, |
|
fill.GetRedComponent(), |
|
fill.GetGreenComponent(), |
|
fill.GetBlueComponent(), |
|
fill.GetAlphaComponent()); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::PenColourAlpha(ColourRGBA fore) { |
|
// Set the Stroke color to match |
|
CGContextSetRGBStrokeColor(gc, |
|
fore.GetRedComponent(), |
|
fore.GetGreenComponent(), |
|
fore.GetBlueComponent(), |
|
fore.GetAlphaComponent()); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::SetFillStroke(FillStroke fillStroke) { |
|
FillColour(fillStroke.fill.colour); |
|
PenColourAlpha(fillStroke.stroke.colour); |
|
CGContextSetLineWidth(gc, fillStroke.stroke.width); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
CGImageRef SurfaceImpl::CreateImage() { |
|
// For now, assume that CreateImage can only be called on PixMap surfaces. |
|
if (!bitmapData) |
|
return NULL; |
|
|
|
CGContextFlush(gc); |
|
|
|
// Create an RGB color space. |
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); |
|
if (colorSpace == NULL) |
|
return NULL; |
|
|
|
const int bitmapBytesPerRow = bitmapWidth * BYTES_PER_PIXEL; |
|
const int bitmapByteCount = bitmapBytesPerRow * bitmapHeight; |
|
|
|
// Make a copy of the bitmap data for the image creation and divorce it |
|
// From the SurfaceImpl lifetime |
|
CFDataRef dataRef = CFDataCreate(kCFAllocatorDefault, bitmapData.get(), bitmapByteCount); |
|
|
|
// Create a data provider. |
|
CGDataProviderRef dataProvider = CGDataProviderCreateWithCFData(dataRef); |
|
|
|
CGImageRef image = NULL; |
|
if (dataProvider != NULL) { |
|
// Create the CGImage. |
|
image = CGImageCreate(bitmapWidth, |
|
bitmapHeight, |
|
BITS_PER_COMPONENT, |
|
BITS_PER_PIXEL, |
|
bitmapBytesPerRow, |
|
colorSpace, |
|
kCGImageAlphaPremultipliedLast, |
|
dataProvider, |
|
NULL, |
|
0, |
|
kCGRenderingIntentDefault); |
|
} |
|
|
|
// The image retains the color space, so we can release it. |
|
CGColorSpaceRelease(colorSpace); |
|
colorSpace = NULL; |
|
|
|
// Done with the data provider. |
|
CGDataProviderRelease(dataProvider); |
|
dataProvider = NULL; |
|
|
|
// Done with the data provider. |
|
CFRelease(dataRef); |
|
|
|
return image; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Returns the vertical logical device resolution of the main monitor. |
|
* This is no longer called. |
|
* For Cocoa, all screens are treated as 72 DPI, even retina displays. |
|
*/ |
|
int SurfaceImpl::LogPixelsY() { |
|
return 72; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Returns the number of device pixels per logical pixel. |
|
* 1 for older displays and 2 for retina displays. Potentially 3 for some phones. |
|
*/ |
|
int SurfaceImpl::PixelDivisions() { |
|
if (gc) { |
|
const CGSize szDevice = CGContextConvertSizeToDeviceSpace(gc, CGSizeMake(1.0, 1.0)); |
|
const int devicePixels = std::round(szDevice.width); |
|
assert(devicePixels == 1 || devicePixels == 2); |
|
return devicePixels; |
|
} |
|
return 1; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Converts the logical font height in points into a device height. |
|
* For Cocoa, points are always used for the result even on retina displays. |
|
*/ |
|
int SurfaceImpl::DeviceHeightFont(int points) { |
|
return points; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::LineDraw(Point start, Point end, Stroke stroke) { |
|
PenColourAlpha(stroke.colour); |
|
CGContextSetLineWidth(gc, stroke.width); |
|
|
|
CGContextBeginPath(gc); |
|
CGContextMoveToPoint(gc, start.x, start.y); |
|
CGContextAddLineToPoint(gc, end.x, end.y); |
|
CGContextStrokePath(gc); |
|
|
|
CGContextSetLineWidth(gc, 1.0f); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::PolyLine(const Point *pts, size_t npts, Stroke stroke) { |
|
PLATFORM_ASSERT(gc && (npts > 1)); |
|
if (!gc || (npts <= 1)) { |
|
return; |
|
} |
|
PenColourAlpha(stroke.colour); |
|
CGContextSetLineWidth(gc, stroke.width); |
|
CGContextBeginPath(gc); |
|
CGContextMoveToPoint(gc, pts[0].x, pts[0].y); |
|
for (size_t i = 1; i < npts; i++) { |
|
CGContextAddLineToPoint(gc, pts[i].x, pts[i].y); |
|
} |
|
CGContextStrokePath(gc); |
|
|
|
CGContextSetLineWidth(gc, 1.0f); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::Polygon(const Scintilla::Internal::Point *pts, size_t npts, FillStroke fillStroke) { |
|
std::vector<CGPoint> points; |
|
std::transform(pts, pts + npts, std::back_inserter(points), CGPointFromPoint); |
|
|
|
CGContextBeginPath(gc); |
|
|
|
SetFillStroke(fillStroke); |
|
|
|
// Draw the polygon |
|
CGContextAddLines(gc, points.data(), npts); |
|
|
|
// Explicitly close the path, so it is closed for stroking AND filling (implicit close = filling only) |
|
CGContextClosePath(gc); |
|
CGContextDrawPath(gc, kCGPathFillStroke); |
|
|
|
// Restore as not all paths set |
|
CGContextSetLineWidth(gc, 1.0f); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::RectangleDraw(PRectangle rc, FillStroke fillStroke) { |
|
if (!gc) |
|
return; |
|
CGContextBeginPath(gc); |
|
SetFillStroke(fillStroke); |
|
|
|
CGContextAddRect(gc, CGRectFromPRectangleInset(rc, fillStroke.stroke.width)); |
|
|
|
CGContextDrawPath(gc, kCGPathFillStroke); |
|
|
|
// Restore as not all paths set |
|
CGContextSetLineWidth(gc, 1.0f); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::RectangleFrame(PRectangle rc, Stroke stroke) { |
|
if (!gc) |
|
return; |
|
|
|
CGContextBeginPath(gc); |
|
PenColourAlpha(stroke.colour); |
|
CGContextSetLineWidth(gc, stroke.width); |
|
|
|
CGContextAddRect(gc, CGRectFromPRectangleInset(rc, stroke.width)); |
|
|
|
CGContextDrawPath(gc, kCGPathStroke); |
|
|
|
// Restore as not all paths set |
|
CGContextSetLineWidth(gc, 1.0f); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::FillRectangle(PRectangle rc, Fill fill) { |
|
if (gc) { |
|
FillColour(fill.colour); |
|
CGRect rect = PRectangleToCGRect(rc); |
|
CGContextFillRect(gc, rect); |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::FillRectangleAligned(PRectangle rc, Fill fill) { |
|
FillRectangle(PixelAlign(rc, PixelDivisions()), fill); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
static void drawImageRefCallback(void *info, CGContextRef gc) { |
|
CGImageRef pattern = static_cast<CGImageRef>(info); |
|
CGContextDrawImage(gc, CGRectMake(0, 0, CGImageGetWidth(pattern), CGImageGetHeight(pattern)), pattern); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
static void releaseImageRefCallback(void *info) { |
|
CGImageRelease(static_cast<CGImageRef>(info)); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::FillRectangle(PRectangle rc, Surface &surfacePattern) { |
|
SurfaceImpl &patternSurface = static_cast<SurfaceImpl &>(surfacePattern); |
|
|
|
// For now, assume that copy can only be called on PixMap surfaces. Shows up black. |
|
CGImageRef image = patternSurface.CreateImage(); |
|
if (image == NULL) { |
|
FillRectangle(rc, ColourRGBA::FromRGB(0)); |
|
return; |
|
} |
|
|
|
const CGPatternCallbacks drawImageCallbacks = { 0, drawImageRefCallback, releaseImageRefCallback }; |
|
|
|
CGPatternRef pattern = CGPatternCreate(image, |
|
CGRectMake(0, 0, patternSurface.bitmapWidth, patternSurface.bitmapHeight), |
|
CGAffineTransformIdentity, |
|
patternSurface.bitmapWidth, |
|
patternSurface.bitmapHeight, |
|
kCGPatternTilingNoDistortion, |
|
true, |
|
&drawImageCallbacks |
|
); |
|
if (pattern != NULL) { |
|
// Create a pattern color space |
|
CGColorSpaceRef colorSpace = CGColorSpaceCreatePattern(NULL); |
|
if (colorSpace != NULL) { |
|
|
|
CGContextSaveGState(gc); |
|
CGContextSetFillColorSpace(gc, colorSpace); |
|
|
|
// Unlike the documentation, you MUST pass in a "components" parameter: |
|
// For coloured patterns it is the alpha value. |
|
const CGFloat alpha = 1.0; |
|
CGContextSetFillPattern(gc, pattern, &alpha); |
|
CGContextFillRect(gc, PRectangleToCGRect(rc)); |
|
CGContextRestoreGState(gc); |
|
// Free the color space, the pattern and image |
|
CGColorSpaceRelease(colorSpace); |
|
} /* colorSpace != NULL */ |
|
colorSpace = NULL; |
|
CGPatternRelease(pattern); |
|
pattern = NULL; |
|
} /* pattern != NULL */ |
|
} |
|
|
|
void SurfaceImpl::RoundedRectangle(PRectangle rc, FillStroke fillStroke) { |
|
// This is only called from the margin marker drawing code for MarkerSymbol::RoundRect |
|
// The Win32 version does |
|
// ::RoundRect(hdc, rc.left + 1, rc.top, rc.right - 1, rc.bottom, 8, 8 ); |
|
// which is a rectangle with rounded corners each having a radius of 4 pixels. |
|
// It would be almost as good just cutting off the corners with lines at |
|
// 45 degrees as is done on GTK+. |
|
|
|
// Create a rectangle with semicircles at the corners |
|
const int MAX_RADIUS = 4; |
|
const int radius = std::min(MAX_RADIUS, static_cast<int>(std::min(rc.Height()/2, rc.Width()/2))); |
|
|
|
// Points go clockwise, starting from just below the top left |
|
// Corners are kept together, so we can easily create arcs to connect them |
|
CGPoint corners[4][3] = { |
|
{ |
|
{ rc.left, rc.top + radius }, |
|
{ rc.left, rc.top }, |
|
{ rc.left + radius, rc.top }, |
|
}, |
|
{ |
|
{ rc.right - radius - 1, rc.top }, |
|
{ rc.right - 1, rc.top }, |
|
{ rc.right - 1, rc.top + radius }, |
|
}, |
|
{ |
|
{ rc.right - 1, rc.bottom - radius - 1 }, |
|
{ rc.right - 1, rc.bottom - 1 }, |
|
{ rc.right - radius - 1, rc.bottom - 1 }, |
|
}, |
|
{ |
|
{ rc.left + radius, rc.bottom - 1 }, |
|
{ rc.left, rc.bottom - 1 }, |
|
{ rc.left, rc.bottom - radius - 1 }, |
|
}, |
|
}; |
|
|
|
// Align the points in the middle of the pixels |
|
for (int i = 0; i < 4; ++ i) { |
|
for (int j = 0; j < 3; ++ j) { |
|
corners[i][j].x += 0.5; |
|
corners[i][j].y += 0.5; |
|
} |
|
} |
|
|
|
SetFillStroke(fillStroke); |
|
|
|
// Move to the last point to begin the path |
|
CGContextBeginPath(gc); |
|
CGContextMoveToPoint(gc, corners[3][2].x, corners[3][2].y); |
|
|
|
for (int i = 0; i < 4; ++ i) { |
|
CGContextAddLineToPoint(gc, corners[i][0].x, corners[i][0].y); |
|
CGContextAddArcToPoint(gc, corners[i][1].x, corners[i][1].y, corners[i][2].x, corners[i][2].y, radius); |
|
} |
|
|
|
// Close the path to enclose it for stroking and for filling, then draw it |
|
CGContextClosePath(gc); |
|
CGContextDrawPath(gc, kCGPathFillStroke); |
|
|
|
// Restore as not all paths set |
|
CGContextSetLineWidth(gc, 1.0f); |
|
} |
|
|
|
// DrawChamferedRectangle is a helper function for AlphaRectangle that either fills or strokes a |
|
// rectangle with its corners chamfered at 45 degrees. |
|
static void DrawChamferedRectangle(CGContextRef gc, PRectangle rc, int cornerSize, CGPathDrawingMode mode) { |
|
// Points go clockwise, starting from just below the top left |
|
CGPoint corners[4][2] = { |
|
{ |
|
{ rc.left, rc.top + cornerSize }, |
|
{ rc.left + cornerSize, rc.top }, |
|
}, |
|
{ |
|
{ rc.right - cornerSize - 1, rc.top }, |
|
{ rc.right - 1, rc.top + cornerSize }, |
|
}, |
|
{ |
|
{ rc.right - 1, rc.bottom - cornerSize - 1 }, |
|
{ rc.right - cornerSize - 1, rc.bottom - 1 }, |
|
}, |
|
{ |
|
{ rc.left + cornerSize, rc.bottom - 1 }, |
|
{ rc.left, rc.bottom - cornerSize - 1 }, |
|
}, |
|
}; |
|
|
|
// Align the points in the middle of the pixels |
|
for (int i = 0; i < 4; ++ i) { |
|
for (int j = 0; j < 2; ++ j) { |
|
corners[i][j].x += 0.5; |
|
corners[i][j].y += 0.5; |
|
} |
|
} |
|
|
|
// Move to the last point to begin the path |
|
CGContextBeginPath(gc); |
|
CGContextMoveToPoint(gc, corners[3][1].x, corners[3][1].y); |
|
|
|
for (int i = 0; i < 4; ++ i) { |
|
CGContextAddLineToPoint(gc, corners[i][0].x, corners[i][0].y); |
|
CGContextAddLineToPoint(gc, corners[i][1].x, corners[i][1].y); |
|
} |
|
|
|
// Close the path to enclose it for stroking and for filling, then draw it |
|
CGContextClosePath(gc); |
|
CGContextDrawPath(gc, mode); |
|
} |
|
|
|
void Scintilla::Internal::SurfaceImpl::AlphaRectangle(PRectangle rc, XYPOSITION cornerSize, FillStroke fillStroke) { |
|
if (gc) { |
|
const XYPOSITION halfStroke = fillStroke.stroke.width / 2.0f; |
|
// Set the Fill color to match |
|
FillColour(fillStroke.fill.colour); |
|
PenColourAlpha(fillStroke.stroke.colour); |
|
PRectangle rcFill = rc; |
|
if (cornerSize == 0) { |
|
// A simple rectangle, no rounded corners |
|
if (fillStroke.fill.colour == fillStroke.stroke.colour) { |
|
// Optimization for simple case |
|
CGRect rect = PRectangleToCGRect(rcFill); |
|
CGContextFillRect(gc, rect); |
|
} else { |
|
rcFill.left += fillStroke.stroke.width; |
|
rcFill.top += fillStroke.stroke.width; |
|
rcFill.right -= fillStroke.stroke.width; |
|
rcFill.bottom -= fillStroke.stroke.width; |
|
CGRect rect = PRectangleToCGRect(rcFill); |
|
CGContextFillRect(gc, rect); |
|
CGContextAddRect(gc, CGRectMake(rc.left + halfStroke, rc.top + halfStroke, |
|
rc.Width() - fillStroke.stroke.width, rc.Height() - fillStroke.stroke.width)); |
|
CGContextStrokeRectWithWidth(gc, |
|
CGRectMake(rc.left + halfStroke, rc.top + halfStroke, |
|
rc.Width() - fillStroke.stroke.width, rc.Height() - fillStroke.stroke.width), |
|
fillStroke.stroke.width); |
|
} |
|
} else { |
|
// Approximate rounded corners with 45 degree chamfers. |
|
// Drawing real circular arcs often leaves some over- or under-drawn pixels. |
|
if (fillStroke.fill.colour == fillStroke.stroke.colour) { |
|
// Specializing this case avoids a few stray light/dark pixels in corners. |
|
rcFill.left -= halfStroke; |
|
rcFill.top -= halfStroke; |
|
rcFill.right += halfStroke; |
|
rcFill.bottom += halfStroke; |
|
DrawChamferedRectangle(gc, rcFill, cornerSize, kCGPathFill); |
|
} else { |
|
rcFill.left += halfStroke; |
|
rcFill.top += halfStroke; |
|
rcFill.right -= halfStroke; |
|
rcFill.bottom -= halfStroke; |
|
DrawChamferedRectangle(gc, rcFill, cornerSize-fillStroke.stroke.width, kCGPathFill); |
|
DrawChamferedRectangle(gc, rc, cornerSize, kCGPathStroke); |
|
} |
|
} |
|
} |
|
} |
|
|
|
void Scintilla::Internal::SurfaceImpl::GradientRectangle(PRectangle rc, const std::vector<ColourStop> &stops, GradientOptions options) { |
|
if (!gc) { |
|
return; |
|
} |
|
|
|
CGPoint ptStart = CGPointMake(rc.left, rc.top); |
|
CGPoint ptEnd = CGPointMake(rc.left, rc.bottom); |
|
if (options == GradientOptions::leftToRight) { |
|
ptEnd = CGPointMake(rc.right, rc.top); |
|
} |
|
|
|
std::vector<CGFloat> components; |
|
std::vector<CGFloat> locations; |
|
for (const ColourStop &stop : stops) { |
|
locations.push_back(stop.position); |
|
components.push_back(stop.colour.GetRedComponent()); |
|
components.push_back(stop.colour.GetGreenComponent()); |
|
components.push_back(stop.colour.GetBlueComponent()); |
|
components.push_back(stop.colour.GetAlphaComponent()); |
|
} |
|
|
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); |
|
if (!colorSpace) { |
|
return; |
|
} |
|
|
|
CGGradientRef gradiantRef = CGGradientCreateWithColorComponents(colorSpace, |
|
components.data(), |
|
locations.data(), |
|
locations.size()); |
|
if (gradiantRef) { |
|
CGContextSaveGState(gc); |
|
CGRect rect = PRectangleToCGRect(rc); |
|
CGContextClipToRect(gc, rect); |
|
CGContextBeginPath(gc); |
|
CGContextAddRect(gc, rect); |
|
CGContextClosePath(gc); |
|
CGContextDrawLinearGradient(gc, gradiantRef, ptStart, ptEnd, 0); |
|
CGGradientRelease(gradiantRef); |
|
CGContextRestoreGState(gc); |
|
} |
|
CGColorSpaceRelease(colorSpace); |
|
} |
|
|
|
static void ProviderReleaseData(void *, const void *data, size_t) { |
|
const unsigned char *pixels = static_cast<const unsigned char *>(data); |
|
delete []pixels; |
|
} |
|
|
|
static CGImageRef ImageCreateFromRGBA(int width, int height, const unsigned char *pixelsImage, bool invert) { |
|
CGImageRef image = 0; |
|
|
|
// Create an RGB color space. |
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); |
|
if (colorSpace) { |
|
const int bitmapBytesPerRow = width * 4; |
|
const int bitmapByteCount = bitmapBytesPerRow * height; |
|
|
|
// Create a data provider. |
|
CGDataProviderRef dataProvider = 0; |
|
if (invert) { |
|
unsigned char *pixelsUpsideDown = new unsigned char[bitmapByteCount]; |
|
|
|
for (int y=0; y<height; y++) { |
|
int yInverse = height - y - 1; |
|
memcpy(pixelsUpsideDown + y * bitmapBytesPerRow, |
|
pixelsImage + yInverse * bitmapBytesPerRow, |
|
bitmapBytesPerRow); |
|
} |
|
|
|
dataProvider = CGDataProviderCreateWithData( |
|
NULL, pixelsUpsideDown, bitmapByteCount, ProviderReleaseData); |
|
} else { |
|
dataProvider = CGDataProviderCreateWithData( |
|
NULL, pixelsImage, bitmapByteCount, NULL); |
|
|
|
} |
|
if (dataProvider) { |
|
// Create the CGImage. |
|
image = CGImageCreate(width, |
|
height, |
|
8, |
|
8 * 4, |
|
bitmapBytesPerRow, |
|
colorSpace, |
|
kCGImageAlphaLast, |
|
dataProvider, |
|
NULL, |
|
0, |
|
kCGRenderingIntentDefault); |
|
|
|
CGDataProviderRelease(dataProvider); |
|
} |
|
|
|
// The image retains the color space, so we can release it. |
|
CGColorSpaceRelease(colorSpace); |
|
} |
|
return image; |
|
} |
|
|
|
void SurfaceImpl::DrawRGBAImage(PRectangle rc, int width, int height, const unsigned char *pixelsImage) { |
|
CGImageRef image = ImageCreateFromRGBA(width, height, pixelsImage, true); |
|
if (image) { |
|
CGRect drawRect = CGRectMake(rc.left, rc.top, rc.Width(), rc.Height()); |
|
CGContextDrawImage(gc, drawRect, image); |
|
CGImageRelease(image); |
|
} |
|
} |
|
|
|
void SurfaceImpl::Ellipse(PRectangle rc, FillStroke fillStroke) { |
|
const CGRect ellipseRect = CGRectFromPRectangleInset(rc, fillStroke.stroke.width / 2.0f); |
|
SetFillStroke(fillStroke); |
|
CGContextBeginPath(gc); |
|
CGContextAddEllipseInRect(gc, ellipseRect); |
|
CGContextDrawPath(gc, kCGPathFillStroke); |
|
// Restore as not all paths set |
|
CGContextSetLineWidth(gc, 1.0f); |
|
} |
|
|
|
void SurfaceImpl::Stadium(PRectangle rc, FillStroke fillStroke, Ends ends) { |
|
const CGFloat midLine = rc.Centre().y; |
|
const CGFloat piOn2 = acos(0.0); |
|
const XYPOSITION halfStroke = fillStroke.stroke.width / 2.0f; |
|
const float radius = rc.Height() / 2.0f - halfStroke; |
|
PRectangle rcInner = rc; |
|
rcInner.left += radius; |
|
rcInner.right -= radius; |
|
|
|
SetFillStroke(fillStroke); |
|
CGContextBeginPath(gc); |
|
|
|
const Ends leftSide = static_cast<Ends>(static_cast<int>(ends) & 0xf); |
|
const Ends rightSide = static_cast<Ends>(static_cast<int>(ends) & 0xf0); |
|
switch (leftSide) { |
|
case Ends::leftFlat: |
|
CGContextMoveToPoint(gc, rc.left + halfStroke, rc.top + halfStroke); |
|
CGContextAddLineToPoint(gc, rc.left + halfStroke, rc.bottom - halfStroke); |
|
break; |
|
case Ends::leftAngle: |
|
CGContextMoveToPoint(gc, rcInner.left + halfStroke, rc.top + halfStroke); |
|
CGContextAddLineToPoint(gc, rc.left + halfStroke, rc.Centre().y); |
|
CGContextAddLineToPoint(gc, rcInner.left + halfStroke, rc.bottom - halfStroke); |
|
break; |
|
case Ends::semiCircles: |
|
default: |
|
CGContextMoveToPoint(gc, rcInner.left + halfStroke, rc.top + halfStroke); |
|
CGContextAddArc(gc, rcInner.left + halfStroke, midLine, radius, -piOn2, piOn2, 1); |
|
break; |
|
} |
|
|
|
switch (rightSide) { |
|
case Ends::rightFlat: |
|
CGContextAddLineToPoint(gc, rc.right - halfStroke, rc.bottom - halfStroke); |
|
CGContextAddLineToPoint(gc, rc.right - halfStroke, rc.top + halfStroke); |
|
break; |
|
case Ends::rightAngle: |
|
CGContextAddLineToPoint(gc, rcInner.right - halfStroke, rc.bottom - halfStroke); |
|
CGContextAddLineToPoint(gc, rc.right - halfStroke, rc.Centre().y); |
|
CGContextAddLineToPoint(gc, rcInner.right - halfStroke, rc.top + halfStroke); |
|
break; |
|
case Ends::semiCircles: |
|
default: |
|
CGContextAddLineToPoint(gc, rcInner.right - halfStroke, rc.bottom - halfStroke); |
|
CGContextAddArc(gc, rcInner.right - halfStroke, midLine, radius, piOn2, -piOn2, 1); |
|
break; |
|
} |
|
|
|
// Close the path to enclose it for stroking and for filling, then draw it |
|
CGContextClosePath(gc); |
|
CGContextDrawPath(gc, kCGPathFillStroke); |
|
|
|
CGContextSetLineWidth(gc, 1.0f); |
|
} |
|
|
|
void SurfaceImpl::CopyImageRectangle(SurfaceImpl *source, PRectangle srcRect, PRectangle dstRect) { |
|
CGImageRef image = source->CreateImage(); |
|
|
|
CGRect src = PRectangleToCGRect(srcRect); |
|
CGRect dst = PRectangleToCGRect(dstRect); |
|
|
|
/* source from QuickDrawToQuartz2D.pdf on developer.apple.com */ |
|
const float w = static_cast<float>(CGImageGetWidth(image)); |
|
const float h = static_cast<float>(CGImageGetHeight(image)); |
|
CGRect drawRect = CGRectMake(0, 0, w, h); |
|
if (!CGRectEqualToRect(src, dst)) { |
|
CGFloat sx = CGRectGetWidth(dst) / CGRectGetWidth(src); |
|
CGFloat sy = CGRectGetHeight(dst) / CGRectGetHeight(src); |
|
CGFloat dx = CGRectGetMinX(dst) - (CGRectGetMinX(src) * sx); |
|
CGFloat dy = CGRectGetMinY(dst) - (CGRectGetMinY(src) * sy); |
|
drawRect = CGRectMake(dx, dy, w*sx, h*sy); |
|
} |
|
CGContextSaveGState(gc); |
|
CGContextClipToRect(gc, dst); |
|
CGContextDrawImage(gc, drawRect, image); |
|
CGContextRestoreGState(gc); |
|
CGImageRelease(image); |
|
} |
|
|
|
void SurfaceImpl::Copy(PRectangle rc, Scintilla::Internal::Point from, Surface &surfaceSource) { |
|
// Maybe we have to make the Surface two contexts: |
|
// a bitmap context which we do all the drawing on, and then a "real" context |
|
// which we copy the output to when we call "Synchronize". Ugh! Gross and slow! |
|
|
|
// For now, assume that copy can only be called on PixMap surfaces |
|
SurfaceImpl &source = static_cast<SurfaceImpl &>(surfaceSource); |
|
|
|
// Get the CGImageRef |
|
CGImageRef image = source.CreateImage(); |
|
// If we could not get an image reference, fill the rectangle black |
|
if (image == NULL) { |
|
FillRectangle(rc, ColourRGBA::FromRGB(0)); |
|
return; |
|
} |
|
|
|
// Now draw the image on the surface |
|
|
|
// Some fancy clipping work is required here: draw only inside of rc |
|
CGContextSaveGState(gc); |
|
CGContextClipToRect(gc, PRectangleToCGRect(rc)); |
|
|
|
//Platform::DebugPrintf(stderr, "Copy: CGContextDrawImage: (%d, %d) - (%d X %d)\n", rc.left - from.x, rc.top - from.y, source.bitmapWidth, source.bitmapHeight ); |
|
CGContextDrawImage(gc, CGRectMake(rc.left - from.x, rc.top - from.y, source.bitmapWidth, source.bitmapHeight), image); |
|
|
|
// Undo the clipping fun |
|
CGContextRestoreGState(gc); |
|
|
|
// Done with the image |
|
CGImageRelease(image); |
|
image = NULL; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
// Bidirectional text support for Arabic and Hebrew. |
|
|
|
std::unique_ptr<IScreenLineLayout> SurfaceImpl::Layout(const IScreenLine *screenLine) { |
|
return std::make_unique<ScreenLineLayout>(screenLine); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::DrawTextNoClip(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, |
|
ColourRGBA fore, ColourRGBA back) { |
|
FillRectangleAligned(rc, back); |
|
DrawTextTransparent(rc, font_, ybase, text, fore); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::DrawTextClipped(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, |
|
ColourRGBA fore, ColourRGBA back) { |
|
CGContextSaveGState(gc); |
|
CGContextClipToRect(gc, PRectangleToCGRect(rc)); |
|
DrawTextNoClip(rc, font_, ybase, text, fore, back); |
|
CGContextRestoreGState(gc); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
CFStringEncoding EncodingFromCharacterSet(bool unicode, CharacterSet characterSet) { |
|
if (unicode) |
|
return kCFStringEncodingUTF8; |
|
|
|
// Unsupported -> Latin1 as reasonably safe |
|
enum { notSupported = kCFStringEncodingISOLatin1}; |
|
|
|
switch (characterSet) { |
|
case CharacterSet::Ansi: |
|
return kCFStringEncodingISOLatin1; |
|
case CharacterSet::Default: |
|
return kCFStringEncodingISOLatin1; |
|
case CharacterSet::Baltic: |
|
return kCFStringEncodingWindowsBalticRim; |
|
case CharacterSet::ChineseBig5: |
|
return kCFStringEncodingBig5; |
|
case CharacterSet::EastEurope: |
|
return kCFStringEncodingWindowsLatin2; |
|
case CharacterSet::GB2312: |
|
return kCFStringEncodingGB_18030_2000; |
|
case CharacterSet::Greek: |
|
return kCFStringEncodingWindowsGreek; |
|
case CharacterSet::Hangul: |
|
return kCFStringEncodingEUC_KR; |
|
case CharacterSet::Mac: |
|
return kCFStringEncodingMacRoman; |
|
case CharacterSet::Oem: |
|
return kCFStringEncodingISOLatin1; |
|
case CharacterSet::Russian: |
|
return kCFStringEncodingKOI8_R; |
|
case CharacterSet::Cyrillic: |
|
return kCFStringEncodingWindowsCyrillic; |
|
case CharacterSet::ShiftJis: |
|
return kCFStringEncodingShiftJIS; |
|
case CharacterSet::Symbol: |
|
return kCFStringEncodingMacSymbol; |
|
case CharacterSet::Turkish: |
|
return kCFStringEncodingWindowsLatin5; |
|
case CharacterSet::Johab: |
|
return kCFStringEncodingWindowsKoreanJohab; |
|
case CharacterSet::Hebrew: |
|
return kCFStringEncodingWindowsHebrew; |
|
case CharacterSet::Arabic: |
|
return kCFStringEncodingWindowsArabic; |
|
case CharacterSet::Vietnamese: |
|
return kCFStringEncodingWindowsVietnamese; |
|
case CharacterSet::Thai: |
|
return kCFStringEncodingISOLatinThai; |
|
case CharacterSet::Iso8859_15: |
|
return kCFStringEncodingISOLatin1; |
|
default: |
|
return notSupported; |
|
} |
|
} |
|
|
|
void SurfaceImpl::DrawTextTransparent(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, |
|
ColourRGBA fore) { |
|
QuartzTextStyle *style = TextStyleFromFont(font_); |
|
if (!style) { |
|
return; |
|
} |
|
CFStringEncoding encoding = EncodingFromCharacterSet(UnicodeMode(), style->getCharacterSet()); |
|
|
|
CGColorRef color = CGColorCreateGenericRGB(fore.GetRedComponent(), |
|
fore.GetGreenComponent(), |
|
fore.GetBlueComponent(), |
|
fore.GetAlphaComponent()); |
|
|
|
style->setCTStyleColour(color); |
|
|
|
CGColorRelease(color); |
|
|
|
QuartzTextLayout layoutDraw(text, encoding, style); |
|
layoutDraw.draw(gc, rc.left, ybase); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::MeasureWidths(const Font *font_, std::string_view text, XYPOSITION *positions) { |
|
const QuartzTextStyle *style = TextStyleFromFont(font_); |
|
if (!style) { |
|
return; |
|
} |
|
CFStringEncoding encoding = EncodingFromCharacterSet(UnicodeMode(), style->getCharacterSet()); |
|
QuartzTextLayout layoutMeasure(text, encoding, style); |
|
const CFStringEncoding encodingUsed = layoutMeasure.getEncoding(); |
|
|
|
CTLineRef mLine = layoutMeasure.getCTLine(); |
|
assert(mLine); |
|
|
|
if (encodingUsed != encoding) { |
|
// Switched to MacRoman to make work so treat as single byte encoding. |
|
for (int i=0; i<text.length(); i++) { |
|
CGFloat xPosition = CTLineGetOffsetForStringIndex(mLine, i+1, nullptr); |
|
positions[i] = xPosition; |
|
} |
|
return; |
|
} |
|
|
|
if (UnicodeMode()) { |
|
// Map the widths given for UTF-16 characters back onto the UTF-8 input string |
|
CFIndex fit = layoutMeasure.getStringLength(); |
|
int ui=0; |
|
int i=0; |
|
std::vector<CGFloat> linePositions(fit); |
|
GetPositions(mLine, linePositions); |
|
while (ui<fit) { |
|
const unsigned char uch = text[i]; |
|
const unsigned int byteCount = UTF8BytesOfLead[uch]; |
|
const int codeUnits = UTF16LengthFromUTF8ByteCount(byteCount); |
|
const CGFloat xPosition = linePositions[ui]; |
|
for (unsigned int bytePos=0; (bytePos<byteCount) && (i<text.length()); bytePos++) { |
|
positions[i++] = xPosition; |
|
} |
|
ui += codeUnits; |
|
} |
|
XYPOSITION lastPos = 0.0f; |
|
if (i > 0) |
|
lastPos = positions[i-1]; |
|
while (i<text.length()) { |
|
positions[i++] = lastPos; |
|
} |
|
} else if (mode.codePage) { |
|
int ui = 0; |
|
for (int i=0; i<text.length();) { |
|
size_t lenChar = DBCSIsLeadByte(mode.codePage, text[i]) ? 2 : 1; |
|
CGFloat xPosition = CTLineGetOffsetForStringIndex(mLine, ui+1, NULL); |
|
for (unsigned int bytePos=0; (bytePos<lenChar) && (i<text.length()); bytePos++) { |
|
positions[i++] = xPosition; |
|
} |
|
ui++; |
|
} |
|
} else { // Single byte encoding |
|
for (int i=0; i<text.length(); i++) { |
|
CGFloat xPosition = CTLineGetOffsetForStringIndex(mLine, i+1, NULL); |
|
positions[i] = xPosition; |
|
} |
|
} |
|
|
|
} |
|
|
|
XYPOSITION SurfaceImpl::WidthText(const Font *font_, std::string_view text) { |
|
const QuartzTextStyle *style = TextStyleFromFont(font_); |
|
if (!style) { |
|
return 1; |
|
} |
|
CFStringEncoding encoding = EncodingFromCharacterSet(UnicodeMode(), style->getCharacterSet()); |
|
QuartzTextLayout layoutMeasure(text, encoding, style); |
|
|
|
return static_cast<XYPOSITION>(layoutMeasure.MeasureStringWidth()); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::DrawTextNoClipUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, |
|
ColourRGBA fore, ColourRGBA back) { |
|
FillRectangleAligned(rc, back); |
|
DrawTextTransparentUTF8(rc, font_, ybase, text, fore); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::DrawTextClippedUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, |
|
ColourRGBA fore, ColourRGBA back) { |
|
CGContextSaveGState(gc); |
|
CGContextClipToRect(gc, PRectangleToCGRect(rc)); |
|
DrawTextNoClipUTF8(rc, font_, ybase, text, fore, back); |
|
CGContextRestoreGState(gc); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::DrawTextTransparentUTF8(PRectangle rc, const Font *font_, XYPOSITION ybase, std::string_view text, |
|
ColourRGBA fore) { |
|
QuartzTextStyle *style = TextStyleFromFont(font_); |
|
if (!style) { |
|
return; |
|
} |
|
const CFStringEncoding encoding = kCFStringEncodingUTF8; |
|
|
|
CGColorRef color = CGColorCreateGenericRGB(fore.GetRedComponent(), |
|
fore.GetGreenComponent(), |
|
fore.GetBlueComponent(), |
|
fore.GetAlphaComponent()); |
|
|
|
style->setCTStyleColour(color); |
|
|
|
CGColorRelease(color); |
|
|
|
QuartzTextLayout layoutDraw(text, encoding, style); |
|
layoutDraw.draw(gc, rc.left, ybase); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void SurfaceImpl::MeasureWidthsUTF8(const Font *font_, std::string_view text, XYPOSITION *positions) { |
|
const QuartzTextStyle *style = TextStyleFromFont(font_); |
|
if (!style) { |
|
return; |
|
} |
|
constexpr CFStringEncoding encoding = kCFStringEncodingUTF8; |
|
QuartzTextLayout layoutMeasure(text, encoding, style); |
|
const CFStringEncoding encodingUsed = layoutMeasure.getEncoding(); |
|
|
|
CTLineRef mLine = layoutMeasure.getCTLine(); |
|
assert(mLine); |
|
|
|
if (encodingUsed != encoding) { |
|
// Switched to MacRoman to make work so treat as single byte encoding. |
|
for (int i=0; i<text.length(); i++) { |
|
CGFloat xPosition = CTLineGetOffsetForStringIndex(mLine, i+1, nullptr); |
|
positions[i] = xPosition; |
|
} |
|
return; |
|
} |
|
|
|
// Map the widths given for UTF-16 characters back onto the UTF-8 input string |
|
CFIndex fit = layoutMeasure.getStringLength(); |
|
int ui=0; |
|
int i=0; |
|
std::vector<CGFloat> linePositions(fit); |
|
GetPositions(mLine, linePositions); |
|
while (ui<fit) { |
|
const unsigned char uch = text[i]; |
|
const unsigned int byteCount = UTF8BytesOfLead[uch]; |
|
const int codeUnits = UTF16LengthFromUTF8ByteCount(byteCount); |
|
const CGFloat xPosition = linePositions[ui]; |
|
for (unsigned int bytePos=0; (bytePos<byteCount) && (i<text.length()); bytePos++) { |
|
positions[i++] = xPosition; |
|
} |
|
ui += codeUnits; |
|
} |
|
XYPOSITION lastPos = 0.0f; |
|
if (i > 0) |
|
lastPos = positions[i-1]; |
|
while (i<text.length()) { |
|
positions[i++] = lastPos; |
|
} |
|
} |
|
|
|
XYPOSITION SurfaceImpl::WidthTextUTF8(const Font *font_, std::string_view text) { |
|
const QuartzTextStyle *style = TextStyleFromFont(font_); |
|
if (!style) { |
|
return 1; |
|
} |
|
QuartzTextLayout layoutMeasure(text, kCFStringEncodingUTF8, style); |
|
return static_cast<XYPOSITION>(layoutMeasure.MeasureStringWidth()); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
|
|
// This string contains a good range of characters to test for size. |
|
const char sizeString[] = "`~!@#$%^&*()-_=+\\|[]{};:\"\'<,>.?/1234567890" |
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; |
|
|
|
XYPOSITION SurfaceImpl::Ascent(const Font *font_) { |
|
const QuartzTextStyle *style = TextStyleFromFont(font_); |
|
if (!style) { |
|
return 1; |
|
} |
|
|
|
float ascent = style->getAscent(); |
|
return ascent + 0.5f; |
|
|
|
} |
|
|
|
XYPOSITION SurfaceImpl::Descent(const Font *font_) { |
|
const QuartzTextStyle *style = TextStyleFromFont(font_); |
|
if (!style) { |
|
return 1; |
|
} |
|
|
|
float descent = style->getDescent(); |
|
return descent + 0.5f; |
|
|
|
} |
|
|
|
XYPOSITION SurfaceImpl::InternalLeading(const Font *) { |
|
return 0; |
|
} |
|
|
|
XYPOSITION SurfaceImpl::Height(const Font *font_) { |
|
|
|
return Ascent(font_) + Descent(font_); |
|
} |
|
|
|
XYPOSITION SurfaceImpl::AverageCharWidth(const Font *font_) { |
|
|
|
XYPOSITION width = WidthText(font_, sizeString); |
|
|
|
return std::round(width / strlen(sizeString)); |
|
} |
|
|
|
void SurfaceImpl::SetClip(PRectangle rc) { |
|
CGContextSaveGState(gc); |
|
CGContextClipToRect(gc, PRectangleToCGRect(rc)); |
|
} |
|
|
|
void SurfaceImpl::PopClip() { |
|
CGContextRestoreGState(gc); |
|
} |
|
|
|
void SurfaceImpl::FlushCachedState() { |
|
CGContextSynchronize(gc); |
|
} |
|
|
|
void SurfaceImpl::FlushDrawing() { |
|
} |
|
|
|
std::unique_ptr<Surface> Surface::Allocate(Technology) { |
|
return std::make_unique<SurfaceImpl>(); |
|
} |
|
|
|
//----------------- Window ------------------------------------------------------------------------- |
|
|
|
// Cocoa uses different types for windows and views, so a Window may |
|
// be either an NSWindow or NSView and the code will check the type |
|
// before performing an action. |
|
|
|
Window::~Window() noexcept { |
|
} |
|
|
|
// Window::Destroy needs to see definition of ListBoxImpl so is located after ListBoxImpl |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
static CGFloat ScreenMax() { |
|
return NSMaxY([NSScreen mainScreen].frame); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
PRectangle Window::GetPosition() const { |
|
if (wid) { |
|
NSRect rect; |
|
id idWin = (__bridge id)(wid); |
|
NSWindow *win; |
|
if ([idWin isKindOfClass: [NSView class]]) { |
|
// NSView |
|
NSView *view = idWin; |
|
win = view.window; |
|
rect = [view convertRect: view.bounds toView: nil]; |
|
rect = [win convertRectToScreen: rect]; |
|
} else { |
|
// NSWindow |
|
win = idWin; |
|
rect = win.frame; |
|
} |
|
CGFloat screenHeight = ScreenMax(); |
|
// Invert screen positions to match Scintilla |
|
return PRectangle( |
|
NSMinX(rect), screenHeight - NSMaxY(rect), |
|
NSMaxX(rect), screenHeight - NSMinY(rect)); |
|
} else { |
|
return PRectangle(0, 0, 1, 1); |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Window::SetPosition(PRectangle rc) { |
|
if (wid) { |
|
id idWin = (__bridge id)(wid); |
|
if ([idWin isKindOfClass: [NSView class]]) { |
|
// NSView |
|
// Moves this view inside the parent view |
|
NSRect nsrc = NSMakeRect(rc.left, rc.bottom, rc.Width(), rc.Height()); |
|
NSView *view = idWin; |
|
nsrc = [view.window convertRectFromScreen: nsrc]; |
|
view.frame = nsrc; |
|
} else { |
|
// NSWindow |
|
PLATFORM_ASSERT([idWin isKindOfClass: [NSWindow class]]); |
|
NSWindow *win = idWin; |
|
CGFloat screenHeight = ScreenMax(); |
|
NSRect nsrc = NSMakeRect(rc.left, screenHeight - rc.bottom, |
|
rc.Width(), rc.Height()); |
|
[win setFrame: nsrc display: YES]; |
|
} |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Window::SetPositionRelative(PRectangle rc, const Window *window) { |
|
PRectangle rcOther = window->GetPosition(); |
|
rc.left += rcOther.left; |
|
rc.right += rcOther.left; |
|
rc.top += rcOther.top; |
|
rc.bottom += rcOther.top; |
|
SetPosition(rc); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
PRectangle Window::GetClientPosition() const { |
|
// This means, in macOS terms, get the "frame bounds". Call GetPosition, just like on Win32. |
|
return GetPosition(); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Window::Show(bool show) { |
|
if (wid) { |
|
id idWin = (__bridge id)(wid); |
|
if ([idWin isKindOfClass: [NSWindow class]]) { |
|
NSWindow *win = idWin; |
|
if (show) { |
|
[win orderFront: nil]; |
|
} else { |
|
[win orderOut: nil]; |
|
} |
|
} |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Invalidates the entire window or view so it is completely redrawn. |
|
*/ |
|
void Window::InvalidateAll() { |
|
if (wid) { |
|
id idWin = (__bridge id)(wid); |
|
NSView *container; |
|
if ([idWin isKindOfClass: [NSView class]]) { |
|
container = idWin; |
|
} else { |
|
// NSWindow |
|
NSWindow *win = idWin; |
|
container = win.contentView; |
|
container.needsDisplay = YES; |
|
} |
|
container.needsDisplay = YES; |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Invalidates part of the window or view so only this part redrawn. |
|
*/ |
|
void Window::InvalidateRectangle(PRectangle rc) { |
|
if (wid) { |
|
id idWin = (__bridge id)(wid); |
|
NSView *container; |
|
if ([idWin isKindOfClass: [NSView class]]) { |
|
container = idWin; |
|
} else { |
|
// NSWindow |
|
NSWindow *win = idWin; |
|
container = win.contentView; |
|
} |
|
[container setNeedsDisplayInRect: PRectangleToNSRect(rc)]; |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Converts the Scintilla cursor enum into an NSCursor and stores it in the associated NSView, |
|
* which then will take care to set up a new mouse tracking rectangle. |
|
*/ |
|
void Window::SetCursor(Cursor curs) { |
|
if (wid) { |
|
id idWin = (__bridge id)(wid); |
|
if ([idWin isKindOfClass: [SCIContentView class]]) { |
|
SCIContentView *container = idWin; |
|
[container setCursor: static_cast<int>(curs)]; |
|
} |
|
} |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
PRectangle Window::GetMonitorRect(Point) { |
|
if (wid) { |
|
id idWin = (__bridge id)(wid); |
|
if ([idWin isKindOfClass: [NSView class]]) { |
|
NSView *view = idWin; |
|
idWin = view.window; |
|
} |
|
if ([idWin isKindOfClass: [NSWindow class]]) { |
|
PRectangle rcPosition = GetPosition(); |
|
|
|
NSWindow *win = idWin; |
|
NSScreen *screen = win.screen; |
|
NSRect rect = screen.visibleFrame; |
|
CGFloat screenHeight = rect.origin.y + rect.size.height; |
|
// Invert screen positions to match Scintilla |
|
PRectangle rcWork( |
|
NSMinX(rect), screenHeight - NSMaxY(rect), |
|
NSMaxX(rect), screenHeight - NSMinY(rect)); |
|
PRectangle rcMonitor(rcWork.left - rcPosition.left, |
|
rcWork.top - rcPosition.top, |
|
rcWork.right - rcPosition.left, |
|
rcWork.bottom - rcPosition.top); |
|
return rcMonitor; |
|
} |
|
} |
|
return PRectangle(); |
|
} |
|
|
|
//----------------- ImageFromXPM ------------------------------------------------------------------- |
|
|
|
// Convert an XPM image into an NSImage for use with Cocoa |
|
|
|
static NSImage *ImageFromXPM(XPM *pxpm) { |
|
NSImage *img = nil; |
|
if (pxpm) { |
|
const int width = pxpm->GetWidth(); |
|
const int height = pxpm->GetHeight(); |
|
PRectangle rcxpm(0, 0, width, height); |
|
std::unique_ptr<Surface> surfaceBase(Surface::Allocate(Technology::Default)); |
|
std::unique_ptr<Surface> surfaceXPM = surfaceBase->AllocatePixMap(width, height); |
|
SurfaceImpl *surfaceIXPM = static_cast<SurfaceImpl *>(surfaceXPM.get()); |
|
CGContextClearRect(surfaceIXPM->GetContext(), CGRectMake(0, 0, width, height)); |
|
pxpm->Draw(surfaceXPM.get(), rcxpm); |
|
CGImageRef imageRef = surfaceIXPM->CreateImage(); |
|
img = [[NSImage alloc] initWithCGImage: imageRef size: NSZeroSize]; |
|
CGImageRelease(imageRef); |
|
} |
|
return img; |
|
} |
|
|
|
//----------------- ListBox and related classes ---------------------------------------------------- |
|
|
|
//----------------- IListBox ----------------------------------------------------------------------- |
|
|
|
namespace { |
|
|
|
// Unnamed namespace hides local IListBox interface. |
|
// IListBox is used to cross languages to send events from Objective C++ |
|
// AutoCompletionDelegate and AutoCompletionDataSource to C++ ListBoxImpl. |
|
|
|
class IListBox { |
|
public: |
|
virtual int Rows() = 0; |
|
virtual NSImage *ImageForRow(NSInteger row) = 0; |
|
virtual NSString *TextForRow(NSInteger row) = 0; |
|
virtual void DoubleClick() = 0; |
|
virtual void SelectionChange() = 0; |
|
}; |
|
|
|
} |
|
|
|
//----------------- AutoCompletionDelegate --------------------------------------------------------- |
|
|
|
// AutoCompletionDelegate is an Objective C++ class so it can implement |
|
// NSTableViewDelegate and receive tableViewSelectionDidChange events. |
|
|
|
@interface AutoCompletionDelegate : NSObject <NSTableViewDelegate> { |
|
IListBox *box; |
|
} |
|
|
|
@property IListBox *box; |
|
|
|
@end |
|
|
|
@implementation AutoCompletionDelegate |
|
|
|
@synthesize box; |
|
|
|
- (void) tableViewSelectionDidChange: (NSNotification *) notification { |
|
#pragma unused(notification) |
|
if (box) { |
|
box->SelectionChange(); |
|
} |
|
} |
|
|
|
@end |
|
|
|
//----------------- AutoCompletionDataSource ------------------------------------------------------- |
|
|
|
// AutoCompletionDataSource provides data to display in the list box. |
|
// It is also the target of the NSTableView so it receives double clicks. |
|
|
|
@interface AutoCompletionDataSource : NSObject <NSTableViewDataSource> { |
|
IListBox *box; |
|
} |
|
|
|
@property IListBox *box; |
|
|
|
@end |
|
|
|
@implementation AutoCompletionDataSource |
|
|
|
@synthesize box; |
|
|
|
- (void) doubleClick: (id) sender { |
|
#pragma unused(sender) |
|
if (box) { |
|
box->DoubleClick(); |
|
} |
|
} |
|
|
|
- (id) tableView: (NSTableView *) aTableView objectValueForTableColumn: (NSTableColumn *) aTableColumn row: (NSInteger) rowIndex { |
|
#pragma unused(aTableView) |
|
if (!box) |
|
return nil; |
|
if ([(NSString *)aTableColumn.identifier isEqualToString: @"icon"]) { |
|
return box->ImageForRow(rowIndex); |
|
} else { |
|
return box->TextForRow(rowIndex); |
|
} |
|
} |
|
|
|
- (void) tableView: (NSTableView *) aTableView setObjectValue: anObject forTableColumn: (NSTableColumn *) aTableColumn row: (NSInteger) rowIndex { |
|
#pragma unused(aTableView) |
|
#pragma unused(anObject) |
|
#pragma unused(aTableColumn) |
|
#pragma unused(rowIndex) |
|
} |
|
|
|
- (NSInteger) numberOfRowsInTableView: (NSTableView *) aTableView { |
|
#pragma unused(aTableView) |
|
if (!box) |
|
return 0; |
|
return box->Rows(); |
|
} |
|
|
|
@end |
|
|
|
//----------------- ListBoxImpl -------------------------------------------------------------------- |
|
|
|
namespace { // unnamed namespace hides ListBoxImpl and associated classes |
|
|
|
struct RowData { |
|
int type; |
|
std::string text; |
|
RowData(int type_, const char *text_) : |
|
type(type_), text(text_) { |
|
} |
|
}; |
|
|
|
class LinesData { |
|
std::vector<RowData> lines; |
|
public: |
|
LinesData() { |
|
} |
|
~LinesData() { |
|
} |
|
int Length() const { |
|
return static_cast<int>(lines.size()); |
|
} |
|
void Clear() { |
|
lines.clear(); |
|
} |
|
void Add(int /* index */, int type, char *str) { |
|
lines.push_back(RowData(type, str)); |
|
} |
|
int GetType(size_t index) const { |
|
if (index < lines.size()) { |
|
return lines[index].type; |
|
} else { |
|
return 0; |
|
} |
|
} |
|
const char *GetString(size_t index) const { |
|
if (index < lines.size()) { |
|
return lines[index].text.c_str(); |
|
} else { |
|
return 0; |
|
} |
|
} |
|
}; |
|
|
|
class ListBoxImpl : public ListBox, IListBox { |
|
private: |
|
NSMutableDictionary *images; |
|
int lineHeight; |
|
bool unicodeMode; |
|
int desiredVisibleRows; |
|
XYPOSITION maxItemWidth; |
|
unsigned int aveCharWidth; |
|
XYPOSITION maxIconWidth; |
|
std::unique_ptr<Font> font; |
|
int maxWidth; |
|
|
|
NSTableView *table; |
|
NSScrollView *scroller; |
|
NSTableColumn *colIcon; |
|
NSTableColumn *colText; |
|
AutoCompletionDataSource *ds; |
|
AutoCompletionDelegate *acd; |
|
|
|
LinesData ld; |
|
IListBoxDelegate *delegate; |
|
|
|
public: |
|
ListBoxImpl() : |
|
images(nil), |
|
lineHeight(10), |
|
unicodeMode(false), |
|
desiredVisibleRows(5), |
|
maxItemWidth(0), |
|
aveCharWidth(8), |
|
maxIconWidth(0), |
|
maxWidth(2000), |
|
table(nil), |
|
scroller(nil), |
|
colIcon(nil), |
|
colText(nil), |
|
ds(nil), |
|
acd(nil), |
|
delegate(nullptr) { |
|
images = [[NSMutableDictionary alloc] init]; |
|
} |
|
~ListBoxImpl() override { |
|
} |
|
|
|
// ListBox methods |
|
void SetFont(const Font *font_) override; |
|
void Create(Window &parent, int ctrlID, Scintilla::Internal::Point pt, int lineHeight_, bool unicodeMode_, Technology technology_) override; |
|
void SetAverageCharWidth(int width) override; |
|
void SetVisibleRows(int rows) override; |
|
int GetVisibleRows() const override; |
|
PRectangle GetDesiredRect() override; |
|
int CaretFromEdge() override; |
|
void Clear() noexcept override; |
|
void Append(char *s, int type = -1) override; |
|
int Length() override; |
|
void Select(int n) override; |
|
int GetSelection() override; |
|
int Find(const char *prefix) override; |
|
std::string GetValue(int n) override; |
|
void RegisterImage(int type, const char *xpm_data) override; |
|
void RegisterRGBAImage(int type, int width, int height, const unsigned char *pixelsImage) override; |
|
void ClearRegisteredImages() override; |
|
void SetDelegate(IListBoxDelegate *lbDelegate) override { |
|
delegate = lbDelegate; |
|
} |
|
void SetList(const char *list, char separator, char typesep) override; |
|
void SetOptions(ListOptions options_) override; |
|
|
|
// To clean up when closed |
|
void ReleaseViews(); |
|
|
|
// For access from AutoCompletionDataSource implement IListBox |
|
int Rows() override; |
|
NSImage *ImageForRow(NSInteger row) override; |
|
NSString *TextForRow(NSInteger row) override; |
|
void DoubleClick() override; |
|
void SelectionChange() override; |
|
}; |
|
|
|
void ListBoxImpl::Create(Window & /*parent*/, int /*ctrlID*/, Scintilla::Internal::Point pt, |
|
int lineHeight_, bool unicodeMode_, Technology) { |
|
lineHeight = lineHeight_; |
|
unicodeMode = unicodeMode_; |
|
maxWidth = 2000; |
|
|
|
NSRect lbRect = NSMakeRect(pt.x, pt.y, 120, lineHeight * desiredVisibleRows); |
|
NSWindow *winLB = [[NSWindow alloc] initWithContentRect: lbRect |
|
styleMask: NSWindowStyleMaskBorderless |
|
backing: NSBackingStoreBuffered |
|
defer: NO]; |
|
[winLB setLevel: NSModalPanelWindowLevel+1]; |
|
[winLB setHasShadow: YES]; |
|
NSRect scRect = NSMakeRect(0, 0, lbRect.size.width, lbRect.size.height); |
|
scroller = [[NSScrollView alloc] initWithFrame: scRect]; |
|
[scroller setHasVerticalScroller: YES]; |
|
table = [[NSTableView alloc] initWithFrame: scRect]; |
|
[table setHeaderView: nil]; |
|
scroller.documentView = table; |
|
colIcon = [[NSTableColumn alloc] initWithIdentifier: @"icon"]; |
|
colIcon.width = 20; |
|
[colIcon setEditable: NO]; |
|
[colIcon setHidden: YES]; |
|
NSImageCell *imCell = [[NSImageCell alloc] init]; |
|
colIcon.dataCell = imCell; |
|
[table addTableColumn: colIcon]; |
|
colText = [[NSTableColumn alloc] initWithIdentifier: @"name"]; |
|
colText.resizingMask = NSTableColumnAutoresizingMask; |
|
[colText setEditable: NO]; |
|
[table addTableColumn: colText]; |
|
ds = [[AutoCompletionDataSource alloc] init]; |
|
ds.box = this; |
|
table.dataSource = ds; // Weak reference |
|
acd = [[AutoCompletionDelegate alloc] init]; |
|
[acd setBox: this]; |
|
table.delegate = acd; |
|
scroller.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; |
|
[winLB.contentView addSubview: scroller]; |
|
|
|
table.target = ds; |
|
table.doubleAction = @selector(doubleClick:); |
|
table.selectionHighlightStyle = NSTableViewSelectionHighlightStyleSourceList; |
|
|
|
if (@available(macOS 11.0, *)) { |
|
[table setStyle: NSTableViewStylePlain]; |
|
} |
|
|
|
wid = (__bridge_retained WindowID)winLB; |
|
} |
|
|
|
void ListBoxImpl::SetFont(const Font *font_) { |
|
// NSCell setFont takes an NSFont* rather than a CTFontRef but they |
|
// are the same thing toll-free bridged. |
|
QuartzTextStyle *style = TextStyleFromFont(font_); |
|
font = std::make_unique<FontQuartz>(style); |
|
NSFont *pfont = (__bridge NSFont *)style->getFontRef(); |
|
[colText.dataCell setFont: pfont]; |
|
CGFloat itemHeight = std::ceil(pfont.boundingRectForFont.size.height); |
|
table.rowHeight = itemHeight; |
|
} |
|
|
|
void ListBoxImpl::SetAverageCharWidth(int width) { |
|
aveCharWidth = width; |
|
} |
|
|
|
void ListBoxImpl::SetVisibleRows(int rows) { |
|
desiredVisibleRows = rows; |
|
} |
|
|
|
int ListBoxImpl::GetVisibleRows() const { |
|
return desiredVisibleRows; |
|
} |
|
|
|
PRectangle ListBoxImpl::GetDesiredRect() { |
|
PRectangle rcDesired; |
|
rcDesired = GetPosition(); |
|
|
|
CGFloat itemHeight; |
|
if (@available(macOS 11.0, *)) { |
|
itemHeight = table.rowHeight; |
|
} else { |
|
// There appears to be an extra pixel above and below the row contents |
|
itemHeight = table.rowHeight + 2; |
|
} |
|
|
|
int rows = Length(); |
|
if ((rows == 0) || (rows > desiredVisibleRows)) |
|
rows = desiredVisibleRows; |
|
|
|
rcDesired.bottom = rcDesired.top + static_cast<XYPOSITION>(itemHeight * rows); |
|
rcDesired.right = rcDesired.left + maxItemWidth + aveCharWidth; |
|
rcDesired.right += 4; // Ensures no truncation of text |
|
|
|
if (Length() > rows) { |
|
[scroller setHasVerticalScroller: YES]; |
|
rcDesired.right += [NSScroller scrollerWidthForControlSize: NSControlSizeRegular |
|
scrollerStyle: NSScrollerStyleLegacy]; |
|
} else { |
|
[scroller setHasVerticalScroller: NO]; |
|
} |
|
rcDesired.right += maxIconWidth; |
|
rcDesired.right += 6; // For icon space |
|
|
|
return rcDesired; |
|
} |
|
|
|
int ListBoxImpl::CaretFromEdge() { |
|
if (colIcon.hidden) |
|
return 3; |
|
else |
|
return 6 + static_cast<int>(colIcon.width); |
|
} |
|
|
|
void ListBoxImpl::ReleaseViews() { |
|
[table setDataSource: nil]; |
|
table = nil; |
|
scroller = nil; |
|
colIcon = nil; |
|
colText = nil; |
|
acd = nil; |
|
ds = nil; |
|
} |
|
|
|
void ListBoxImpl::Clear() noexcept { |
|
maxItemWidth = 0; |
|
maxIconWidth = 0; |
|
ld.Clear(); |
|
} |
|
|
|
void ListBoxImpl::Append(char *s, int type) { |
|
int count = Length(); |
|
ld.Add(count, type, s); |
|
|
|
Scintilla::Internal::SurfaceImpl surface; |
|
XYPOSITION width = surface.WidthText(font.get(), s); |
|
if (width > maxItemWidth) { |
|
maxItemWidth = width; |
|
colText.width = maxItemWidth; |
|
} |
|
NSImage *img = images[@(type)]; |
|
if (img) { |
|
XYPOSITION widthIcon = img.size.width; |
|
if (widthIcon > maxIconWidth) { |
|
[colIcon setHidden: NO]; |
|
maxIconWidth = widthIcon; |
|
colIcon.width = maxIconWidth; |
|
} |
|
} |
|
} |
|
|
|
void ListBoxImpl::SetList(const char *list, char separator, char typesep) { |
|
Clear(); |
|
size_t count = strlen(list) + 1; |
|
std::vector<char> words(list, list+count); |
|
char *startword = words.data(); |
|
char *numword = nullptr; |
|
int i = 0; |
|
for (; words[i]; i++) { |
|
if (words[i] == separator) { |
|
words[i] = '\0'; |
|
if (numword) |
|
*numword = '\0'; |
|
Append(startword, numword?atoi(numword + 1):-1); |
|
startword = words.data() + i + 1; |
|
numword = nullptr; |
|
} else if (words[i] == typesep) { |
|
numword = words.data() + i; |
|
} |
|
} |
|
if (startword) { |
|
if (numword) |
|
*numword = '\0'; |
|
Append(startword, numword?atoi(numword + 1):-1); |
|
} |
|
[table reloadData]; |
|
} |
|
|
|
void ListBoxImpl::SetOptions(ListOptions) { |
|
} |
|
|
|
int ListBoxImpl::Length() { |
|
return ld.Length(); |
|
} |
|
|
|
void ListBoxImpl::Select(int n) { |
|
[table selectRowIndexes: [NSIndexSet indexSetWithIndex: n] byExtendingSelection: NO]; |
|
[table scrollRowToVisible: n]; |
|
} |
|
|
|
int ListBoxImpl::GetSelection() { |
|
return static_cast<int>(table.selectedRow); |
|
} |
|
|
|
int ListBoxImpl::Find(const char *prefix) { |
|
int count = Length(); |
|
for (int i = 0; i < count; i++) { |
|
const char *s = ld.GetString(i); |
|
if (s && (s[0] != '\0') && (0 == strncmp(prefix, s, strlen(prefix)))) { |
|
return i; |
|
} |
|
} |
|
return - 1; |
|
} |
|
|
|
std::string ListBoxImpl::GetValue(int n) { |
|
const char *textString = ld.GetString(n); |
|
if (textString) { |
|
return textString; |
|
} |
|
return std::string(); |
|
} |
|
|
|
void ListBoxImpl::RegisterImage(int type, const char *xpm_data) { |
|
XPM xpm(xpm_data); |
|
NSImage *img = ImageFromXPM(&xpm); |
|
images[@(type)] = img; |
|
} |
|
|
|
void ListBoxImpl::RegisterRGBAImage(int type, int width, int height, const unsigned char *pixelsImage) { |
|
CGImageRef imageRef = ImageCreateFromRGBA(width, height, pixelsImage, false); |
|
NSImage *img = [[NSImage alloc] initWithCGImage: imageRef size: NSZeroSize]; |
|
CGImageRelease(imageRef); |
|
images[@(type)] = img; |
|
} |
|
|
|
void ListBoxImpl::ClearRegisteredImages() { |
|
[images removeAllObjects]; |
|
} |
|
|
|
int ListBoxImpl::Rows() { |
|
return ld.Length(); |
|
} |
|
|
|
NSImage *ListBoxImpl::ImageForRow(NSInteger row) { |
|
return images[@(ld.GetType(row))]; |
|
} |
|
|
|
NSString *ListBoxImpl::TextForRow(NSInteger row) { |
|
const char *textString = ld.GetString(row); |
|
NSString *sTitle; |
|
if (unicodeMode) |
|
sTitle = @(textString); |
|
else |
|
sTitle = [NSString stringWithCString: textString encoding: NSWindowsCP1252StringEncoding]; |
|
return sTitle; |
|
} |
|
|
|
void ListBoxImpl::DoubleClick() { |
|
if (delegate) { |
|
ListBoxEvent event(ListBoxEvent::EventType::doubleClick); |
|
delegate->ListNotify(&event); |
|
} |
|
} |
|
|
|
void ListBoxImpl::SelectionChange() { |
|
if (delegate) { |
|
ListBoxEvent event(ListBoxEvent::EventType::selectionChange); |
|
delegate->ListNotify(&event); |
|
} |
|
} |
|
|
|
} // unnamed namespace |
|
|
|
//----------------- ListBox ------------------------------------------------------------------------ |
|
|
|
// ListBox is implemented by the ListBoxImpl class. |
|
|
|
ListBox::ListBox() noexcept { |
|
} |
|
|
|
ListBox::~ListBox() noexcept { |
|
} |
|
|
|
std::unique_ptr<ListBox> ListBox::Allocate() { |
|
return std::make_unique<ListBoxImpl>(); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Window::Destroy() noexcept { |
|
ListBoxImpl *listbox = dynamic_cast<ListBoxImpl *>(this); |
|
if (listbox) { |
|
listbox->ReleaseViews(); |
|
} |
|
if (wid) { |
|
id idWin = (__bridge id)(wid); |
|
if ([idWin isKindOfClass: [NSWindow class]]) { |
|
[idWin close]; |
|
} |
|
} |
|
wid = nullptr; |
|
} |
|
|
|
|
|
//----------------- ScintillaContextMenu ----------------------------------------------------------- |
|
|
|
@implementation ScintillaContextMenu : |
|
NSMenu |
|
|
|
// This NSMenu subclass serves also as target for menu commands and forwards them as |
|
// notification messages to the front end. |
|
|
|
- (void) handleCommand: (NSMenuItem *) sender { |
|
owner->HandleCommand(sender.tag); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
- (void) setOwner: (Scintilla::Internal::ScintillaCocoa *) newOwner { |
|
owner = newOwner; |
|
} |
|
|
|
@end |
|
|
|
//----------------- Menu --------------------------------------------------------------------------- |
|
|
|
Menu::Menu() noexcept |
|
: mid(0) { |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Menu::CreatePopUp() { |
|
Destroy(); |
|
mid = (__bridge_retained MenuID)[[ScintillaContextMenu alloc] initWithTitle: @""]; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Menu::Destroy() noexcept { |
|
CFBridgingRelease(mid); |
|
mid = nullptr; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Menu::Show(Point, const Window &) { |
|
// Cocoa menus are handled a bit differently. We only create the menu. The framework |
|
// takes care to show it properly. |
|
} |
|
|
|
//----------------- Platform ----------------------------------------------------------------------- |
|
|
|
ColourRGBA Platform::Chrome() { |
|
return ColourRGBA(0xE0, 0xE0, 0xE0); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
ColourRGBA Platform::ChromeHighlight() { |
|
return ColourRGBA(0xFF, 0xFF, 0xFF); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Returns the currently set system font for the user. |
|
*/ |
|
const char *Platform::DefaultFont() { |
|
return "Menlo-Regular"; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Returns the currently set system font size for the user. |
|
*/ |
|
int Platform::DefaultFontSize() { |
|
return 11; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
/** |
|
* Returns the time span in which two consecutive mouse clicks must occur to be considered as |
|
* double click. |
|
* |
|
* @return time span in milliseconds |
|
*/ |
|
unsigned int Platform::DoubleClickTime() { |
|
NSTimeInterval threshold = NSEvent.doubleClickInterval; |
|
if (threshold == 0) |
|
threshold = 0.5; |
|
return static_cast<unsigned int>(threshold * 1000.0); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
//#define TRACE |
|
#ifdef TRACE |
|
|
|
void Platform::DebugDisplay(const char *s) noexcept { |
|
fprintf(stderr, "%s", s); |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Platform::DebugPrintf(const char *format, ...) noexcept { |
|
const int BUF_SIZE = 2000; |
|
char buffer[BUF_SIZE]; |
|
|
|
va_list pArguments; |
|
va_start(pArguments, format); |
|
vsnprintf(buffer, BUF_SIZE, format, pArguments); |
|
va_end(pArguments); |
|
Platform::DebugDisplay(buffer); |
|
} |
|
|
|
#else |
|
|
|
void Platform::DebugDisplay(const char *) noexcept {} |
|
|
|
void Platform::DebugPrintf(const char *, ...) noexcept {} |
|
|
|
#endif |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
static bool assertionPopUps = true; |
|
|
|
bool Platform::ShowAssertionPopUps(bool assertionPopUps_) noexcept { |
|
bool ret = assertionPopUps; |
|
assertionPopUps = assertionPopUps_; |
|
return ret; |
|
} |
|
|
|
//-------------------------------------------------------------------------------------------------- |
|
|
|
void Platform::Assert(const char *c, const char *file, int line) noexcept { |
|
char buffer[2000]; |
|
snprintf(buffer, sizeof(buffer), "Assertion [%s] failed at %s %d\r\n", c, file, line); |
|
Platform::DebugDisplay(buffer); |
|
#ifdef DEBUG |
|
// Jump into debugger in assert on Mac |
|
pthread_kill(pthread_self(), SIGTRAP); |
|
#endif |
|
} |
|
|
|
//--------------------------------------------------------------------------------------------------
|
|
|