mirror of
https://github.com/notepad-plus-plus/notepad-plus-plus.git
synced 2025-07-31 01:34:58 +02:00
Release 5.5.2 ( https://www.scintilla.org/scintilla552.zip ) Released 21 August 2024. Add SCI_SETCOPYSEPARATOR for separator between parts of a multiple selection when copied to the clipboard. Feature #1530. Add SCI_GETUNDOSEQUENCE to determine whether an undo sequence is active and its nesting depth. Add SCI_STYLESETSTRETCH to support condensed and expanded text styles. Add SCI_LINEINDENT and SCI_LINEDEDENT. Feature #1524. Fix bug on Cocoa where double-click stopped working when system had been running for a long time. On Cocoa implement more values of font weight and stretch. Release 5.4.0 ( https://www.scintilla.org/lexilla540.zip ) Released 21 August 2024. Inside Lexilla, LexerModule instances are now const. This will require changes to applications that modify Lexilla.cxx, which may be done to add custom lexers. Lexer added for TOML "toml". Bash: Handle backslash in heredoc delimiter. Issue #257. Progress: Fix lexing of nested comments. Pull request #258. Force lower-casing of case-insensitive keyword lists so keywords match in some lexers. Issue #259. Close #15564
2381 lines
73 KiB
Plaintext
2381 lines
73 KiB
Plaintext
/**
|
|
* 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.stretch, 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
|
|
}
|
|
|
|
//--------------------------------------------------------------------------------------------------
|