Merge remote-tracking branch 'upstream/main'

This commit is contained in:
daboe01
2026-05-30 15:12:31 +02:00
14 changed files with 2085 additions and 334 deletions
+205 -7
View File
@@ -29,8 +29,10 @@
@import "CPClipView.j"
@import "CPScroller.j"
@import "CPView.j"
@import "CPRulerView.j"
@class CPTableView
@class CPRulerView
#define SHOULD_SHOW_CORNER_VIEW() (_scrollerStyle === CPScrollerStyleLegacy && _verticalScroller && ![_verticalScroller isHidden])
@@ -140,6 +142,14 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
int _scrollerStyle;
int _scrollerKnobStyle;
// Ruler Support
BOOL _hasVerticalRuler;
BOOL _hasHorizontalRuler;
BOOL _rulersVisible;
CPRulerView _verticalRuler;
CPRulerView _horizontalRuler;
}
@@ -306,6 +316,10 @@ var CPScrollerStyleGlobal = CPScrollerStyleOverlay,
_scrollerKnobStyle = CPScrollerKnobStyleDefault;
[self setScrollerStyle:CPScrollerStyleGlobal];
_hasVerticalRuler = NO;
_hasHorizontalRuler = NO;
_rulersVisible = NO;
_delegate = nil;
_scrollTimer = nil;
_implementedDelegateMethods = 0;
@@ -794,6 +808,103 @@ Notifies the delegate when the scroll view has finished scrolling.
}
#pragma mark -
#pragma mark Rulers
- (BOOL)hasHorizontalRuler
{
return _hasHorizontalRuler;
}
- (void)setHasHorizontalRuler:(BOOL)shouldHaveHorizontalRuler
{
if (_hasHorizontalRuler === shouldHaveHorizontalRuler)
return;
_hasHorizontalRuler = shouldHaveHorizontalRuler;
if (_hasHorizontalRuler && !_horizontalRuler)
{
_horizontalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPRulerOrientationHorizontal];
}
[self tile];
}
- (BOOL)hasVerticalRuler
{
return _hasVerticalRuler;
}
- (void)setHasVerticalRuler:(BOOL)shouldHaveVerticalRuler
{
if (_hasVerticalRuler === shouldHaveVerticalRuler)
return;
_hasVerticalRuler = shouldHaveVerticalRuler;
if (_hasVerticalRuler && !_verticalRuler)
{
_verticalRuler = [[CPRulerView alloc] initWithScrollView:self orientation:CPRulerOrientationVertical];
}
[self tile];
}
- (BOOL)rulersVisible
{
return _rulersVisible;
}
- (void)setRulersVisible:(BOOL)areRulersVisible
{
if (_rulersVisible === areRulersVisible)
return;
_rulersVisible = areRulersVisible;
[self tile];
}
- (CPRulerView)horizontalRulerView
{
return _horizontalRuler;
}
- (void)setHorizontalRulerView:(CPRulerView)aRulerView
{
if (_horizontalRuler === aRulerView)
return;
[_horizontalRuler removeFromSuperview];
_horizontalRuler = aRulerView;
if (_horizontalRuler)
[self addSubview:_horizontalRuler];
[self tile];
}
- (CPRulerView)verticalRulerView
{
return _verticalRuler;
}
- (void)setVerticalRulerView:(CPRulerView)aRulerView
{
if (_verticalRuler === aRulerView)
return;
[_verticalRuler removeFromSuperview];
_verticalRuler = aRulerView;
if (_verticalRuler)
[self addSubview:_verticalRuler];
[self tile];
}
#pragma mark -
#pragma mark Privates
@@ -1123,10 +1234,7 @@ Notifies the delegate when the scroll view has finished scrolling.
*/
- (void)tile
{
// yuck.
// RESIZE: tile->setHidden AND refl
// Outside Change: refl->tile->setHidden AND refl
// scroll: refl.
[self reflectScrolledClipView:_contentView];
}
/*!
@@ -1170,6 +1278,41 @@ Notifies the delegate when the scroll view has finished scrolling.
contentFrame.origin.y += headerClipViewHeight;
contentFrame.size.height -= headerClipViewHeight;
// Adjust content view based on horizontal / vertical ruler presence
var showHorizontalRuler = _rulersVisible && _hasHorizontalRuler && _horizontalRuler,
showVerticalRuler = _rulersVisible && _hasVerticalRuler && _verticalRuler;
var horizRulerThickness = showHorizontalRuler ? ([_horizontalRuler respondsToSelector:@selector(ruleThickness)] ? [_horizontalRuler ruleThickness] : 16.0) : 0.0,
vertRulerThickness = showVerticalRuler ? ([_verticalRuler respondsToSelector:@selector(ruleThickness)] ? [_verticalRuler ruleThickness] : 24.0) : 0.0;
if (showHorizontalRuler)
{
if ([_horizontalRuler superview] !== self)
[self addSubview:_horizontalRuler];
[_horizontalRuler setHidden:NO];
}
else if (_horizontalRuler)
{
[_horizontalRuler setHidden:YES];
}
if (showVerticalRuler)
{
if ([_verticalRuler superview] !== self)
[self addSubview:_verticalRuler];
[_verticalRuler setHidden:NO];
}
else if (_verticalRuler)
{
[_verticalRuler setHidden:YES];
}
contentFrame.origin.y += horizRulerThickness;
contentFrame.size.height -= horizRulerThickness;
contentFrame.origin.x += vertRulerThickness;
contentFrame.size.width -= vertRulerThickness;
var difference = CGSizeMake(CGRectGetWidth(documentFrame) - CGRectGetWidth(contentFrame), CGRectGetHeight(documentFrame) - CGRectGetHeight(contentFrame)),
verticalScrollerWidth = [_verticalScroller scrollerWidth],
horizontalScrollerHeight = [_horizontalScroller scrollerWidth],
@@ -1262,6 +1405,7 @@ Notifies the delegate when the scroll view has finished scrolling.
[_contentView setFrame:contentFrame];
[_headerClipView setFrame:[self _headerClipViewFrame]];
[[_headerClipView documentView] setNeedsDisplay:YES];
if (SHOULD_SHOW_CORNER_VIEW())
{
[_cornerView setFrame:[self _cornerViewFrame]];
@@ -1276,6 +1420,37 @@ Notifies the delegate when the scroll view has finished scrolling.
[[self bottomCornerView] setBackgroundColor:[self currentValueForThemeAttribute:@"bottom-corner-color"]];
}
// Position and redraw rulers to track viewport updates
if (showHorizontalRuler)
{
[_horizontalRuler setFrame:CGRectMake(
CGRectGetMinX(contentFrame),
CGRectGetMinY(contentFrame) - horizRulerThickness,
CGRectGetWidth(contentFrame),
horizRulerThickness
)];
if ([_horizontalRuler respondsToSelector:@selector(updateRuler)])
[_horizontalRuler updateRuler];
else
[_horizontalRuler setNeedsDisplay:YES];
}
if (showVerticalRuler)
{
[_verticalRuler setFrame:CGRectMake(
CGRectGetMinX(contentFrame) - vertRulerThickness,
CGRectGetMinY(contentFrame),
vertRulerThickness,
CGRectGetHeight(contentFrame)
)];
if ([_verticalRuler respondsToSelector:@selector(updateRuler)])
[_verticalRuler updateRuler];
else
[_verticalRuler setNeedsDisplay:YES];
}
--_recursionCount;
}
@@ -1438,8 +1613,8 @@ Notifies the delegate when the scroll view has finished scrolling.
y = maxY - 1.5;
CGContextMoveToPoint(context, maxX - 1.0, y);
CGContextAddLineToPoint(context, minX + 2.0, y);
CGContextMoveToPoint(maxX - 1.0, y);
CGContextAddLineToPoint(minX + 2.0, y);
x = minX + 0.5;
@@ -1618,7 +1793,14 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
CPScrollViewBottomCornerViewKey = @"CPScrollViewBottomCornerViewKey",
CPScrollViewBorderTypeKey = @"CPScrollViewBorderTypeKey",
CPScrollViewScrollerStyleKey = @"CPScrollViewScrollerStyleKey",
CPScrollViewScrollerKnobStyleKey = @"CPScrollViewScrollerKnobStyleKey";
CPScrollViewScrollerKnobStyleKey = @"CPScrollViewScrollerKnobStyleKey",
// Ruler Coding Keys
CPScrollViewHasVRulerKey = @"CPScrollViewHasVRuler",
CPScrollViewHasHRulerKey = @"CPScrollViewHasHRuler",
CPScrollViewRulersVisibleKey = @"CPScrollViewRulersVisible",
CPScrollViewVRulerKey = @"CPScrollViewVRuler",
CPScrollViewHRulerKey = @"CPScrollViewHRuler";
@implementation CPScrollView (CPCoding)
@@ -1653,6 +1835,14 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
_cornerView = [aCoder decodeObjectForKey:CPScrollViewCornerViewKey];
_bottomCornerView = [aCoder decodeObjectForKey:CPScrollViewBottomCornerViewKey];
// Ruler decoding
_hasVerticalRuler = [aCoder decodeBoolForKey:CPScrollViewHasVRulerKey];
_hasHorizontalRuler = [aCoder decodeBoolForKey:CPScrollViewHasHRulerKey];
_rulersVisible = [aCoder decodeBoolForKey:CPScrollViewRulersVisibleKey];
_verticalRuler = [aCoder decodeObjectForKey:CPScrollViewVRulerKey];
_horizontalRuler = [aCoder decodeObjectForKey:CPScrollViewHRulerKey];
_delegate = nil;
_scrollTimer = nil;
_implementedDelegateMethods = 0;
@@ -1706,6 +1896,14 @@ var CPScrollViewContentViewKey = @"CPScrollViewContentView",
[aCoder encodeInt:_scrollerStyle forKey:CPScrollViewScrollerStyleKey];
[aCoder encodeInt:_scrollerKnobStyle forKey:CPScrollViewScrollerKnobStyleKey];
// Ruler encoding
[aCoder encodeBool:_hasVerticalRuler forKey:CPScrollViewHasVRulerKey];
[aCoder encodeBool:_hasHorizontalRuler forKey:CPScrollViewHasHRulerKey];
[aCoder encodeBool:_rulersVisible forKey:CPScrollViewRulersVisibleKey];
[aCoder encodeObject:_verticalRuler forKey:CPScrollViewVRulerKey];
[aCoder encodeObject:_horizontalRuler forKey:CPScrollViewHRulerKey];
}
@end
+78 -6
View File
@@ -296,6 +296,8 @@ CPTableViewFirstColumnOnlyAutoresizingStyle = 5;
_CPTableDrawView _tableDrawView;
SEL _doubleAction;
id _doubleClickTarget @accessors(property=doubleClickTarget);
id _doubleClickArgument @accessors(property=doubleClickArgument);
CPInteger _clickedRow;
CPInteger _clickedColumn;
unsigned _columnAutoResizingStyle;
@@ -3486,13 +3488,73 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
[removeIndexes addIndex:columnIdx];
}
var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])];
[self _unloadDataViewsInRows:rowIndexes columns:removeIndexes];
if ([removeIndexes count] > 0)
{
var rowIndexes = [CPIndexSet indexSetWithIndexesInRange:CPMakeRange(0, [self numberOfRows])];
[self _unloadDataViewsInRows:rowIndexes columns:removeIndexes];
[_tableColumns removeObjectsAtIndexes:removeIndexes];
[_tableColumns removeObjectsAtIndexes:removeIndexes];
_dirtyTableColumnRangeIndex = 0;
[self _recalculateTableColumnRanges];
_dirtyTableColumnRangeIndex = 0;
[self _recalculateTableColumnRanges];
// Shift cached index sets downwards to account for the removed columns
var shiftIndexSet = function(indexSet)
{
var newSet = [CPIndexSet indexSet];
[indexSet enumerateIndexesUsingBlock:function(idx, stop)
{
if (![removeIndexes containsIndex:idx])
{
var shift = 0,
remIdx = [removeIndexes firstIndex];
while (remIdx !== CPNotFound && remIdx < idx)
{
shift++;
remIdx = [removeIndexes indexGreaterThanIndex:remIdx];
}
[newSet addIndex:idx - shift];
}
}];
return newSet;
};
_exposedColumns = shiftIndexSet(_exposedColumns);
_selectedColumnIndexes = shiftIndexSet(_selectedColumnIndexes);
// Shift individual index variables
var shiftIndex = function(idx)
{
if (idx === CPNotFound || idx === -1)
return idx;
if ([removeIndexes containsIndex:idx])
return CPNotFound;
var shift = 0,
remIdx = [removeIndexes firstIndex];
while (remIdx !== CPNotFound && remIdx < idx)
{
shift++;
remIdx = [removeIndexes indexGreaterThanIndex:remIdx];
}
return idx - shift;
};
_editingColumn = shiftIndex(_editingColumn);
_draggedColumnIndex = shiftIndex(_draggedColumnIndex);
if (_draggedColumnIndex === CPNotFound)
_draggedColumnIndex = -1;
_clickedColumn = shiftIndex(_clickedColumn);
if (_clickedColumn === CPNotFound)
_clickedColumn = -1;
}
[_differedColumnDataToRemove removeAllObjects];
_needsDifferedTableColumnRemove = NO;
@@ -4734,7 +4796,12 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
//double click actions
if ([[CPApp currentEvent] clickCount] === 2 && _doubleAction)
[self sendAction:_doubleAction to:_target];
{
var target = _doubleClickTarget || _target,
argument = [self infoForBinding:@"doubleClickArgument"] ? _doubleClickArgument : self;
[CPApp sendAction:_doubleAction to:target from:argument];
}
}
/*
@@ -6098,6 +6165,11 @@ Your delegate can implement this method to avoid subclassing the tableview to ad
{
if (aBinding == @"content")
_contentBindingExplicitlySet = YES;
else if (aBinding == @"doubleClickTarget")
{
if ([options objectForKey:CPSelectorNameBindingOption])
[self setDoubleAction:CPSelectorFromString([options objectForKey:CPSelectorNameBindingOption])];
}
[super bind:aBinding toObject:anObject withKeyPath:aKeyPath options:options];
}
+6
View File
@@ -1154,6 +1154,12 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
if (![self isEnabled] || !([self isEditable] || [self isSelectable]))
return;
if ([self isEditable] && !_isEditing)
{
_isEditing = YES;
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
}
// CPTextField uses an HTML input element to take the input so we need to
// propagate the dom event so the element is updated. This has to be done
// before interpretKeyEvents: though so individual commands have a chance
+12 -9
View File
@@ -286,6 +286,8 @@ _oncontextmenuhandler = function () { return false; };
if (removeRange.length)
_removeInvalidLineFragmentsRange = CPMakeRangeCopy(removeRange);
else
_removeInvalidLineFragmentsRange = nil;
// We erased all lines
if (!startIndex)
@@ -303,13 +305,17 @@ _oncontextmenuhandler = function () { return false; };
- (BOOL)_rescuingInvalidFragmentsWasPossibleForGlyphRange:(CPRange)aRange
{
var l = _lineFragments.length,
location = aRange.location,
found = NO,
targetLine = 0;
// 1. EARLY EXIT: If there are no fragments to rescue (e.g. setting new text), do nothing.
if (!_lineFragmentsForRescue || _lineFragmentsForRescue.length === 0)
return NO;
// try to find the first linefragment of the desired range
for (; targetLine < l; targetLine++)
var l = _lineFragments.length,
location = aRange.location,
found = NO,
targetLine = l - 1; // Start from the END of the array
// 2. REVERSE SEARCH: The fragment we want is almost always at the end.
for (; targetLine >= 0; targetLine--)
{
if (CPLocationInRange(location, _lineFragments[targetLine]._range))
{
@@ -332,9 +338,6 @@ _oncontextmenuhandler = function () { return false; };
newLength = [[_textStorage string].length],
removalSkip = 1;
// if (ABS(newLength - oldLength) > 1)
// return NO;
if (![oldLineFragment isVisuallyIdenticalToFragment:newLineFragment])
{
isIdentical = NO;
+304 -141
View File
@@ -2,10 +2,6 @@
* CPParagraphStyle.j
* AppKit
*
* FIXME
* This is basically a stub.
* We need to store all the spacing informations as well as writing direction (among others)
*
* Created by Daniel Boehringer on 11/01/2014
* Copyright Daniel Boehringer 2014.
*
@@ -26,198 +22,365 @@
@import <Foundation/CPObject.j>
@import <Foundation/CPArray.j>
@import "CPText.j"
CPLeftTabStopType = 0;
@import <Foundation/CPDictionary.j>
CPParagraphStyleAttributeName = @"CPParagraphStyleAttributeName";
var _sharedDefaultParagraphStyle,
_defaultTabStopArray;
// Define missing global tab stop type constants
CPLeftTabStopType = 0;
CPRightTabStopType = 1;
CPCenterTabStopType = 2;
CPDecimalTabStopType = 3;
@implementation CPParagraphStyle : CPObject
// Standard Tab Interval (28pts is roughly 4 spaces in standard fonts)
var kDefaultTabInterval = 28.0;
// MARK: - CPTextTab Implementation
@implementation CPTextTab : CPObject
{
CPArray _tabStops @accessors(property=tabStops);
CPTextAlignment _alignment @accessors(property=alignment);
unsigned _firstLineHeadIndent @accessors(property=firstLineHeadIndent);
unsigned _headIndent @accessors(property=headIndent);
unsigned _tailIndent @accessors(property=tailIndent);
unsigned _paragraphSpacing @accessors(property=paragraphSpacing);
unsigned _minimumLineHeight @accessors(property=minimumLineHeight);
unsigned _maximumLineHeight @accessors(property=maximumLineHeight);
unsigned _lineSpacing @accessors(property=lineSpacing);
CPTextAlignment _alignment @accessors(readonly, property=alignment);
float _location @accessors(readonly, property=location);
CPDictionary _options @accessors(readonly, property=options);
}
#pragma mark -
#pragma mark Class methods
+ (CPParagraphStyle)defaultParagraphStyle
- (id)initWithTextAlignment:(CPTextAlignment)anAlignment location:(float)aLocation options:(CPDictionary)options
{
if (!_sharedDefaultParagraphStyle)
_sharedDefaultParagraphStyle = [self new];
return _sharedDefaultParagraphStyle;
}
+ (CPArray)_defaultTabStops
{
if (!_defaultTabStopArray)
if (self = [super init])
{
var i;
_defaultTabStopArray = [];
// <!> FIXME: Define constants for these magic numbers: 13, 28
for (i = 1; i < 16 ; i++)
{
_defaultTabStopArray.push([[CPTextTab alloc] initWithType:CPLeftTabStopType location:i * 28]);
}
_alignment = anAlignment;
_location = aLocation;
_options = [options copy];
}
return _defaultTabStopArray;
}
#pragma mark -
#pragma mark Init methods
- (id)init
{
[self _initWithDefaults];
return self;
}
- (CPParagraphStyle)initWithParagraphStyle:(CPParagraphStyle)other
// Convenience initializer matching AppKit behavior
- (id)initWithType:(CPTabStopType)aType location:(float)aLocation
{
self = [super init];
_tabStops = [other._tabStops copy];
_alignment = other._alignment;
_firstLineHeadIndent = other._firstLineHeadIndent;
_headIndent = other._headIndent;
_tailIndent = other._tailIndent;
_paragraphSpacing = other._paragraphSpacing;
_minimumLineHeight = other._minimumLineHeight;
_maximumLineHeight = other._maximumLineHeight;
_lineSpacing = other._lineSpacing;
return self;
// Map old TabStopType to TextAlignment for modern compatibility
return [self initWithTextAlignment:aType location:aLocation options:nil];
}
- (void)_initWithDefaults
// Added to resolve the unrecognized selector exception in the RTF producer
- (CPTabStopType)tabStopType
{
_alignment = CPLeftTextAlignment;
_tabStops = [[[self class] _defaultTabStops] copy];
return _alignment;
}
- (void)addTabStop:(CPTextTab)aStop
- (BOOL)isEqual:(id)other
{
_tabStops.push(aStop);
if (self === other) return YES;
if (![other isKindOfClass:[CPTextTab class]]) return NO;
return _location === [other location] &&
_alignment === [other alignment] &&
((_options == nil && [other options] == nil) || [_options isEqualToDictionary:[other options]]);
}
- (id)copy
{
var other = [[self class] alloc];
return [other initWithParagraphStyle:self];
return [[CPTextTab alloc] initWithTextAlignment:_alignment location:_location options:_options];
}
@end
var CPParagraphStyleTabStopsKey = @"CPParagraphStyleTabStopsKey",
CPParagraphStyleAlignmentKey = @"CPParagraphStyleAlignmentKey",
CPParagraphStyleFirstLineHeadIndentKey = @"CPParagraphStyleFirstLineHeadIndentKey",
CPParagraphStyleHeadIndentKey = @"CPParagraphStyleHeadIndentKey",
CPParagraphStyleTailIndentKey = @"CPParagraphStyleTailIndentKey",
CPParagraphStyleParagraphSpacingKey = @"CPParagraphStyleParagraphSpacingKey",
CPParagraphStyleMinimumLineHeightKey = @"CPParagraphStyleMinimumLineHeightKey",
CPParagraphStyleMaximumLineHeightKey = @"CPParagraphStyleMaximumLineHeightKey",
CPParagraphStyleLineSpacingKey = @"CPParagraphStyleLineSpacingKey";
@implementation CPParagraphStyle (CPCoding)
- (id)initWithCoder:(id)aCoder
- (id)initWithCoder:(CPCoder)aCoder
{
self = [self init];
if (self)
if (self = [super init])
{
_tabStops = [aCoder decodeObjectForKey:"CPParagraphStyleTabStopsKey"];
_alignment = [aCoder decodeIntForKey:"CPParagraphStyleAlignmentKey"];
_firstLineHeadIndent = [aCoder decodeIntForKey:"CPParagraphStyleFirstLineHeadIndentKey"];
_headIndent = [aCoder decodeIntForKey:"CPParagraphStyleHeadIndentKey"];
_tailIndent = [aCoder decodeIntForKey:"CPParagraphStyleTailIndentKey"];
_paragraphSpacing = [aCoder decodeIntForKey:"CPParagraphStyleParagraphSpacingKey"];
_minimumLineHeight = [aCoder decodeIntForKey:"CPParagraphStyleMinimumLineHeightKey"];
_maximumLineHeight = [aCoder decodeIntForKey:"CPParagraphStyleMaximumLineHeightKey"];
_lineSpacing = [aCoder decodeIntForKey:"CPParagraphStyleLineSpacingKey"];
_alignment = [aCoder decodeIntForKey:@"CPTextTabAlignment"];
_location = [aCoder decodeFloatForKey:@"CPTextTabLocation"];
_options = [aCoder decodeObjectForKey:@"CPTextTabOptions"];
}
return self;
}
- (void)encodeWithCoder:(id)aCoder
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeInt:_alignment forKey:"CPParagraphStyleAlignmentKey"];
[aCoder encodeObject:_tabStops forKey:"CPParagraphStyleTabStopsKey"];
[aCoder encodeInt:_firstLineHeadIndent forKey:"CPParagraphStyleFirstLineHeadIndentKey"];
[aCoder encodeInt:_headIndent forKey:"CPParagraphStyleHeadIndentKey"];
[aCoder encodeInt:_tailIndent forKey:"CPParagraphStyleTailIndentKey"];
[aCoder encodeInt:_paragraphSpacing forKey:"CPParagraphStyleParagraphSpacingKey"];
[aCoder encodeInt:_minimumLineHeight forKey:"CPParagraphStyleMinimumLineHeightKey"];
[aCoder encodeInt:_maximumLineHeight forKey:"CPParagraphStyleMaximumLineHeightKey"];
[aCoder encodeInt:_lineSpacing forKey:"CPParagraphStyleLineSpacingKey"];
[aCoder encodeInt:_alignment forKey:@"CPTextTabAlignment"];
[aCoder encodeFloat:_location forKey:@"CPTextTabLocation"];
[aCoder encodeObject:_options forKey:@"CPTextTabOptions"];
}
@end
@implementation CPTextTab : CPObject
// MARK: - CPParagraphStyle Implementation
var _sharedDefaultParagraphStyle = nil;
@implementation CPParagraphStyle : CPObject
{
int _type @accessors(property = tabStopType);
double _location @accessors(property = location);
float _lineSpacing @accessors(readonly, property=lineSpacing);
float _paragraphSpacing @accessors(readonly, property=paragraphSpacing);
CPTextAlignment _alignment @accessors(readonly, property=alignment);
float _headIndent @accessors(readonly, property=headIndent);
float _tailIndent @accessors(readonly, property=tailIndent);
float _firstLineHeadIndent @accessors(readonly, property=firstLineHeadIndent);
float _minimumLineHeight @accessors(readonly, property=minimumLineHeight);
float _maximumLineHeight @accessors(readonly, property=maximumLineHeight);
CPLineBreakMode _lineBreakMode @accessors(readonly, property=lineBreakMode);
CPWritingDirection _baseWritingDirection @accessors(readonly, property=baseWritingDirection);
float _lineHeightMultiple @accessors(readonly, property=lineHeightMultiple);
float _paragraphSpacingBefore @accessors(readonly, property=paragraphSpacingBefore);
float _defaultTabInterval @accessors(readonly, property=defaultTabInterval);
CPArray _tabStops @accessors(readonly, property=tabStops);
}
- (id)initWithType:(CPTabStopType) aType location:(double) aLocation
+ (CPParagraphStyle)defaultParagraphStyle
{
if ([self = [super init]])
if (!_sharedDefaultParagraphStyle)
{
_type = aType;
_location = aLocation;
_sharedDefaultParagraphStyle = [[CPParagraphStyle alloc] init];
// Ensure defaults are set on the shared instance internal vars
// Since it's immutable, we rely on the init to set these.
}
return _sharedDefaultParagraphStyle;
}
+ (CPWritingDirection)defaultWritingDirectionForLanguage:(CPString)languageName
{
// Simplified: Cappuccino usually assumes LTR unless specified otherwise.
return CPWritingDirectionLeftToRight;
}
- (id)init
{
if (self = [super init])
{
_lineSpacing = 0.0;
_paragraphSpacing = 0.0;
_alignment = CPLeftTextAlignment;
_headIndent = 0.0;
_tailIndent = 0.0;
_firstLineHeadIndent = 0.0;
_minimumLineHeight = 0.0;
_maximumLineHeight = 0.0;
_lineBreakMode = CPLineBreakByWordWrapping;
_baseWritingDirection = CPWritingDirectionNatural;
_lineHeightMultiple = 0.0;
_paragraphSpacingBefore = 0.0;
_defaultTabInterval = kDefaultTabInterval;
// Generate default tab stops
_tabStops = [];
for (var i = 1; i <= 12; i++)
{
[_tabStops addObject:[[CPTextTab alloc] initWithType:CPLeftTextAlignment
location:i * kDefaultTabInterval]];
}
}
return self;
}
@end
var CPTextTabTypeKey = @"CPTextTabTypeKey",
CPTextTabLocationKey = @"CPTextTabLocationKey";
@implementation CPTextTab (CPCoding)
- (id)initWithCoder:(id)aCoder
- (id)initWithParagraphStyle:(CPParagraphStyle)other
{
self = [self init];
if (self)
if (self = [super init])
{
_type = [aCoder decodeIntForKey:"CPTextTabTypeKey"];
_location = [aCoder decodeDoubleForKey:"CPTextTabLocationKey"];
_lineSpacing = [other lineSpacing];
_paragraphSpacing = [other paragraphSpacing];
_alignment = [other alignment];
_headIndent = [other headIndent];
_tailIndent = [other tailIndent];
_firstLineHeadIndent = [other firstLineHeadIndent];
_minimumLineHeight = [other minimumLineHeight];
_maximumLineHeight = [other maximumLineHeight];
_lineBreakMode = [other lineBreakMode];
_baseWritingDirection = [other baseWritingDirection];
_lineHeightMultiple = [other lineHeightMultiple];
_paragraphSpacingBefore = [other paragraphSpacingBefore];
_defaultTabInterval = [other defaultTabInterval];
_tabStops = [[other tabStops] copy];
}
return self;
}
- (void)encodeWithCoder:(id)aCoder
- (id)copy
{
[aCoder encodeInt:_type forKey:"CPTextTabTypeKey"];
[aCoder encodeDouble:_location forKey:"CPTextTabLocationKey"];
// Since this class is immutable, return self.
// Subclasses (Mutable) will override.
if ([self class] === [CPParagraphStyle class])
return self;
return [[CPParagraphStyle alloc] initWithParagraphStyle:self];
}
- (id)mutableCopy
{
return [[CPMutableParagraphStyle alloc] initWithParagraphStyle:self];
}
// MARK: - Coding Support
- (id)initWithCoder:(CPCoder)aCoder
{
if (self = [super init])
{
_lineSpacing = [aCoder decodeFloatForKey:@"CPParagraphStyleLineSpacing"];
_paragraphSpacing = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacing"];
_alignment = [aCoder decodeIntForKey:@"CPParagraphStyleAlignment"];
_headIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleHeadIndent"];
_tailIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleTailIndent"];
_firstLineHeadIndent = [aCoder decodeFloatForKey:@"CPParagraphStyleFirstLineHeadIndent"];
_minimumLineHeight = [aCoder decodeFloatForKey:@"CPParagraphStyleMinimumLineHeight"];
_maximumLineHeight = [aCoder decodeFloatForKey:@"CPParagraphStyleMaximumLineHeight"];
_lineBreakMode = [aCoder decodeIntForKey:@"CPParagraphStyleLineBreakMode"];
_baseWritingDirection = [aCoder decodeIntForKey:@"CPParagraphStyleBaseWritingDirection"];
_lineHeightMultiple = [aCoder decodeFloatForKey:@"CPParagraphStyleLineHeightMultiple"];
_paragraphSpacingBefore = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacingBefore"];
_defaultTabInterval = [aCoder decodeFloatForKey:@"CPParagraphStyleDefaultTabInterval"];
_tabStops = [aCoder decodeObjectForKey:@"CPParagraphStyleTabStops"];
}
return self;
}
- (void)encodeWithCoder:(CPCoder)aCoder
{
[aCoder encodeFloat:_lineSpacing forKey:@"CPParagraphStyleLineSpacing"];
[aCoder encodeFloat:_paragraphSpacing forKey:@"CPParagraphStyleParagraphSpacing"];
[aCoder encodeInt:_alignment forKey:@"CPParagraphStyleAlignment"];
[aCoder encodeFloat:_headIndent forKey:@"CPParagraphStyleHeadIndent"];
[aCoder encodeFloat:_tailIndent forKey:@"CPParagraphStyleTailIndent"];
[aCoder encodeFloat:_firstLineHeadIndent forKey:@"CPParagraphStyleFirstLineHeadIndent"];
[aCoder encodeFloat:_minimumLineHeight forKey:@"CPParagraphStyleMinimumLineHeight"];
[aCoder encodeFloat:_maximumLineHeight forKey:@"CPParagraphStyleMaximumLineHeight"];
[aCoder encodeInt:_lineBreakMode forKey:@"CPParagraphStyleLineBreakMode"];
[aCoder encodeInt:_baseWritingDirection forKey:@"CPParagraphStyleBaseWritingDirection"];
[aCoder encodeFloat:_lineHeightMultiple forKey:@"CPParagraphStyleLineHeightMultiple"];
[aCoder encodeFloat:_paragraphSpacingBefore forKey:@"CPParagraphStyleParagraphSpacingBefore"];
[aCoder encodeFloat:_defaultTabInterval forKey:@"CPParagraphStyleDefaultTabInterval"];
[aCoder encodeObject:_tabStops forKey:@"CPParagraphStyleTabStops"];
}
// MARK: - Equality
- (BOOL)isEqual:(id)other
{
if (self === other) return YES;
if (![other isKindOfClass:[CPParagraphStyle class]]) return NO;
return _lineSpacing === [other lineSpacing] &&
_paragraphSpacing === [other paragraphSpacing] &&
_alignment === [other alignment] &&
_headIndent === [other headIndent] &&
_tailIndent === [other tailIndent] &&
_firstLineHeadIndent === [other firstLineHeadIndent] &&
_lineBreakMode === [other lineBreakMode] &&
[_tabStops isEqualToArray:[other tabStops]];
}
@end
// MARK: - CPMutableParagraphStyle Implementation
@implementation CPMutableParagraphStyle : CPParagraphStyle
{
}
- (id)initWithParagraphStyle:(CPParagraphStyle)other
{
if (self = [super initWithParagraphStyle:other])
{
// Ensure our tab stops array is mutable in the mutable subclass
_tabStops = [[other tabStops] mutableCopy];
}
return self;
}
- (id)initWithCoder:(CPCoder)aCoder
{
if (self = [super initWithCoder:aCoder])
{
_tabStops = [_tabStops mutableCopy];
}
return self;
}
- (void)setLineSpacing:(float)aLineSpacing
{
_lineSpacing = aLineSpacing;
}
- (void)setParagraphSpacing:(float)aParagraphSpacing
{
_paragraphSpacing = aParagraphSpacing;
}
- (void)setAlignment:(CPTextAlignment)anAlignment
{
_alignment = anAlignment;
}
- (void)setHeadIndent:(float)aHeadIndent
{
_headIndent = aHeadIndent;
}
- (void)setTailIndent:(float)aTailIndent
{
_tailIndent = aTailIndent;
}
- (void)setFirstLineHeadIndent:(float)aFirstLineHeadIndent
{
_firstLineHeadIndent = aFirstLineHeadIndent;
}
- (void)setMinimumLineHeight:(float)aMinimumLineHeight
{
_minimumLineHeight = aMinimumLineHeight;
}
- (void)setMaximumLineHeight:(float)aMaximumLineHeight
{
_maximumLineHeight = aMaximumLineHeight;
}
- (void)setLineBreakMode:(CPLineBreakMode)aLineBreakMode
{
_lineBreakMode = aLineBreakMode;
}
- (void)setBaseWritingDirection:(CPWritingDirection)aBaseWritingDirection
{
_baseWritingDirection = aBaseWritingDirection;
}
- (void)setLineHeightMultiple:(float)aLineHeightMultiple
{
_lineHeightMultiple = aLineHeightMultiple;
}
- (void)setParagraphSpacingBefore:(float)aParagraphSpacingBefore
{
_paragraphSpacingBefore = aParagraphSpacingBefore;
}
- (void)setDefaultTabInterval:(float)aDefaultTabInterval
{
_defaultTabInterval = aDefaultTabInterval;
}
- (void)addTabStop:(CPTextTab)aTabStop
{
[_tabStops addObject:aTabStop];
}
- (void)removeTabStop:(CPTextTab)aTabStop
{
[_tabStops removeObject:aTabStop];
}
- (void)setTabStops:(CPArray)newTabStops
{
if (_tabStops === newTabStops) return;
_tabStops = [newTabStops mutableCopy];
}
- (id)copyWithZone:(CPZone)aZone
{
// Return an immutable copy
return [[CPParagraphStyle alloc] initWithParagraphStyle:self];
}
@end
+566
View File
@@ -0,0 +1,566 @@
/*
* CPRulerView.j
* AppKit
*
* Created by Daniel Boehringer on 11/01/2014
* Copyright Daniel Boehringer 2014.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import <Foundation/CPObject.j>
@import "CPView.j"
@import "CPTextField.j"
@import "CPColor.j"
@import "CPFont.j"
@import "CPMenu.j"
@import "CPMenuItem.j"
// Orientations matching AppKit standards
// typedef enum CPRulerOrientation
CPHorizontalRuler = 0,
CPVerticalRuler = 1,
CPRulerOrientationHorizontal = 0,
CPRulerOrientationVertical = 1
@class CPRulerView;
// MARK: - CPRulerMarker (Interactive Handles with Dynamic Alignment Icons)
@implementation CPRulerMarker : CPView
{
CPRulerView _rulerView @accessors(property=rulerView);
float _imageValue @accessors(property=imageValue);
id _representedObject @accessors(property=representedObject);
CPTextField _label;
}
- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation imageValue:(float)anImageValue representedObject:(id)anObject
{
if (self = [super initWithFrame:CGRectMake(0, 0, 12, 12)])
{
_rulerView = aRulerView;
_imageValue = anImageValue;
_representedObject = anObject;
_label = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 12, 12)];
[_label setFont:[CPFont systemFontOfSize:10.0]];
[_label setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
[_label setAlignment:CPCenterTextAlignment];
[self addSubview:_label];
[self updateMarkerIcon];
}
return self;
}
- (CPTextField)label
{
return _label;
}
- (void)setRepresentedObject:(id)anObject
{
_representedObject = anObject;
[self updateMarkerIcon];
}
// Dynamically sets the Unicode triangle direction based on the alignment or indent type
// Dynamically sets the Unicode arrow direction/type based on the alignment or indent type
- (void)updateMarkerIcon
{
if ([_representedObject isKindOfClass:[CPTextTab class]])
{
var align = [_representedObject alignment];
if (align === CPLeftTextAlignment)
[_label setStringValue:@"▶"]; // Left-aligned points Right
else if (align === CPCenterTextAlignment)
[_label setStringValue:@"▼"]; // Center-aligned points Down
else if (align === CPRightTextAlignment)
[_label setStringValue:@"◀"]; // Right-aligned points Left
}
else if ([_representedObject isKindOfClass:[CPString class]])
{
if (_representedObject === @"CPFirstLineIndent")
[_label setStringValue:@"⥔"]; // Dotted shaft arrow pointing down for first-line indent
else if (_representedObject === @"CPHeadIndent")
[_label setStringValue:@"⥜"]; // Solid shaft arrow pointing down for following-lines (head) indent
else if (_representedObject === @"CPTailIndent")
[_label setStringValue:@"⥘"]; // Solid downward triangle for tail indent
else
[_label setStringValue:@"⇡"]; // Fallback standard up marker
}
else
{
[_label setStringValue:@"⇡"]; // Fallback standard up marker
}
}
#pragma mark -
#pragma mark Context Menu Support
- (CPMenu)menuForEvent:(CPEvent)anEvent
{
var menu = [[CPMenu alloc] initWithTitle:@"Marker Context Menu"];
// If the marker represents a standard tab stop, allow changing its type
if ([_representedObject isKindOfClass:[CPTextTab class]])
{
var itemLeft = [menu addItemWithTitle:@"Left Tab Stop" action:@selector(changeTypeToLeft:) keyEquivalent:@""],
itemCenter = [menu addItemWithTitle:@"Center Tab Stop" action:@selector(changeTypeToCenter:) keyEquivalent:@""],
itemRight = [menu addItemWithTitle:@"Right Tab Stop" action:@selector(changeTypeToRight:) keyEquivalent:@""];
[itemLeft setTarget:self];
[itemCenter setTarget:self];
[itemRight setTarget:self];
var align = [_representedObject alignment];
if (align === CPLeftTextAlignment) [itemLeft setState:CPOnState];
else if (align === CPCenterTextAlignment) [itemCenter setState:CPOnState];
else if (align === CPRightTextAlignment) [itemRight setState:CPOnState];
[menu addItem:[CPMenuItem separatorItem]];
}
// Determine the context-specific delete title
var deleteTitle = @"Delete Tab Stop";
if ([_representedObject isKindOfClass:[CPString class]])
{
if (_representedObject === @"CPFirstLineIndent")
deleteTitle = @"Delete 1st line indentation marker";
else if (_representedObject === @"CPHeadIndent")
deleteTitle = @"Delete head indentation marker";
else if (_representedObject === @"CPTailIndent")
deleteTitle = @"Delete tail indentation marker";
}
var itemDelete = [menu addItemWithTitle:deleteTitle action:@selector(deleteMarker:) keyEquivalent:@""];
[itemDelete setTarget:self];
return menu;
}
- (void)changeTypeToLeft:(id)sender
{
[self _changeAlignment:CPLeftTextAlignment];
}
- (void)changeTypeToCenter:(id)sender
{
[self _changeAlignment:CPCenterTextAlignment];
}
- (void)changeTypeToRight:(id)sender
{
[self _changeAlignment:CPRightTextAlignment];
}
- (void)_changeAlignment:(CPTextAlignment)alignment
{
if (![_representedObject isKindOfClass:[CPTextTab class]])
return;
var oldTab = _representedObject;
var newTab = [[CPTextTab alloc] initWithType:alignment location:_imageValue];
// Using setRepresentedObject: automatically updates the marker triangle direction
[self setRepresentedObject:newTab];
var client = [_rulerView clientView];
if (client && [client respondsToSelector:@selector(rulerView:didUpdateMarker:oldTab:)])
{
[client rulerView:_rulerView didUpdateMarker:self oldTab:oldTab];
}
}
- (void)deleteMarker:(id)sender
{
var client = [_rulerView clientView];
if (client && [client respondsToSelector:@selector(rulerView:didRemoveMarker:)])
{
[client rulerView:_rulerView didRemoveMarker:self];
}
[_rulerView removeMarker:self];
}
@end
// MARK: - CPRulerView (Pure DOM + Interactive Engine)
@implementation CPRulerView : CPView
{
CPScrollView _scrollView @accessors(property=scrollView);
CPRulerOrientation _orientation @accessors(property=orientation);
CPView _clientView @accessors(property=clientView);
float _ruleThickness @accessors(property=ruleThickness);
float _reservedThicknessForMarkers;
CPArray _markers;
// Dragger variables
CPRulerMarker _draggingMarker;
CGPoint _dragStartPoint;
float _dragStartLocation;
}
- (id)initWithScrollView:(CPScrollView)aScrollView orientation:(CPRulerOrientation)anOrientation
{
if (self = [super initWithFrame:CGRectMakeZero()])
{
_scrollView = aScrollView;
_orientation = anOrientation;
_clientView = [aScrollView documentView];
_ruleThickness = (anOrientation === CPHorizontalRuler) ? 16.0 : 24.0;
_reservedThicknessForMarkers = 0.0;
_markers = [];
[self setBackgroundColor:[CPColor colorWithWhite:0.96 alpha:1.0]];
}
return self;
}
- (void)setFrame:(CGRect)aFrame
{
[super setFrame:aFrame];
[self updateRuler];
}
// Markers registration
- (void)addMarker:(CPRulerMarker)aMarker
{
if ([_markers containsObject:aMarker])
return;
[_markers addObject:aMarker];
[self addSubview:aMarker];
[self _positionMarker:aMarker];
}
- (void)removeMarker:(CPRulerMarker)aMarker
{
[_markers removeObject:aMarker];
[aMarker removeFromSuperview];
}
- (void)setMarkers:(CPArray)newMarkers
{
for (var i = 0; i < [_markers count]; i++)
[[_markers objectAtIndex:i] removeFromSuperview];
_markers = [newMarkers mutableCopy];
for (var i = 0; i < [_markers count]; i++)
{
var marker = [_markers objectAtIndex:i];
[self addSubview:marker];
[self _positionMarker:marker];
}
}
- (CPRulerMarker)_markerAtPoint:(CGPoint)aPoint
{
for (var i = 0; i < [_markers count]; i++)
{
var marker = [_markers objectAtIndex:i];
if (CGRectContainsPoint([marker frame], aPoint))
return marker;
}
return nil;
}
- (void)_positionMarker:(CPRulerMarker)aMarker
{
if (!_scrollView)
return;
var clipView = [_scrollView contentView],
scrollPoint = [clipView bounds].origin,
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal),
rulerHeight = CGRectGetHeight([self bounds]),
rulerWidth = CGRectGetWidth([self bounds]),
markerLocation = [aMarker imageValue];
if (isHorizontal)
{
var x = markerLocation - scrollPoint.x - 6.0, // Center the 12px wide marker
y = rulerHeight - 11.0; // Sit perfectly above bottom border
// Keep horizontal marker within the bounds of the ruler to prevent clipping
if (x < 0.0)
x = 0.0;
else if (x + 12.0 > rulerWidth)
x = rulerWidth - 12.0;
[aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)];
}
else
{
var x = rulerWidth - 11.0,
y = markerLocation - scrollPoint.y - 6.0;
// Keep vertical marker within the bounds of the ruler to prevent clipping
if (y < 0.0)
y = 0.0;
else if (y + 12.0 > rulerHeight)
y = rulerHeight - 12.0;
[aMarker setFrame:CGRectMake(x, y, 12.0, 12.0)];
}
}
#pragma mark -
#pragma mark Interaction Handlers
- (void)mouseDown:(CPEvent)anEvent
{
var locationInWindow = [anEvent locationInWindow],
localPoint = [self convertPoint:locationInWindow fromView:nil],
clipView = [_scrollView contentView],
scrollPoint = [clipView bounds].origin,
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal);
var rulerLocation = isHorizontal ? (localPoint.x + scrollPoint.x) : (localPoint.y + scrollPoint.y);
// 1. Check if clicked an existing marker
var clickedMarker = [self _markerAtPoint:localPoint];
if (clickedMarker)
{
_draggingMarker = clickedMarker;
_dragStartPoint = localPoint;
_dragStartLocation = [_draggingMarker imageValue];
}
// 2. Otherwise, create a new marker dynamically where the user clicked
else
{
var newMarker = [[CPRulerMarker alloc] initWithRulerView:self
markerLocation:rulerLocation
imageValue:rulerLocation
representedObject:nil];
[self addMarker:newMarker];
// Notify the client view (e.g., CPTextView) that a new marker was added
var client = [self clientView];
if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)])
[client rulerView:self didAddMarker:newMarker];
_draggingMarker = newMarker;
_dragStartPoint = localPoint;
_dragStartLocation = rulerLocation;
}
}
- (void)mouseDragged:(CPEvent)anEvent
{
if (!_draggingMarker)
return;
var locationInWindow = [anEvent locationInWindow],
localPoint = [self convertPoint:locationInWindow fromView:nil],
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal);
var delta = isHorizontal ? (localPoint.x - _dragStartPoint.x) : (localPoint.y - _dragStartPoint.y),
newLocation = _dragStartLocation + delta;
if (newLocation < 0) newLocation = 0;
[_draggingMarker setImageValue:newLocation];
[self _positionMarker:_draggingMarker];
// Check if dragged off the ruler (more than 15px off the boundary)
var draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
: (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15);
if (draggedOff)
{
// Visual feedback: Dim the handle to 40% and turn the triangle icon gray
[_draggingMarker setAlphaValue:0.4];
[[_draggingMarker label] setTextColor:[CPColor grayColor]];
}
else
{
// Restore standard styling when dragged back into the active strip
[_draggingMarker setAlphaValue:1.0];
[[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
}
// Notify the CPTextView that the marker coordinates shifted
var client = [self clientView];
if (client && [client respondsToSelector:@selector(rulerView:didMoveMarker:)])
[client rulerView:self didMoveMarker:_draggingMarker];
}
- (void)mouseUp:(CPEvent)anEvent
{
if (!_draggingMarker)
return;
var localPoint = [self convertPoint:[anEvent locationInWindow] fromView:nil],
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal),
// If dragged more than 15 pixels off the ruler, delete the marker
draggedOff = isHorizontal ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15)
: (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15);
if (draggedOff)
{
var client = [self clientView];
if (client && [client respondsToSelector:@selector(rulerView:didRemoveMarker:)])
[client rulerView:self didRemoveMarker:_draggingMarker];
[self removeMarker:_draggingMarker];
}
else
{
// Ensure marker style is fully restored if not deleted
[_draggingMarker setAlphaValue:1.0];
[[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]];
}
_draggingMarker = nil;
}
#pragma mark -
#pragma mark DOM Layout Builder
- (void)updateRuler
{
// Wipe subviews to redraw the dynamic visible tick lines/numbers
[self setSubviews:@[]];
if (!_scrollView)
return;
var clipView = [_scrollView contentView],
scrollBounds = [clipView bounds],
scrollPoint = scrollBounds.origin,
visibleSize = scrollBounds.size,
isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal);
if (isHorizontal)
{
var start = Math.floor(scrollPoint.x / 10) * 10,
end = scrollPoint.x + visibleSize.width,
rulerHeight = CGRectGetHeight([self bounds]),
rulerWidth = CGRectGetWidth([self bounds]);
// Draw solid horizontal bottom border (pure, razor-sharp CSS DOM view)
var bottomBorder = [[CPView alloc] initWithFrame:CGRectMake(0, rulerHeight - 1, rulerWidth, 1)];
[bottomBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]];
[self addSubview:bottomBorder];
for (var val = start; val <= end; val += 10)
{
if (val < 0) continue;
var screenX = val - scrollPoint.x,
isMajor = (val % 50 === 0),
tickHeight = isMajor ? 8.0 : 4.0,
tickY = rulerHeight - tickHeight - 1.0;
// Tick mark CSS line view
var tick = [[CPView alloc] initWithFrame:CGRectMake(screenX, tickY, 1.0, tickHeight)];
[tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]];
[self addSubview:tick];
// Unit label
if (isMajor)
{
var labelX = screenX - 20.0,
alignment = CPCenterTextAlignment;
// Adjust label frame and alignment if it lands near left/right bounds
if (labelX < 0.0)
{
labelX = Math.max(0.0, screenX);
alignment = CPLeftTextAlignment;
}
else if (labelX + 40.0 > rulerWidth)
{
labelX = rulerWidth - 40.0;
alignment = CPRightTextAlignment;
}
var label = [[CPTextField alloc] initWithFrame:CGRectMake(labelX, 1.0, 40.0, 12.0)];
[label setStringValue:[CPString stringWithFormat:@"%d", val]];
[label setFont:[CPFont systemFontOfSize:8.0]];
[label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]];
[label setAlignment:alignment];
[self addSubview:label];
}
}
}
else
{
// Vertical Ruler
var start = Math.floor(scrollPoint.y / 10) * 10,
end = scrollPoint.y + visibleSize.height,
rulerHeight = CGRectGetHeight([self bounds]),
rulerWidth = CGRectGetWidth([self bounds]);
// Draw solid vertical right border (pure DOM)
var rightBorder = [[CPView alloc] initWithFrame:CGRectMake(rulerWidth - 1, 0, 1, rulerHeight)];
[rightBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]];
[self addSubview:rightBorder];
for (var val = start; val <= end; val += 10)
{
if (val < 0) continue;
var screenY = val - scrollPoint.y,
isMajor = (val % 50 === 0),
tickWidth = isMajor ? 8.0 : 4.0,
tickX = rulerWidth - tickWidth - 1.0;
// Tick mark CSS line view
var tick = [[CPView alloc] initWithFrame:CGRectMake(tickX, screenY, tickWidth, 1.0)];
[tick setBackgroundColor:[CPColor colorWithWhite:0.65 alpha:1.0]];
[self addSubview:tick];
// Unit label
if (isMajor)
{
var labelY = screenY - 6.0;
// Adjust label frame if it lands near top/bottom bounds
if (labelY < 0.0)
labelY = 0.0;
else if (labelY + 12.0 > rulerHeight)
labelY = rulerHeight - 12.0;
var label = [[CPTextField alloc] initWithFrame:CGRectMake(1.0, labelY, rulerWidth - 12.0, 12.0)];
[label setStringValue:[CPString stringWithFormat:@"%d", val]];
[label setFont:[CPFont systemFontOfSize:8.0]];
[label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]];
[label setAlignment:CPRightTextAlignment];
[self addSubview:label];
}
}
}
// Reposition and display active markers
for (var i = 0; i < [_markers count]; i++)
{
var marker = [_markers objectAtIndex:i];
if ([marker superview] !== self)
[self addSubview:marker];
[self _positionMarker:marker];
}
}
@end
+334 -3
View File
@@ -31,6 +31,7 @@
@import "CPTextStorage.j"
@import "CPTextContainer.j"
@import "CPLayoutManager.j"
@import "CPParagraphStyle.j"
@import "_CPRTFParser.j"
@import "_CPRTFProducer.j"
@@ -69,7 +70,7 @@ _MidRange = function(a1)
function _isWhitespaceCharacter(chr)
{
return (chr === '\n' || chr === '\r' || chr === ' ' || chr === '\t');
return (chr === '\n' || chr === '\r' || chr === ' '); // || chr === '\t'
}
_characterTripletFromStringAtIndex = function(string, index)
@@ -541,12 +542,15 @@ var kDelegateRespondsTo_textShouldBeginEditing
- (void)_becomeFirstResponder
{
[self updateInsertionPointStateAndRestartTimer:YES];
// SYNCHRONIZE ACTIVE PARAGRAPH MARKERS ON EDITOR FOCUS
[self updateRuler];
[[CPFontManager sharedFontManager] setSelectedFont:[self font] isMultiple:NO];
[self setNeedsDisplay:YES];
[[CPRunLoop currentRunLoop] performSelector:@selector(focusForTextView:) target:[_CPNativeInputManager class] argument:self order:0 modes:[CPDefaultRunLoopMode]];
}
- (BOOL)becomeFirstResponder
{
[super becomeFirstResponder];
@@ -1782,6 +1786,9 @@ Sets the selection to a range of characters in response to user action.
[self _enrichEssentialTypingAttributes:_typingAttributes];
}
// SYNCHRONIZE ACTIVE PARAGRAPH MARKERS ON TYPING ATTRIBUTES CHANGE
[self updateRuler];
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
// We always clear the saved selection range from the last mouse down event here.
@@ -2405,6 +2412,328 @@ Sets the selection to a range of characters in response to user action.
@end
@implementation CPTextView (CPRulerSupport)
- (void)updateRuler
{
var scrollView = [self enclosingScrollView];
if (!scrollView || ![scrollView hasHorizontalRuler] || ![scrollView rulersVisible])
return;
var ruler = [scrollView horizontalRulerView];
if (!ruler)
return;
var selectedRange = [self selectedRange],
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
currentAttributes = _typingAttributes;
var textLength = [_textStorage length];
if (textLength > 0)
{
var charIndex = selectedRange.location;
// Safety bounds checks for cursor placements
if (charIndex >= textLength)
charIndex = textLength - 1;
if (charIndex < 0)
charIndex = 0;
currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
}
if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
var markers = [],
tabStops = [paragraphStyle tabStops],
count = [tabStops count];
// A. Load existing tab stop markers onto the ruler
for (var i = 0; i < count; i++)
{
var tab = [tabStops objectAtIndex:i],
marker = [[CPRulerMarker alloc] initWithRulerView:ruler
markerLocation:[tab location]
imageValue:[tab location]
representedObject:tab];
[markers addObject:marker];
}
// B. Add Indentation Handles (First line indent & Head indent)
var firstLineMarker = [[CPRulerMarker alloc] initWithRulerView:ruler
markerLocation:[paragraphStyle firstLineHeadIndent]
imageValue:[paragraphStyle firstLineHeadIndent]
representedObject:@"CPFirstLineIndent"];
[markers addObject:firstLineMarker];
var headMarker = [[CPRulerMarker alloc] initWithRulerView:ruler
markerLocation:[paragraphStyle headIndent]
imageValue:[paragraphStyle headIndent]
representedObject:@"CPHeadIndent"];
[markers addObject:headMarker];
[ruler setMarkers:markers];
}
// Local comparison function helper for tab sorting
var compareTabStops = function(obj1, obj2, context) {
if ([obj1 location] < [obj2 location]) return CPOrderedAscending;
if ([obj1 location] > [obj2 location]) return CPOrderedDescending;
return CPOrderedSame;
};
- (void)rulerView:(CPRulerView)rulerView didAddMarker:(CPRulerMarker)marker
{
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
return;
var selectedRange = [self selectedRange],
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
currentAttributes = _typingAttributes;
// Retrieve active attributes at the cursor position if there is no selection
var textLength = [_textStorage length],
charIndex = selectedRange.location;
if (textLength > 0)
{
if (charIndex >= textLength)
charIndex = textLength - 1;
if (charIndex < 0)
charIndex = 0;
currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
}
if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
var mutableStyle = [paragraphStyle mutableCopy];
// Create a new Left-aligned tab stop where the user clicked
var newTab = [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:[marker imageValue]],
tabs = [[mutableStyle tabStops] mutableCopy];
[tabs addObject:newTab];
// Sort tabs by location ascending
[tabs sortUsingFunction:compareTabStops context:nil];
[mutableStyle setTabStops:tabs];
[marker setRepresentedObject:newTab];
// Find the target text range to modify (selection or containing paragraph)
var targetRange = selectedRange;
if (targetRange.length === 0)
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
if (targetRange.length > 0)
{
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
[_layoutManager textStorage:_textStorage
edited:0
range:CPMakeRangeCopy(targetRange)
changeInLength:0
invalidatedRange:CPMakeRangeCopy(targetRange)];
}
if (selectedRange.length === 0)
{
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
}
}
- (void)rulerView:(CPRulerView)rulerView didMoveMarker:(CPRulerMarker)marker
{
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
return;
var selectedRange = [self selectedRange],
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
currentAttributes = _typingAttributes;
var textLength = [_textStorage length],
charIndex = selectedRange.location;
if (textLength > 0)
{
if (charIndex >= textLength)
charIndex = textLength - 1;
if (charIndex < 0)
charIndex = 0;
currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
}
if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
var mutableStyle = [paragraphStyle mutableCopy],
oldTab = [marker representedObject];
if (!oldTab)
return;
// A. Handle standard tab stops
if ([oldTab isKindOfClass:[CPTextTab class]])
{
var newTab = [[CPTextTab alloc] initWithType:[oldTab alignment] location:[marker imageValue]],
tabs = [[mutableStyle tabStops] mutableCopy];
[tabs removeObject:oldTab];
[tabs addObject:newTab];
// Sort tabs by location ascending
[tabs sortUsingFunction:compareTabStops context:nil];
[mutableStyle setTabStops:tabs];
[marker setRepresentedObject:newTab];
}
// B. Handle Indentation Marker drags (First Line, Left, and Right indents)
else if ([oldTab isKindOfClass:[CPString class]])
{
if (oldTab === @"CPFirstLineIndent")
[mutableStyle setFirstLineHeadIndent:[marker imageValue]];
else if (oldTab === @"CPHeadIndent")
[mutableStyle setHeadIndent:[marker imageValue]];
else if (oldTab === @"CPTailIndent")
[mutableStyle setTailIndent:[marker imageValue]];
}
// Find the target text range to modify (selection or containing paragraph)
var targetRange = selectedRange;
if (targetRange.length === 0)
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
if (targetRange.length > 0)
{
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
[_layoutManager textStorage:_textStorage
edited:0
range:CPMakeRangeCopy(targetRange)
changeInLength:0
invalidatedRange:CPMakeRangeCopy(targetRange)];
}
if (selectedRange.length === 0)
{
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
}
}
- (void)rulerView:(CPRulerView)rulerView didRemoveMarker:(CPRulerMarker)marker
{
if (![self _didBeginEditing] || ![self shouldChangeTextInRange:_selectionRange replacementString:nil])
return;
var selectedRange = [self selectedRange],
paragraphStyle = [CPParagraphStyle defaultParagraphStyle],
currentAttributes = _typingAttributes;
var textLength = [_textStorage length],
charIndex = selectedRange.location;
if (textLength > 0)
{
if (charIndex >= textLength)
charIndex = textLength - 1;
if (charIndex < 0)
charIndex = 0;
currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil];
}
if ([currentAttributes objectForKey:CPParagraphStyleAttributeName])
paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName];
var mutableStyle = [paragraphStyle mutableCopy],
oldTab = [marker representedObject];
if (!oldTab)
return;
var tabs = [[mutableStyle tabStops] mutableCopy];
[tabs removeObject:oldTab];
[mutableStyle setTabStops:tabs];
// Find the target text range to modify (selection or containing paragraph)
var targetRange = selectedRange;
if (targetRange.length === 0)
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
if (targetRange.length > 0)
{
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
[_layoutManager textStorage:_textStorage
edited:0
range:CPMakeRangeCopy(targetRange)
changeInLength:0
invalidatedRange:CPMakeRangeCopy(targetRange)];
}
if (selectedRange.length === 0)
{
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
}
}
- (void)rulerView:(CPRulerView)rulerView didUpdateMarker:(CPRulerMarker)marker oldTab:(id)oldTab
{
var selectedRange = [self selectedRange],
targetRange = selectedRange;
if (targetRange.length === 0)
targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph];
if (targetRange.length === 0)
return;
var paragraphStyle = [[self textStorage] attribute:CPParagraphStyleAttributeName atIndex:targetRange.location effectiveRange:NULL];
if (!paragraphStyle)
paragraphStyle = [CPParagraphStyle defaultParagraphStyle];
var mutableStyle = [paragraphStyle mutableCopy],
newTab = [marker representedObject],
tabs = [[mutableStyle tabStops] mutableCopy];
[tabs removeObject:oldTab];
[tabs addObject:newTab];
// Sort tabs ascending
[tabs sortUsingFunction:compareTabStops context:nil];
[mutableStyle setTabStops:tabs];
[_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)];
[_layoutManager textStorage:_textStorage
edited:0
range:CPMakeRangeCopy(targetRange)
changeInLength:0
invalidatedRange:CPMakeRangeCopy(targetRange)];
if (selectedRange.length === 0)
{
[_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName];
[[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self];
}
// Force layouts and view canvas update
[_layoutManager _validateLayoutAndGlyphs];
[self sizeToFit];
[self setNeedsDisplay:YES];
}
@end
@implementation CPTextView (CPTextViewDelegate)
@@ -2778,8 +3107,10 @@ var _CPCopyPlaceholder = '-';
// Intercept problematic keys before the browser acts.
_CPNativeInputField.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === '')) {
if (e.key === 'Tab' || e.key === 'Enter' || (e.key === 'Backspace' && _CPNativeInputField.innerHTML === ''))
{
// Prevent browser default action:
// - 'Tab': Prevents browser focus navigation, allowing Cappuccino key bindings (like option-tab or tab) to work.
// - 'Enter': Prevents inserting <div><br></div>.
// - 'Backspace' on empty: Prevents inserting junk characters on iPadOS.
e.preventDefault();
+141 -18
View File
@@ -6,7 +6,7 @@
* All modifications copyright Daniel Boehringer 2013.
* Extensive code formatting and review by Andrew Hankinson
* Based on original work by
* Emmanuel Maillard on 27/02/2010.
* Created by Emmanuel Maillard on 27/02/2010.
* Copyright Emmanuel Maillard 2010.
*
* This library is free software; you can redistribute it and/or
@@ -135,31 +135,34 @@ var CPSystemTypesetterFactory,
return [_layoutManager textContainers];
}
// Retrieves correct CPTextTab stop accounting for CPArray properties
- (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction
{
var tabStops = [_currentParagraph tabStops];
if (!tabStops)
tabStops = [CPParagraphStyle _defaultTabStops];
tabStops = [[CPParagraphStyle defaultParagraphStyle] tabStops];
var l = tabStops.length;
var l = [tabStops count];
if (aWidth > tabStops[l - 1]._location)
if (l === 0)
return nil;
for (var i = l - 1; i >= 0; i--)
// Find the first tab stop that is strictly greater than the current width
for (var i = 0; i < l; i++)
{
if (aWidth > tabStops[i]._location)
{
if (i + 1 < l)
return tabStops[i + 1];
}
var tab = [tabStops objectAtIndex:i];
if ([tab location] > aWidth)
return tab;
}
if (i === -1)
return tabStops[0];
// If aWidth exceeds the last tab stop, dynamically calculate the next
// tab location using the default tab interval.
var defaultInterval = [_currentParagraph defaultTabInterval] || 28.0;
var nextLocation = CEIL((aWidth + 1.0) / defaultInterval) * defaultInterval;
return nil;
return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation];
}
- (BOOL)_flushRange:(CPRange)lineRange
@@ -263,6 +266,14 @@ var CPSystemTypesetterFactory,
currentParagraphMaximumLineHeight,
currentParagraphLineSpacing;
// Track physical line starts to prevent overwriting lineOrigin.x in tab segments
var isStartOfPhysicalLine = YES;
// Track paragraph indents and margins
var isFirstLineOfLayout = YES,
isFirstLineOfParagraph = YES,
rightMargin = containerSizeWidth;
if (glyphIndex > 0)
lineOrigin = CGPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin);
else if ([_layoutManager extraLineFragmentTextContainer])
@@ -289,6 +300,56 @@ var CPSystemTypesetterFactory,
currentParagraphMaximumLineHeight = [_currentParagraph maximumLineHeight];
currentParagraphLineSpacing = [_currentParagraph lineSpacing];
// Recalculate right margin on paragraph style change
var tailIndent = [_currentParagraph tailIndent];
if (tailIndent > 0.0)
rightMargin = tailIndent;
else if (tailIndent < 0.0)
rightMargin = containerSizeWidth + tailIndent;
else
rightMargin = containerSizeWidth;
// If we are at the start of a physical line, we update lineOrigin.x
if (isStartOfPhysicalLine)
{
if (glyphIndex > 0)
{
var prevChar = theString.charCodeAt(glyphIndex - 1);
isFirstLineOfParagraph = (prevChar === 10 || prevChar === 13);
}
else
{
isFirstLineOfParagraph = YES;
}
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
isFirstLineOfLayout = NO;
}
// Calculate the right wrapping margin based on tail indent
var tailIndent = [_currentParagraph tailIndent];
if (tailIndent > 0.0)
rightMargin = tailIndent;
else if (tailIndent < 0.0)
rightMargin = containerSizeWidth + tailIndent;
else
rightMargin = containerSizeWidth;
// Handle the layout's very first line indentation
if (isFirstLineOfLayout)
{
if (glyphIndex > 0)
{
var prevChar = theString.charCodeAt(glyphIndex - 1);
isFirstLineOfParagraph = (prevChar === 10 || prevChar === 13);
}
else
{
isFirstLineOfParagraph = YES;
}
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
isFirstLineOfLayout = NO;
}
if (!currentFont)
currentFont = [_textStorage font] || [CPFont systemFontOfSize:12.0];
@@ -316,6 +377,9 @@ var CPSystemTypesetterFactory,
lineRange.length++;
measuringRange.length++;
// We are processing characters, so we are no longer at the start of a physical line
isStartOfPhysicalLine = NO;
var currentCharCode = theString.charCodeAt(glyphIndex), // use pure javascript methods for performance reasons
rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor;
@@ -356,15 +420,68 @@ var CPSystemTypesetterFactory,
isTabStop = YES;
if (nextTab)
rangeWidth = nextTab._location - lineOrigin.x;
{
// Look-ahead to measure the width of the incoming text segment for alignment
var nextSegmentWidth = 0.0,
tempIndex = glyphIndex + 1,
segmentString = "";
while (tempIndex < numberOfGlyphs)
{
var nextCharCode = theString.charCodeAt(tempIndex);
if (nextCharCode === 9 || nextCharCode === 10 || nextCharCode === 13)
break;
segmentString += theString.charAt(tempIndex);
tempIndex++;
}
if (segmentString.length > 0)
nextSegmentWidth = [segmentString sizeWithFont:currentFont inWidth:NULL].width;
var tabLocation = [nextTab location],
tabAlignment = [nextTab alignment];
// Mathematically offset the tab character's right boundary
if (tabAlignment === CPCenterTextAlignment)
{
rangeWidth = (tabLocation - nextSegmentWidth / 2.0) - lineOrigin.x;
}
else if (tabAlignment === CPRightTextAlignment)
{
rangeWidth = (tabLocation - nextSegmentWidth) - lineOrigin.x;
}
else // Left align tab stop
{
rangeWidth = tabLocation - lineOrigin.x;
}
// Enforce a minimum safety spacer width to avoid character overlapping
var minRangeWidth = prevRangeWidth + 5.0;
if (rangeWidth < minRangeWidth)
rangeWidth = minRangeWidth;
}
else
rangeWidth += 28; //FIXME
} // fallthrough intentional
{
rangeWidth += 28.0; // standard fallback spacer
}
break;
}
case 32: // ' '
wrapRange = CPMakeRangeCopy(lineRange);
wrapWidth = rangeWidth;
wrapRange._height = _lineHeight;
wrapRange._base = _lineBase;
// Optimization: Start measuring from the next character to avoid O(n^2)
// string width calculation within a line since spaces do not carry ligatures or kerning.
// Only reset the measuring range if the next character is NOT another space.
// This prevents compounded subpixel rounding errors with contiguous spaces.
if (theString.charCodeAt(glyphIndex + 1) !== 32)
{
currentAnchor = rangeWidth;
measuringRange = CPMakeRange(glyphIndex + 1, 0);
}
break;
case 10:
@@ -375,7 +492,8 @@ var CPSystemTypesetterFactory,
advancements.push({width: rangeWidth - prevRangeWidth, height: ascent, descent: descent});
prevRangeWidth = _lineWidth = rangeWidth;
if (lineOrigin.x + rangeWidth > containerSizeWidth)
// Wrap lines against the tail indent (rightMargin) instead of container boundaries
if (lineOrigin.x + rangeWidth > rightMargin)
{
if (wrapWidth)
{
@@ -419,12 +537,17 @@ var CPSystemTypesetterFactory,
containerSizeHeight = containerSize.height;
}
lineOrigin.x = 0;
// If this is a soft wrap (isWordWrapped), next line gets headIndent.
// If it was a paragraph return, it gets firstLineHeadIndent.
isFirstLineOfParagraph = !isWordWrapped;
lineOrigin.x = isFirstLineOfParagraph ? [_currentParagraph firstLineHeadIndent] : [_currentParagraph headIndent];
numLines++;
isNewline = NO;
_lineFragments = [];
_lineHeight = 0;
_lineBase = ascent;
isStartOfPhysicalLine = YES;
}
isTabStop = NO;
+134 -58
View File
@@ -4,11 +4,6 @@
Copyright (C) 2014 Daniel Boehringer
FIXME: this class should be redone using a 'real' parser
* all paragraph spacing information is currently not parsed
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
@@ -37,9 +32,25 @@ FIXME: this class should be redone using a 'real' parser
@global CPFontAttributeName
@global CPForegroundColorAttributeName
@global CPBackgroundColorAttributeName
@global CPParagraphStyleAttributeName
@global CPLeftTabStopType
@global CPRightTabStopType
@global CPCenterTabStopType
@global CPDecimalTabStopType
var hexTable = [];
var cp1252Map = {
0x80: 0x20AC, 0x82: 0x201A, 0x83: 0x0192, 0x84: 0x201E, 0x85: 0x2026,
0x86: 0x2020, 0x87: 0x2021, 0x88: 0x02C6, 0x89: 0x2030, 0x8A: 0x0160,
0x8B: 0x2039, 0x8C: 0x0152, 0x8E: 0x017D, 0x91: 0x2018, 0x92: 0x2019,
0x93: 0x201C, 0x94: 0x201D, 0x95: 0x2022, 0x96: 0x2013, 0x97: 0x2014,
0x98: 0x02DC, 0x99: 0x2122, 0x9A: 0x0161, 0x9B: 0x203A, 0x9C: 0x0153,
0x9E: 0x017E, 0x9F: 0x0178
};
// Hold the attributes of the current run
@implementation _RTFAttribute : CPObject
{
@@ -56,6 +67,7 @@ var hexTable = [];
BOOL strikethrough;
BOOL script;
BOOL _tabChanged;
CPTabStopType _nextTabType;
}
- (id)init
@@ -65,6 +77,7 @@ var hexTable = [];
[self resetFont];
[self resetParagraphStyle];
_range = CPMakeRange(0, 0);
_nextTabType = CPLeftTabStopType;
}
return self;
@@ -74,11 +87,19 @@ var hexTable = [];
{
var mynew = [_RTFAttribute new];
mynew.paragraph = [paragraph copy];
mynew.paragraph = [paragraph mutableCopy];
mynew.fontName = fontName;
mynew.fontSize = fontSize;
mynew.bold = bold;
mynew.italic = italic;
mynew.underline = underline;
mynew.strikethrough = strikethrough;
mynew.script = script;
mynew.fgColour = fgColour;
mynew.bgColour = bgColour;
mynew.ulColour = ulColour;
mynew._tabChanged = _tabChanged;
mynew._nextTabType = _nextTabType;
return mynew;
}
@@ -134,7 +155,9 @@ var hexTable = [];
- (void)resetParagraphStyle
{
paragraph = [[CPParagraphStyle defaultParagraphStyle] copy];
paragraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
_tabChanged = NO;
_nextTabType = CPLeftTabStopType;
}
- (void)resetFont
@@ -152,7 +175,7 @@ var hexTable = [];
- (void)addTab:(float)location type:(CPTextTabType)type
{
var tab = [[CPTextTab alloc] initWithType:CPLeftTabStopType
var tab = [[CPTextTab alloc] initWithType:type
location:location];
if (!_tabChanged)
@@ -164,6 +187,8 @@ var hexTable = [];
{
[paragraph addTabStop: tab];
}
_nextTabType = CPLeftTabStopType;
}
- (CPDictionary)dictionary
@@ -175,6 +200,9 @@ var hexTable = [];
if (fgColour)
[ret setObject:fgColour forKey:CPForegroundColorAttributeName];
if (bgColour)
[ret setObject:bgColour forKey:CPBackgroundColorAttributeName];
return ret;
}
@end
@@ -193,7 +221,7 @@ var kRgsymRtf = {
"b" : [ "b", 1, false, kRTFParserType_prop, "propBold"],
"ul" : [ "ul", 1, false, kRTFParserType_prop, "propUnderline"],
"i" : [ "i", 1, false, kRTFParserType_prop, "propItalic"],
"li" : [ "li", 0, false, kRTFParserType_prop, "propPgnFormat"],
// "li" : [ "li", 0, false, kRTFParserType_prop, "propPgnFormat"],
"pgnucltr" : [ "pgnucltr", "pgULtr", true, kRTFParserType_prop, "propPgnFormat"],
"pgnlcltr" : [ "pgnlcltr", "pgLLtr", true, kRTFParserType_prop, "propPgnFormat"],
"qc" : [ "qc", "justC", true, kRTFParserType_prop, "propJust"],
@@ -321,17 +349,25 @@ var kRgsymRtf = {
- (BOOL)pushState
{
_states.push["group"];
// Push stack as an object containing scoping context
_states.push({
curState: _curState,
run: [_currentRun copy]
});
return YES;
}
- (BOOL)popState
{
_states.pop();
if (_curState > 0)
_curState--;
if (_states.length > 0)
{
var state = _states.pop();
_curState = state.curState;
[self _flushCurrentRun];
_currentRun = state.run;
_currentRun._range = CPMakeRange([_result length], 0);
}
return YES;
}
@@ -346,25 +382,23 @@ var kRgsymRtf = {
return '';
case "ipfnHex":
ch = _rtf.charAt(++_currentParseIndex);
var hex = '';
while (new RegExp("[a-fA-F0-9\\']").test(ch))
// Konsumiere exakt 2 Zeichen nach dem \'
for (var i = 0; i < 2; i++)
{
if (ch == "'")
var nextCh = _rtf.charAt(++_currentParseIndex);
if (/[a-fA-F0-9]/.test(nextCh))
{
_currentParseIndex++;
continue;
hex += nextCh;
}
else
{
_currentParseIndex--;
break;
}
hex += (ch + '');
ch = _rtf.charAt(++_currentParseIndex);
}
//ch = parseInt(ch, 16);
//console.log("hex : " + hex);
_hexreturn = YES;
_currentParseIndex--;
if (_curState !== 0)
return '';
@@ -406,10 +440,14 @@ var kRgsymRtf = {
var dict = [_currentRun dictionary];
[_result setAttributes:dict range:_currentRun._range]; // flush previous run
_currentRun.fgColour = [CPColor blackColor];
// Deep copy the current run style for the next sequence of characters
_currentRun = [_currentRun copy];
}
else
{
_currentRun = [_RTFAttribute new];
}
_currentRun._range = CPMakeRange(newOffset, 0); // open a new one
}
@@ -422,6 +460,7 @@ var kRgsymRtf = {
{
case "pard":
[self _flushCurrentRun];
[_currentRun resetParagraphStyle];
break;
case "b": // bold
@@ -430,7 +469,7 @@ var kRgsymRtf = {
if (_currentRun && _currentRun.bold)
[self _flushCurrentRun];
_currentRun.bold = NO
_currentRun.bold = NO;
}
else
{
@@ -448,7 +487,7 @@ var kRgsymRtf = {
if (_currentRun && _currentRun.italic)
[self _flushCurrentRun];
_currentRun.italic = NO
_currentRun.italic = NO;
}
else
{
@@ -464,6 +503,18 @@ var kRgsymRtf = {
[_currentRun.paragraph setAlignment:CPCenterTextAlignment];
break;
case "ql": // paragraph left
[_currentRun.paragraph setAlignment:CPLeftTextAlignment];
break;
case "qr": // paragraph right
[_currentRun.paragraph setAlignment:CPRightTextAlignment];
break;
case "qj": // paragraph justified
[_currentRun.paragraph setAlignment:CPJustifiedTextAlignment];
break;
case "paperw":
_paper.width = param;
break;
@@ -570,6 +621,20 @@ var kRgsymRtf = {
break;
case "cb": // change background color
case "highlight":
[self _flushCurrentRun];
var colorIndex = parseInt(param) - 1;
if (_currentRun)
{
if (colorIndex >= 0 && colorIndex < _colorArray.length)
_currentRun.bgColour = _colorArray[colorIndex];
else
_currentRun.bgColour = nil;
}
break;
case "f": // change font
[self _flushCurrentRun];
var fontIndex = parseInt(param);
@@ -583,11 +648,41 @@ var kRgsymRtf = {
_currentRun.fontSize = parseInt(param) / 2;
break;
case "tx": // tabstop
case "fi": // first line indent
if (_currentRun)
[_currentRun.paragraph setFirstLineHeadIndent:parseInt(param) / 20.0];
break;
case "li": // left indent / head indent
if (_currentRun)
[_currentRun.paragraph setHeadIndent:parseInt(param) / 20.0];
break;
case "ri": // right indent / tail indent
if (_currentRun)
[_currentRun.paragraph setTailIndent:parseInt(param) / 20.0];
break;
case "tqc": // center tab stop style flag
if (_currentRun)
_currentRun._nextTabType = CPCenterTabStopType;
break;
case "tqr": // right tab stop style flag
if (_currentRun)
_currentRun._nextTabType = CPRightTabStopType;
break;
case "tqdec": // decimal tab stop style flag
if (_currentRun)
_currentRun._nextTabType = CPDecimalTabStopType;
break;
case "tx": // tabstop location definition
var location = parseInt(param) / 20;
if (_currentRun)
[_currentRun addTab:location type:CPLeftTabStopType];
[_currentRun addTab:location type:_currentRun._nextTabType];
break;
@@ -596,9 +691,6 @@ var kRgsymRtf = {
}
if (_states.length > 0)
_curState = 1;
return '';
}
}
@@ -734,32 +826,16 @@ var kRgsymRtf = {
{
if (ch.length > 0)
{
if (parseInt(ch, 16) & 0x80)
{
hex += ch.toUpperCase();
}
else
{
[self _appendPlainString: String.fromCharCode(parseInt((hex + ch), 16))];
hex = '';
var byteVal = parseInt(ch, 16);
var unicodeVal = byteVal;
// Windows-1252 Mapping für den Bereich 0x80 - 0x9F anwenden
if (byteVal >= 0x80 && byteVal <= 0x9F) {
unicodeVal = cp1252Map[byteVal] || byteVal;
}
if (hex.length == 4)
{
var temp = parseInt(hex, 16);
if (hexTable && hexTable[hex.toUpperCase()] !== undefined)
temp = parseInt(hexTable[hex.toUpperCase()], 16);
[self _appendPlainString: String.fromCharCode(temp)];
hex = '';
}
[self _appendPlainString: String.fromCharCode(unicodeVal)];
}
else
{
CPLogConsole("hex skipped");
}
_hexreturn = NO;
}
else if (ch !== undefined && _curState === 0)
+20 -2
View File
@@ -349,6 +349,12 @@ CPTokenFieldDeleteButtonType = 1;
if (theBinding)
[theBinding reverseSetValueFor:@"objectValue"];
if (!_isEditing)
{
_isEditing = YES;
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
}
[self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
_shouldNotifyTarget = YES;
@@ -453,8 +459,8 @@ CPTokenFieldDeleteButtonType = 1;
{
[_tokenScrollView documentView]._DOMElement.appendChild(element);
//post CPControlTextDidBeginEditingNotification
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
// Removed so CPTokenField doesn't fire the notification the moment it becomes the first responder, but instead defers to the first keystroke, just like Cocoa (see keyDown: in CPTextField).
// [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
[[CPRunLoop mainRunLoop] performBlock:function()
{
@@ -498,6 +504,8 @@ CPTokenFieldDeleteButtonType = 1;
[self _resignFirstKeyResponder];
_isEditing = NO;
if (_shouldNotifyTarget)
{
_shouldNotifyTarget = NO;
@@ -1032,6 +1040,16 @@ CPTokenFieldDeleteButtonType = 1;
CPTokenFieldTextDidChangeValue = [self stringValue];
#endif
// Has to be enabled, and it also has to be editable or selectable.
if (![self isEnabled] || !([self isEditable] || [self isSelectable]))
return;
if ([self isEditable] && !_isEditing)
{
_isEditing = YES;
[self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]];
}
// Leave the default _propagateCurrentDOMEvent setting in place. This might be YES or NO depending
// on if something that could be a browser shortcut was pressed or not, such as Cmd-R to reload.
// If it was NO we want to leave it at NO however and only enable it in insertText:. This is what
+2 -1
View File
@@ -1951,7 +1951,8 @@ CPTexturedBackgroundWindowMask
return [[self firstResponder] keyUp:anEvent];
case CPKeyDown:
if ([anEvent charactersIgnoringModifiers] === CPTabCharacter)
if ([anEvent charactersIgnoringModifiers] === CPTabCharacter &&
!([anEvent modifierFlags] & (CPAlternateKeyMask | CPCommandKeyMask)))
{
if ([anEvent modifierFlags] & CPShiftKeyMask)
[self selectPreviousKeyView:self];
+2 -8
View File
@@ -3,18 +3,12 @@
# Cappuccino: Build Desktop-Class Web Applications
Cappuccino is an open-source framework that supports building powerful, desktop-class applications running in any modern web browser. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C.
Cappuccino faithfully implements the proven design patterns of NeXTSTEP/Apple's Cocoa frameworks, enabling the creation of incredibly complex and reliable applications with a fraction of the code.
> **✨ Project Status: Active Development & Node.js Transition**
> Cappuccino has been under continuous development since 2008 and is actively maintained. A major transition to a modern, **Node.js-based toolchain** has recently been finalized. The current release is a production-ready Release Candidate, with a formal release scheduled for 2026. It is stable, fast, and ready for new projects.
---
## Why Use Cappuccino?
Cappuccino is not intended for building simple websites. It is for building **applications**especially complex, data-rich, line-of-business tools where productivity and user experience are paramount.
Cappuccino is for building **applications** in the browser — especially complex, data-rich, line-of-business tools where productivity and user experience are paramount. Instead of direct manipulation of HTML, CSS, and the DOM, applications are built using Objective-J, a superset of JavaScript modeled on Objective-C. This gives you a lot of benefits:
* **💻 True Desktop Behavior, Out-of-the-Box:** Applications built with Cappuccino behave like native desktop software by default. This includes a rich palette of UI controls, **full keyboard navigation and focus management**, and **multi-level undo/redo support** — as you can see in this [Showcase application](https://ansb.uniklinik-freiburg.de/ThemeKitchenSinkA3). Also take a look at the [Cookbook tutorial](https://cappuccino-cookbook.5apps.com/).
* **🚀 Incredible Productivity:** Less code is needed. High-level abstractions and a powerful object-oriented model mean development is focused on application logic, not browser quirks.
@@ -84,7 +78,7 @@ Pure JavaScript and Objective-J can be mixed and matched, even in the same file.
## Find Out More
* **Official Website:** [cappuccino.dev](http://cappuccino.dev)
* **Documentation & Tutorials:** [cappuccino.dev/learn/](http://cappuccino.dev/learn/), [cappuccino cookbook](https://cappuccino-cookbook.5apps.com)
* **Documentation & Tutorials:** [cappuccino.dev/learn/](http://cappuccino.dev/learn/), [Browser online documentation](https://daboe01.github.io/CappDoc/), [cappuccino cookbook](https://cappuccino-cookbook.5apps.com)
* **Gitter Community Chat:** [gitter.im/cappuccino/cappuccino](https://gitter.im/cappuccino/cappuccino)
* **GitHub Wiki:** [github.com/cappuccino/cappuccino/wiki](https://github.com/cappuccino/cappuccino/wiki)
* **FAQ:** [cappuccino.dev/support/faq.html](http://cappuccino.dev/support/faq.html)
+267 -77
View File
@@ -8,33 +8,41 @@
@import <Foundation/Foundation.j>
@import <AppKit/CPTextView.j>
@import <AppKit/CPFontPanel.j>
@import <AppKit/CPRulerView.j>
@import <AppKit/CPSplitView.j>
@import <AppKit/CPButton.j>
@import <AppKit/CPTextField.j>
@import <AppKit/CPScrollView.j>
@import <AppKit/CPParagraphStyle.j>
@implementation AppController : CPObject
{
CPTextView _textView;
CPTextView _textView2;
CPTextView _textView;
CPTextView _textView2;
CPScrollView _scrollView;
CPScrollView _scrollView2;
}
- (void) openSheet:(id)sender
- (void)openSheet:(id)sender
{
var plusPopover =[CPPopover new];
var plusPopover = [CPPopover new];
[plusPopover setDelegate:self];
[plusPopover setAnimates:NO];
[plusPopover setBehavior:CPPopoverBehaviorTransient];
[plusPopover setAppearance:CPPopoverAppearanceMinimal];
var myViewController=[CPViewController new];
var myViewController = [CPViewController new];
[plusPopover setContentViewController:myViewController];
var textView = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, 200, 10)];
[textView setBackgroundColor:[CPColor whiteColor]];
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, 200, 150)];
[scrollView setDocumentView:textView];
[myViewController setView:scrollView];
[plusPopover showRelativeToRect:NULL ofView:sender preferredEdge:nil];
[[textView window] makeFirstResponder:textView];
}
- (void)orderFrontFontPanel:(id)sender
@@ -42,100 +50,282 @@
[[CPFontManager sharedFontManager] orderFrontFontPanel:self];
}
- (void)toggleRuler:(id)sender
{
[_scrollView setRulersVisible:![_scrollView rulersVisible]];
}
- (void)alignLeft:(id)sender
{
[_textView alignLeft:self];
}
- (void)alignCenter:(id)sender
{
[_textView alignCenter:self];
}
- (void)alignRight:(id)sender
{
[_textView alignRight:self];
}
- (void)alignJustified:(id)sender
{
[_textView alignJustified:self];
}
- (void)insertAttachment:(id)sender
{
// Insert modern spinner image attachment
var tempImageView = [[CPImageView alloc] initWithFrame:CGRectMake(0, 0, 32, 32)];
[tempImageView setImage:[[CPImage alloc] initWithContentsOfFile:@"Resources/spinner.gif" size:CGSizeMake(32, 32)]];
[_textView insertText:[CPTextStorage attributedStringWithAttachment:tempImageView]];
[_textView insertText:@" "];
// Insert an interactive button attachment
var tempButton = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 80, 28)];
[tempButton setTitle:@"Click Me"];
[_textView insertText:[CPTextStorage attributedStringWithAttachment:tempButton]];
[_textView insertText:@" "];
}
- (void)applicationDidFinishLaunching:(CPNotification)aNotification
{
// CPLogRegister(CPLogConsole);
var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],
contentView = [theWindow contentView];
[contentView setBackgroundColor:[CPColor colorWithWhite:0.95 alpha:1.0]];
var mybutton=[[CPButton alloc] initWithFrame:CGRectMake(0, 0, 250, 25)];
[mybutton setTitle:"Open sheet (must not be triggered by return)"]
[mybutton setTarget:self];
[mybutton setAction:@selector(openSheet:)];
[mybutton setKeyEquivalent:@"\r"];
// 1. Clean visual header / toolbar area
var toolbarView = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([contentView bounds]), 60)];
[toolbarView setAutoresizingMask:CPViewWidthSizable];
[toolbarView setBackgroundColor:[CPColor colorWithWhite:0.88 alpha:1.0]];
[contentView addSubview:toolbarView];
[contentView addSubview:mybutton];
var currentX = 15;
// Popover Trigger
var sheetButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 145, 30)];
[sheetButton setTitle:@"Open Popover Sheet"];
[sheetButton setTarget:self];
[sheetButton setAction:@selector(openSheet:)];
[toolbarView addSubview:sheetButton];
currentX += 155;
_textView = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, 500, 200)];
// Font Panel Trigger
var fontButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 125, 30)];
[fontButton setTitle:@"Show Font Panel"];
[fontButton setTarget:self];
[fontButton setAction:@selector(orderFrontFontPanel:)];
[toolbarView addSubview:fontButton];
currentX += 135;
// Toggle Ruler Trigger
var rulerButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 120, 30)];
[rulerButton setTitle:@"Toggle Ruler"];
[rulerButton setTarget:self];
[rulerButton setAction:@selector(toggleRuler:)];
[toolbarView addSubview:rulerButton];
currentX += 130;
// RTF Roundtrip Trigger
var rtfButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 150, 30)];
[rtfButton setTitle:@"RTF Round-trip ➔"];
[rtfButton setTarget:self];
[rtfButton setAction:@selector(makeRTF:)];
[toolbarView addSubview:rtfButton];
currentX += 160;
// Insert Attachment Trigger
var attachButton = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 140, 30)];
[attachButton setTitle:@"Insert Attachment"];
[attachButton setTarget:self];
[attachButton setAction:@selector(insertAttachment:)];
[toolbarView addSubview:attachButton];
currentX += 150;
// Text Alignment Group
var labelAlign = [[CPTextField alloc] initWithFrame:CGRectMake(currentX, 22, 45, 20)];
[labelAlign setStringValue:@"Align:"];
[labelAlign setFont:[CPFont systemFontOfSize:12]];
[toolbarView addSubview:labelAlign];
currentX += 45;
var alignLeftBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 50, 30)];
[alignLeftBtn setTitle:@"Left"];
[alignLeftBtn setTarget:self];
[alignLeftBtn setAction:@selector(alignLeft:)];
[toolbarView addSubview:alignLeftBtn];
currentX += 55;
var alignCenterBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 60, 30)];
[alignCenterBtn setTitle:@"Center"];
[alignCenterBtn setTarget:self];
[alignCenterBtn setAction:@selector(alignCenter:)];
[toolbarView addSubview:alignCenterBtn];
currentX += 65;
var alignRightBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 55, 30)];
[alignRightBtn setTitle:@"Right"];
[alignRightBtn setTarget:self];
[alignRightBtn setAction:@selector(alignRight:)];
[toolbarView addSubview:alignRightBtn];
currentX += 60;
var alignJustifyBtn = [[CPButton alloc] initWithFrame:CGRectMake(currentX, 15, 65, 30)];
[alignJustifyBtn setTitle:@"Justify"];
[alignJustifyBtn setTarget:self];
[alignJustifyBtn setAction:@selector(alignJustified:)];
[toolbarView addSubview:alignJustifyBtn];
// Default return key target test
var returnButton = [[CPButton alloc] initWithFrame:CGRectMake(CGRectGetWidth([contentView bounds]) - 270, 15, 250, 30)];
[returnButton setAutoresizingMask:CPViewMinXMargin];
[returnButton setTitle:@"Key Return Target"];
[returnButton setTarget:self];
[returnButton setAction:@selector(openSheet:)];
[returnButton setKeyEquivalent:@"\r"];
[toolbarView addSubview:returnButton];
// 2. Main content area: Split View Layout for side-by-side comparison
var splitView = [[CPSplitView alloc] initWithFrame:CGRectMake(0, 60, CGRectGetWidth([contentView bounds]), CGRectGetHeight([contentView bounds]) - 60)];
[splitView setVertical:YES];
[splitView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
var leftContainer = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([splitView bounds]) / 2, CGRectGetHeight([splitView bounds]))];
[leftContainer setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
var rightContainer = [[CPView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([splitView bounds]) / 2, CGRectGetHeight([splitView bounds]))];
[rightContainer setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[splitView addSubview:leftContainer];
[splitView addSubview:rightContainer];
[contentView addSubview:splitView];
// Left Container: label & Scroll/TextView containing Ruler
var leftLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 10, CGRectGetWidth([leftContainer bounds]) - 30, 20)];
[leftLabel setStringValue:@"Rich Text Editor"];
[leftLabel setFont:[CPFont boldSystemFontOfSize:14]];
[leftLabel setAutoresizingMask:CPViewWidthSizable];
[leftContainer addSubview:leftLabel];
_textView = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([leftContainer bounds]) - 30, CGRectGetHeight([leftContainer bounds]) - 70)];
[_textView setRichText:YES];
_textView2 = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, 1000, 200)];
_textView2._isRichText = NO;
[_textView setBackgroundColor:[CPColor whiteColor]];
_scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(15, 40, CGRectGetWidth([leftContainer bounds]) - 30, CGRectGetHeight([leftContainer bounds]) - 65)];
[_scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_scrollView setDocumentView:_textView];
// ATTACH THE NEW CPRULERVIEW SYSTEM
[_scrollView setHasHorizontalRuler:YES];
[_scrollView setRulersVisible:YES];
[leftContainer addSubview:_scrollView];
// Right Container: RTF Plain-Text Source and Parser Window
var rightLabel = [[CPTextField alloc] initWithFrame:CGRectMake(15, 10, CGRectGetWidth([rightContainer bounds]) - 30, 20)];
[rightLabel setStringValue:@"RTF Raw Output & Source Parser Window"];
[rightLabel setFont:[CPFont boldSystemFontOfSize:14]];
[rightLabel setAutoresizingMask:CPViewWidthSizable];
[rightContainer addSubview:rightLabel];
_textView2 = [[CPTextView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([rightContainer bounds]) - 30, CGRectGetHeight([rightContainer bounds]) - 70)];
_textView2._isRichText = NO;
[_textView2 setBackgroundColor:[CPColor whiteColor]];
var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 70, 520, 220)];
var scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(20, 550, 1020, 220)];
_scrollView2 = [[CPScrollView alloc] initWithFrame:CGRectMake(15, 40, CGRectGetWidth([rightContainer bounds]) - 30, CGRectGetHeight([rightContainer bounds]) - 65)];
[_scrollView2 setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable];
[_scrollView2 setDocumentView:_textView2];
[rightContainer addSubview:_scrollView2];
[scrollView setDocumentView:_textView];
[scrollView2 setDocumentView:_textView2];
// 3. Build application Main Menu
var mainMenu = [CPApp mainMenu];
[contentView addSubview: scrollView];
[contentView addSubview: scrollView2];
while ([mainMenu numberOfItems] > 0)
[mainMenu removeItemAtIndex:0];
// build our menu
var mainMenu = [CPApp mainMenu];
var item = [mainMenu insertItemWithTitle:@"Edit" action:nil keyEquivalent:nil atIndex:0],
editMenu = [[CPMenu alloc] initWithTitle:@"Edit Menu"];
while ([mainMenu numberOfItems] > 0)
[mainMenu removeItemAtIndex:0];
[editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"];
[editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"];
[editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"];
[editMenu addItemWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""];
[editMenu addItemWithTitle:@"Select All" action:@selector(selectAll:) keyEquivalent:@"a"];
[editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"];
[editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"];
[mainMenu setSubmenu:editMenu forItem:item];
var item = [mainMenu insertItemWithTitle:@"Edit" action:nil keyEquivalent:nil atIndex:0],
editMenu = [[CPMenu alloc] initWithTitle:@"Edit Menu"];
item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0];
// Format Menu
item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0];
var formatMenu = [[CPMenu alloc] initWithTitle:@"Format Menu"];
[formatMenu addItemWithTitle:@"Font panel" action:@selector(orderFrontFontPanel:) keyEquivalent:@"t"];
[formatMenu addItem:[CPMenuItem separatorItem]];
// Styles
[formatMenu addItemWithTitle:@"Bold" action:@selector(bold:) keyEquivalent:@"b"];
[formatMenu addItemWithTitle:@"Italic" action:@selector(italic:) keyEquivalent:@"i"];
[formatMenu addItemWithTitle:@"Underline" action:@selector(underline:) keyEquivalent:@"u"];
[formatMenu addItem:[CPMenuItem separatorItem]];
// Alignment
[formatMenu addItemWithTitle:@"Align Left" action:@selector(alignLeft:) keyEquivalent:@"{"];
[formatMenu addItemWithTitle:@"Center" action:@selector(alignCenter:) keyEquivalent:@"|"];
[formatMenu addItemWithTitle:@"Align Right" action:@selector(alignRight:) keyEquivalent:@"}"];
[formatMenu addItemWithTitle:@"Justify" action:@selector(alignJustified:) keyEquivalent:@""];
[mainMenu setSubmenu:formatMenu forItem:item];
[editMenu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"];
[editMenu addItemWithTitle:@"Copy" action:@selector(copy:) keyEquivalent:@"c"];
[editMenu addItemWithTitle:@"Paste" action:@selector(paste:) keyEquivalent:@"v"];
[editMenu addItemWithTitle:@"Delete" action:@selector(delete:) keyEquivalent:@""];
[editMenu addItemWithTitle:@"Select All" action:@selector(selectAll:) keyEquivalent:@"a"];
[editMenu addItemWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@"z"];
[editMenu addItemWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@"Z"];
// 4. Load Rich Sample Text content
[_textView insertText:@"123 456 "];
[mainMenu setSubmenu:editMenu forItem:item];
item = [mainMenu insertItemWithTitle:@"Format" action:nil keyEquivalent:nil atIndex:0];
var formatMenu = [[CPMenu alloc] initWithTitle:@"Format Menu"];
[formatMenu addItemWithTitle:@"Font panel" action:@selector(orderFrontFontPanel:) keyEquivalent:@"f"];
[formatMenu addItemWithTitle:@"Underline" action:@selector(underline:) keyEquivalent:@"u"];
[mainMenu setSubmenu:formatMenu forItem:item];
[_textView insertText:"123"];
var tempImageView = [[CPImageView alloc] initWithFrame:CGRectMake(0, 0, 32, 32)];
[tempImageView setImage:[[CPImage alloc] initWithContentsOfFile:@"Resources/spinner.gif" size:CGSizeMake(32, 32)]]
[_textView insertText:[CPTextStorage attributedStringWithAttachment:tempImageView]];
[_textView insertText:" 456 "];
var tempButton = [[CPButton alloc] initWithFrame:CGRectMake(0, 0, 64, 28)]
[_textView insertText:[CPTextStorage attributedStringWithAttachment:tempButton]];
var centeredParagraph=[CPParagraphStyle new];
[centeredParagraph setAlignment: CPCenterTextAlignment];
[_textView insertText:"\n"];
[_textView insertText:[[CPAttributedString alloc] initWithString:@"Fusce\n"
attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:"Arial" size:18], [CPColor redColor], [CPColor yellowColor]]
// Elegant Slate-Blue & Soft Light Blue paragraph
var centeredParagraph = [CPMutableParagraphStyle new];
[centeredParagraph setAlignment:CPCenterTextAlignment];
[_textView insertText:@"\n"];
var elegantForeground = [CPColor colorWithRed:0.18 green:0.24 blue:0.35 alpha:1.0]; // Slate Blue
var elegantBackground = [CPColor colorWithRed:0.92 green:0.95 blue:0.98 alpha:1.0]; // Soft Sky Blue tint
[_textView insertText:[[CPAttributedString alloc] initWithString:@"My Headline with blue background\n"
attributes:[CPDictionary dictionaryWithObjects:[centeredParagraph, [CPFont boldFontWithName:@"Arial" size:18], elegantForeground, elegantBackground]
forKeys:[CPParagraphStyleAttributeName, CPFontAttributeName, CPForegroundColorAttributeName, CPBackgroundColorAttributeName]]]];
[_textView insertText:"\n"];
[_textView insertText:[[CPAttributedString alloc] initWithString:@"Yellow\n"
attributes:[CPDictionary dictionaryWithObjects:[[CPFont boldFontWithName:"Arial" size:25], [CPColor yellowColor]]
forKeys:[CPFontAttributeName, CPBackgroundColorAttributeName]]]];
[theWindow orderFront:self];
// [_textView setEditable:NO];
[CPMenu setMenuBarVisible:YES];
// Highlighted Heading - Pine & Sage Green tones
[_textView insertText:@"\n"];
var showcaseForeground = [CPColor colorWithRed:0.15 green:0.25 blue:0.15 alpha:1.0]; // Forest Green
var showcaseBackground = [CPColor colorWithRed:0.94 green:0.97 blue:0.92 alpha:1.0]; // Light Sage Green
[_textView insertText:[[CPAttributedString alloc] initWithString:@"Interactive Ruler Showcase\n"
attributes:[CPDictionary dictionaryWithObjects:[[CPFont boldFontWithName:@"Arial" size:22], showcaseForeground, showcaseBackground]
forKeys:[CPFontAttributeName, CPForegroundColorAttributeName, CPBackgroundColorAttributeName]]]];
console.log([[CPFont systemFontOfSize:12] cssString])
var context = document.createElement("canvas").getContext("2d");
context.font = '12px Arial, sans-serif';
var testingText = 'A A A A A A A A';
console.log(ROUND(context.measureText(testingText).width));
console.log(ROUND([CPPlatformString sizeOfString:testingText withFont:[CPFont systemFontOfSize:12] forWidth:NULL].width));
// DEMONSTRATION OF CPRULERVIEW TAB MARKERS
var tabParagraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
var tab1 = [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:100.0];
var tab2 = [[CPTextTab alloc] initWithType:CPCenterTextAlignment location:220.0];
var tab3 = [[CPTextTab alloc] initWithType:CPRightTextAlignment location:340.0];
[tabParagraph setTabStops:[tab1, tab2, tab3]];
[_textView insertText:@"\n"];
[_textView insertText:[[CPAttributedString alloc] initWithString:@"\tTab1\tTab2\tTab3\n\tLeftAlign\tCenterAlign\tRightAlign\n"
attributes:[CPDictionary dictionaryWithObject:tabParagraph forKey:CPParagraphStyleAttributeName]]];
// DEMONSTRATION OF INDENTATION MARKERS
var indentParagraph = [[CPParagraphStyle defaultParagraphStyle] mutableCopy];
[indentParagraph setFirstLineHeadIndent:30.0];
[indentParagraph setHeadIndent:50.0];
[indentParagraph setTailIndent:-30.0];
[_textView insertText:@"\n"];
[_textView insertText:[[CPAttributedString alloc] initWithString:@"This paragraph has a first-line indent of 30pt, a head indent of 50pt, and a tail indent of -30pt. Check the horizontal ruler above to see how the indent markers align with this paragraph, and adjust them directly!\n"
attributes:[CPDictionary dictionaryWithObject:indentParagraph forKey:CPParagraphStyleAttributeName]]];
[theWindow orderFront:self];
[CPMenu setMenuBarVisible:YES];
}
- (void) makeRTF:(id)sender
- (void)makeRTF:(id)sender
{
[_textView2 setString: [_CPRTFProducer produceRTF:[_textView textStorage] documentAttributes: @{}] ];
var tc = [_CPRTFParser new];
+14 -4
View File
@@ -164,10 +164,20 @@ tableTestDragType = @"CPTableViewTestDragType";
- (void)removeColumn:(id)sender
{
// if ([[tableView tableColumns] containsObject:randomColumn])
[tableView removeTableColumn:randomColumn];
//else
// [tableView addTableColumn:randomColumn];
var columns = [tableView tableColumns];
// Check if we still have columns left to remove
if (columns && [columns count] > 0)
{
var columnToRemove = [columns lastObject];
[tableView removeTableColumn:columnToRemove];
CPLog.debug(@"Removed column with identifier: " + [columnToRemove identifier]);
}
else
{
CPLog.debug(@"No more columns to remove!");
}
}
- (void)addColumn:(id)sender