diff --git a/AppKit/CPScrollView.j b/AppKit/CPScrollView.j index ac042e3a2..444aee022 100644 --- a/AppKit/CPScrollView.j +++ b/AppKit/CPScrollView.j @@ -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 diff --git a/AppKit/CPTableView.j b/AppKit/CPTableView.j index 7a1ea6129..2f66bc3d7 100644 --- a/AppKit/CPTableView.j +++ b/AppKit/CPTableView.j @@ -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]; } diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j index 7b2f03590..7e51b40b4 100644 --- a/AppKit/CPTextField.j +++ b/AppKit/CPTextField.j @@ -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 diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index f64d8d6a4..fd0accf43 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -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; diff --git a/AppKit/CPTextView/CPParagraphStyle.j b/AppKit/CPTextView/CPParagraphStyle.j index 54d88ee73..e89cda17a 100644 --- a/AppKit/CPTextView/CPParagraphStyle.j +++ b/AppKit/CPTextView/CPParagraphStyle.j @@ -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 @import - -@import "CPText.j" - -CPLeftTabStopType = 0; +@import 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 diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j new file mode 100644 index 000000000..d72e9803a --- /dev/null +++ b/AppKit/CPTextView/CPRulerView.j @@ -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 +@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 diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 599b587cb..32d9c6b88 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -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

. // - 'Backspace' on empty: Prevents inserting junk characters on iPadOS. e.preventDefault(); diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 49169206f..06c8a368f 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -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; diff --git a/AppKit/CPTextView/_CPRTFParser.j b/AppKit/CPTextView/_CPRTFParser.j index 2fdb03985..df1569481 100644 --- a/AppKit/CPTextView/_CPRTFParser.j +++ b/AppKit/CPTextView/_CPRTFParser.j @@ -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) diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index c69d481ca..e015c0bcb 100644 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -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 diff --git a/AppKit/CPWindow/CPWindow.j b/AppKit/CPWindow/CPWindow.j index 1b233d067..73530fd08 100644 --- a/AppKit/CPWindow/CPWindow.j +++ b/AppKit/CPWindow/CPWindow.j @@ -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]; diff --git a/README.markdown b/README.markdown index 859570108..300f255b6 100644 --- a/README.markdown +++ b/README.markdown @@ -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) diff --git a/Tests/Manual/CPTextView/AppController.j b/Tests/Manual/CPTextView/AppController.j index 750a34d00..7abb6d277 100755 --- a/Tests/Manual/CPTextView/AppController.j +++ b/Tests/Manual/CPTextView/AppController.j @@ -8,33 +8,41 @@ @import @import @import +@import +@import +@import +@import +@import +@import @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]; diff --git a/Tests/Manual/TableTest/OldTest/AppController.j b/Tests/Manual/TableTest/OldTest/AppController.j index d28b73b64..8c8d4741f 100644 --- a/Tests/Manual/TableTest/OldTest/AppController.j +++ b/Tests/Manual/TableTest/OldTest/AppController.j @@ -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