diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index dc69a131c..71c9e9992 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -25,6 +25,8 @@ @import "CPTextField.j" @import "CPColor.j" @import "CPFont.j" +@import "CPMenu.j" +@import "CPMenuItem.j" // Orientations matching AppKit standards // typedef enum CPRulerOrientation @@ -36,11 +38,11 @@ CPRulerOrientationVertical = 1 @class CPRulerView; -// MARK: - CPRulerMarker (Interactive High-Res DOM Handle) +// MARK: - CPRulerMarker (Interactive Handles with Dynamic Alignment Icons) @implementation CPRulerMarker : CPView { - CPRulerView _rulerView @accessors(readonly, property=rulerView); + CPRulerView _rulerView @accessors(property=rulerView); float _imageValue @accessors(property=imageValue); id _representedObject @accessors(property=representedObject); CPTextField _label; @@ -48,24 +50,140 @@ CPRulerOrientationVertical = 1 - (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation imageValue:(float)anImageValue representedObject:(id)anObject { - // Render a crisp, resizable 12x12 container for the Unicode indicator if (self = [super initWithFrame:CGRectMake(0, 0, 12, 12)]) { _rulerView = aRulerView; _imageValue = anImageValue; _representedObject = anObject; - // Beautiful, razor-sharp upward-pointing triangle for high-res screens _label = [[CPTextField alloc] initWithFrame:CGRectMake(0, 0, 12, 12)]; - [_label setStringValue:@"▲"]; [_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 +- (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 + { + [_label setStringValue:@"▲"]; // Indent markers point Up + } +} + +#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 @@ -215,11 +333,6 @@ CPRulerOrientationVertical = 1 _draggingMarker = newMarker; _dragStartPoint = localPoint; _dragStartLocation = rulerLocation; - - // NOTIFY CLIENT OF THE NEW MARKER ADDITION - var client = [self clientView]; - if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)]) - [client rulerView:self didAddMarker:newMarker]; } } @@ -293,6 +406,7 @@ CPRulerOrientationVertical = 1 _draggingMarker = nil; } + #pragma mark - #pragma mark DOM Layout Builder diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index c3b14a058..916549d58 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2465,14 +2465,12 @@ Sets the selection to a range of characters in response to user action. markerLocation:[paragraphStyle firstLineHeadIndent] imageValue:[paragraphStyle firstLineHeadIndent] representedObject:@"CPFirstLineIndent"]; - [firstLineMarker._label setStringValue:@"▼"]; // downward arrow styling [markers addObject:firstLineMarker]; var headMarker = [[CPRulerMarker alloc] initWithRulerView:ruler markerLocation:[paragraphStyle headIndent] imageValue:[paragraphStyle headIndent] representedObject:@"CPHeadIndent"]; - [headMarker._label setStringValue:@"▼"]; [markers addObject:headMarker]; [ruler setMarkers:markers]; @@ -2638,6 +2636,45 @@ var compareTabStops = function(obj1, obj2, context) { } } +- (void)rulerView:(CPRulerView)rulerView didUpdateMarker:(CPRulerMarker)marker oldTab:(id)oldTab +{ + var selectedRange = [self selectedRange]; + if (selectedRange.length === 0) + selectedRange = [self selectionRangeForProposedRange:CPMakeRange(selectedRange.location, 0) granularity:CPSelectByParagraph]; + + if (selectedRange.length === 0) + return; + + var paragraphStyle = [[self textStorage] attribute:CPParagraphStyleAttributeName atIndex:selectedRange.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(selectedRange)]; + + [_layoutManager textStorage:_textStorage + edited:0 + range:CPMakeRangeCopy(selectedRange) + changeInLength:0 + invalidatedRange:CPMakeRangeCopy(selectedRange)]; + + // Force layouts and view canvas update + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self setNeedsDisplay:YES]; +} + @end @implementation CPTextView (CPTextViewDelegate) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 186ea0a63..3c209b4fc 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 @@ -268,6 +268,11 @@ var CPSystemTypesetterFactory, currentParagraphMaximumLineHeight, currentParagraphLineSpacing; + // 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]) @@ -294,6 +299,57 @@ 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 line (no characters processed yet), + // we must update lineOrigin.x to use the newly loaded paragraph style! + if (lineRange.length === 0) + { + 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]; @@ -432,7 +488,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) { @@ -476,7 +533,11 @@ 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 = [];