diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index 2a1080269..639e6d9cb 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -377,13 +377,17 @@ CPRulerOrientationVertical = 1; } else { + // Allow client view to constrain initial placement + var client = [self clientView]; + if (client && [client respondsToSelector:@selector(rulerView:willAddMarker:atLocation:)]) + rulerLocation = [client rulerView:self willAddMarker:nil atLocation:rulerLocation]; + var newMarker = [[CPRulerMarker alloc] initWithRulerView:self markerLocation:rulerLocation imageValue:rulerLocation representedObject:nil]; [self addMarker:newMarker]; - var client = [self clientView]; if (client && [client respondsToSelector:@selector(rulerView:didAddMarker:)]) [client rulerView:self didAddMarker:newMarker]; @@ -405,6 +409,12 @@ CPRulerOrientationVertical = 1; var delta = isHorizontal ? (localPoint.x - _dragStartPoint.x) : (localPoint.y - _dragStartPoint.y), newLocation = Math.max(0.0, _dragStartLocation + delta); + // Constrain marker location to last possible position + var client = [self clientView]; + + if (client && [client respondsToSelector:@selector(rulerView:willMoveMarker:toLocation:)]) + newLocation = [client rulerView:self willMoveMarker:_draggingMarker toLocation:newLocation]; + [_draggingMarker setImageValue:newLocation]; [self _positionMarker:_draggingMarker]; @@ -426,7 +436,6 @@ CPRulerOrientationVertical = 1; [[_draggingMarker label] setTextColor:[CPColor colorWithWhite:0.2 alpha:1.0]]; } - var client = [self clientView]; if (client && [client respondsToSelector:@selector(rulerView:didMoveMarker:)]) [client rulerView:self didMoveMarker:_draggingMarker]; } diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index b9257fbc1..2ccaaa0a4 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2729,6 +2729,96 @@ var compareTabStops = function(obj1, obj2, context) { [self setNeedsDisplay:YES]; } +- (float)rulerView:(CPRulerView)aRulerView willMoveMarker:(CPRulerMarker)aMarker toLocation:(float)proposedLocation +{ + var rep = [aMarker representedObject]; + + // Indent markers can be dragged anywhere >= 0 + if (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent") + return Math.max(0.0, proposedLocation); + + var textLength = [_textStorage length]; + if (textLength === 0) + return Math.max(0.0, proposedLocation); + + var selectedRange = [self selectedRange], + targetRange = selectedRange; + + if (targetRange.length === 0) + targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; + + if (targetRange.length === 0) + return Math.max(0.0, proposedLocation); + + [_layoutManager _validateLayoutAndGlyphs]; + + var theString = [_textStorage string], + minLocation = 0.0, + currentParagraphStyle = [_textStorage attribute:CPParagraphStyleAttributeName atIndex:targetRange.location effectiveRange:nil] || [CPParagraphStyle defaultParagraphStyle], + tabStops = [currentParagraphStyle tabStops] || []; + + // Find which tab stop index this marker represents + var targetTabIndex = -1; + for (var i = 0; i < [tabStops count]; i++) + { + var tab = [tabStops objectAtIndex:i]; + if (tab === rep || ([tab location] === [rep location] && [tab alignment] === [rep alignment])) + { + targetTabIndex = i; + break; + } + } + + var start = targetRange.location, + end = CPMaxRange(targetRange), + tabCountInLine = 0, + lineStart = start; + + for (var i = start; i < end; i++) + { + var charCode = theString.charCodeAt(i); + if (charCode === 10 || charCode === 13) + { + tabCountInLine = 0; + lineStart = i + 1; + continue; + } + + if (charCode === 9) // '\t' + { + // If this tab corresponds to the marker being dragged + if (tabCountInLine === targetTabIndex || targetTabIndex === -1) + { + var precedingX = 0.0; + if (i === lineStart) + { + var isFirstLine = (lineStart === 0 || theString.charCodeAt(lineStart - 1) === 10 || theString.charCodeAt(lineStart - 1) === 13); + precedingX = isFirstLine ? [currentParagraphStyle firstLineHeadIndent] : [currentParagraphStyle headIndent]; + } + else + { + // Find right edge of glyph immediately preceding the tab + var glyphRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(i - 1, 1) inTextContainer:_textContainer]; + precedingX = CGRectGetMaxX(glyphRect); + } + + // Minimum safety spacer (e.g. + 2px after the preceding glyph) + var limit = precedingX + 2.0; + if (limit > minLocation) + minLocation = limit; + } + tabCountInLine++; + } + } + + return Math.max(minLocation, proposedLocation); +} + +- (float)rulerView:(CPRulerView)aRulerView willAddMarker:(CPRulerMarker)aMarker atLocation:(float)proposedLocation +{ + return [self rulerView:aRulerView willMoveMarker:aMarker toLocation:proposedLocation]; +} + @end @implementation CPTextView (CPTextViewDelegate) diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index eee7c2efe..7100bd051 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -138,38 +138,30 @@ var CPSystemTypesetterFactory, return [_layoutManager textContainers]; } -// Retrieves correct CPTextTab stop accounting for CPArray properties +// Retrieves correct CPTextTab stop accounting for custom stops and default intervals - (CPTextTab)textTabForWidth:(double)aWidth writingDirection:(CPWritingDirection)direction { var tabStops = [_currentParagraph tabStops], defaultInterval = [_currentParagraph defaultTabInterval] || 28.0; - // Calculate the immediate next standard tab stop location based on defaultInterval - var nextDefaultLocation = (Math.floor(aWidth / defaultInterval) + 1) * defaultInterval; + var l = tabStops ? [tabStops count] : 0; - // Search for the first custom tab stop strictly greater than aWidth - var nextCustomTab = nil; - - if (tabStops) + // 1. If custom tab stops exist ahead of current position, use the first one encountered + if (l > 0) { - var l = [tabStops count]; for (var i = 0; i < l; i++) { var tab = [tabStops objectAtIndex:i]; + if ([tab location] > aWidth) - { - nextCustomTab = tab; - break; - } + return tab; } } - // If a custom tab stop exists and is closer than (or at) the next default interval, use it - if (nextCustomTab && [nextCustomTab location] <= nextDefaultLocation) - return nextCustomTab; + // 2. Otherwise (or when all custom tab stops are behind the text), advance to the next default interval + var nextLocation = (Math.floor(aWidth / defaultInterval) + 1) * defaultInterval; - // Otherwise, snap to the immediate next regular tab interval - return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextDefaultLocation]; + return [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:nextLocation]; } - (BOOL)_flushRange:(CPRange)lineRange @@ -209,7 +201,7 @@ var CPSystemTypesetterFactory, [_layoutManager setLocation:CGPointMake(myX, _lineBase) forStartOfGlyphRange:lineRange]; [_layoutManager _setAdvancements:advancements forGlyphRange:lineRange]; - //fix the _lineFragments when fontsizes differ + // fix the _lineFragments when fontsizes differ var l = _lineFragments.length; for (var i = 0 ; i < l ; i++) @@ -255,7 +247,7 @@ var CPSystemTypesetterFactory, isTabStop = NO, isAttachment = NO, isWordWrapped = NO, - numberOfGlyphs= [_textStorage length], + numberOfGlyphs = [_textStorage length], leading, numLines = 0, theString = [_textStorage string], @@ -297,7 +289,7 @@ var CPSystemTypesetterFactory, for (; numLines != maxNumLines && glyphIndex < numberOfGlyphs; glyphIndex++) { - // check whether there any change in the attributes from here on + // check whether there is any change in the attributes from here on if (!CPLocationInRange(glyphIndex, _attributesRange)) { _currentAttributes = [_textStorage attributesAtIndex:glyphIndex effectiveRange:_attributesRange]; @@ -414,10 +406,10 @@ var CPSystemTypesetterFactory, // 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 + var currentCharCode = theString.charCodeAt(glyphIndex), rangeWidth = [theString.substr(measuringRange.location, measuringRange.length) sizeWithFont:currentFont inWidth:NULL].width + currentAnchor; - switch (currentCharCode) // faster than sending actionForControlCharacterAtIndex: called for each char. + switch (currentCharCode) { case CPAttachmentCharacter: { @@ -449,6 +441,7 @@ var CPSystemTypesetterFactory, } case 9: // '\t' { + // Measure against the actual text position before the tab stop var nextTab = [self textTabForWidth:prevRangeWidth + lineOrigin.x writingDirection:0]; isTabStop = YES; @@ -506,10 +499,6 @@ var CPSystemTypesetterFactory, 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; @@ -539,7 +528,7 @@ var CPSystemTypesetterFactory, isNewline = YES; isWordWrapped = YES; - glyphIndex = CPMaxRange(lineRange) - 1; // start the line starts directly at current character + glyphIndex = CPMaxRange(lineRange) - 1; } if (isNewline || isTabStop || isAttachment) @@ -571,8 +560,6 @@ var CPSystemTypesetterFactory, containerSizeHeight = containerSize.height; } - // 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]; @@ -598,7 +585,7 @@ var CPSystemTypesetterFactory, } } - // this is to "flush" the remaining characters + // Flush remaining characters if (lineRange.length) [self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO];