diff --git a/AppKit/CPTextView/CPRulerView.j b/AppKit/CPTextView/CPRulerView.j index ab0e4d3ff..2a1080269 100644 --- a/AppKit/CPTextView/CPRulerView.j +++ b/AppKit/CPTextView/CPRulerView.j @@ -28,7 +28,6 @@ @import "CPMenu.j" @import "CPMenuItem.j" -// Orientations matching AppKit standards @typedef CPRulerOrientation CPHorizontalRuler = 0; CPVerticalRuler = 1; @@ -40,7 +39,7 @@ CPRulerOrientationVertical = 1; @class CPTextTab; -// MARK: - CPRulerMarker (Interactive Handles with Dynamic Alignment Icons) +// MARK: - CPRulerMarker @implementation CPRulerMarker : CPView { @@ -59,13 +58,18 @@ CPRulerOrientationVertical = 1; _imageValue = anImageValue; _representedObject = anObject; + // CRITICAL: Subviews must not intercept mouse events so CPRulerView receives all mouseDragged: events + [self setHitTests:NO]; + _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]; + [_label setHitTests:NO]; [self addSubview:_label]; _customHandleView = [[CPView alloc] initWithFrame:CGRectMakeZero()]; + [_customHandleView setHitTests:NO]; [self addSubview:_customHandleView]; [self updateMarkerIcon]; @@ -90,8 +94,6 @@ CPRulerOrientationVertical = 1; [self updateMarkerIcon]; } -// Dynamically sets the Unicode triangle direction based on the alignment or indent type, -// or draws custom split-height grab handles for indentation controls. - (void)updateMarkerIcon { var isIndentMarker = (_representedObject === @"CPFirstLineIndent" || _representedObject === @"CPHeadIndent"); @@ -103,28 +105,19 @@ CPRulerOrientationVertical = 1; var frame = [self bounds]; [_customHandleView setFrame:CGRectMake(0, 0, frame.size.width, frame.size.height)]; - - // Remove old internal rendering to update cleanly [[_customHandleView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)]; var isFirstLine = (_representedObject === @"CPFirstLineIndent"); + [_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.45 alpha:1.0]]; - // Dark outline/border representation - [_customHandleView setBackgroundColor:[CPColor colorWithWhite:0.5 alpha:1.0]]; - - // Inner fill (top handle is lighter, bottom is slightly darker) - var innerView = [[CPView alloc] initWithFrame:CGRectMake(1.0, 1.0, frame.size.width - 2.0, frame.size.height - 2.0)]; - - if (isFirstLine) - [innerView setBackgroundColor:[CPColor colorWithWhite:0.92 alpha:1.0]]; - else - [innerView setBackgroundColor:[CPColor colorWithWhite:0.80 alpha:1.0]]; - + var innerView = [[CPView alloc] initWithFrame:CGRectMake(1.0, 1.0, Math.max(1.0, frame.size.width - 2.0), Math.max(1.0, frame.size.height - 2.0))]; + [innerView setHitTests:NO]; + [innerView setBackgroundColor:isFirstLine ? [CPColor colorWithWhite:0.95 alpha:1.0] : [CPColor colorWithWhite:0.80 alpha:1.0]]; [_customHandleView addSubview:innerView]; - // Horizontal indicator line to visually guide drag interactions - var gripLine = [[CPView alloc] initWithFrame:CGRectMake(Math.floor(frame.size.width / 2.0) - 1.0, 2.0, 1.0, frame.size.height - 4.0)]; - [gripLine setBackgroundColor:[CPColor colorWithWhite:0.6 alpha:1.0]]; + var gripLine = [[CPView alloc] initWithFrame:CGRectMake(Math.floor(frame.size.width / 2.0) - 1.0, 1.0, 1.0, Math.max(1.0, frame.size.height - 3.0))]; + [gripLine setHitTests:NO]; + [gripLine setBackgroundColor:[CPColor colorWithWhite:0.5 alpha:1.0]]; [innerView addSubview:gripLine]; } else @@ -137,33 +130,23 @@ CPRulerOrientationVertical = 1; { var align = [_representedObject alignment]; if (align === CPLeftTextAlignment) - [_label setStringValue:@"▶"]; // Left-aligned points Right + [_label setStringValue:@"▶"]; else if (align === CPCenterTextAlignment) - [_label setStringValue:@"▼"]; // Center-aligned points Down + [_label setStringValue:@"▼"]; else if (align === CPRightTextAlignment) - [_label setStringValue:@"◀"]; // Right-aligned points Left - } - else if ([_representedObject isKindOfClass:[CPString class]]) - { - if (_representedObject === @"CPTailIndent") - [_label setStringValue:@"⥘"]; // Solid downward triangle for tail indent - else - [_label setStringValue:@"⇡"]; // Fallback standard up marker + [_label setStringValue:@"◀"]; } else { - [_label setStringValue:@"⇡"]; // Fallback standard up marker + [_label setStringValue:@"▲"]; } } } -// MARK: - -// 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:@""], @@ -182,17 +165,11 @@ CPRulerOrientationVertical = 1; [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"; - } + if (_representedObject === @"CPFirstLineIndent") + deleteTitle = @"Reset 1st line indentation"; + else if (_representedObject === @"CPHeadIndent") + deleteTitle = @"Reset head indentation"; var itemDelete = [menu addItemWithTitle:deleteTitle action:@selector(deleteMarker:) keyEquivalent:@""]; [itemDelete setTarget:self]; @@ -200,66 +177,49 @@ CPRulerOrientationVertical = 1; return menu; } -- (void)changeTypeToLeft:(id)sender -{ - [self _changeAlignment:CPLeftTextAlignment]; -} - -- (void)changeTypeToCenter:(id)sender -{ - [self _changeAlignment:CPCenterTextAlignment]; -} - -- (void)changeTypeToRight:(id)sender -{ - [self _changeAlignment:CPRightTextAlignment]; -} +- (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]; + var oldTab = _representedObject, + 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) +// MARK: - CPRulerView @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; + CPRulerMarker _draggingMarker @accessors(getter=draggingMarker); CGPoint _dragStartPoint; float _dragStartLocation; } @@ -287,7 +247,16 @@ CPRulerOrientationVertical = 1; [self updateRuler]; } -// Markers registration +- (CPView)clientView +{ + return _clientView || [_scrollView documentView]; +} + +- (void)setClientView:(CPView)aView +{ + _clientView = aView; +} + - (void)addMarker:(CPRulerMarker)aMarker { if ([_markers containsObject:aMarker]) @@ -317,14 +286,20 @@ CPRulerOrientationVertical = 1; [self addSubview:marker]; [self _positionMarker:marker]; } + [self updateRuler]; } - (CPRulerMarker)_markerAtPoint:(CGPoint)aPoint { - for (var i = 0; i < [_markers count]; i++) + // Search in reverse order so indent paddles (added last) are prioritized over overlapping 0-location tabs + for (var i = [_markers count] - 1; i >= 0; i--) { - var marker = [_markers objectAtIndex:i]; - if (CGRectContainsPoint([marker frame], aPoint)) + var marker = [_markers objectAtIndex:i], + frame = [marker frame]; + + // Expanded hit-box by 3px on all sides for easy grabbing + var hitFrame = CGRectMake(frame.origin.x - 3.0, frame.origin.y - 2.0, frame.size.width + 6.0, frame.size.height + 4.0); + if (CGRectContainsPoint(hitFrame, aPoint)) return marker; } return nil; @@ -340,55 +315,48 @@ CPRulerOrientationVertical = 1; isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal), rulerHeight = CGRectGetHeight([self bounds]), rulerWidth = CGRectGetWidth([self bounds]), - markerLocation = [aMarker imageValue]; + markerLocation = [aMarker imageValue], + rep = [aMarker representedObject]; if (isHorizontal) { - var x = markerLocation - scrollPoint.x - 6.0, // Center the 12px wide marker + var x = markerLocation - scrollPoint.x - 6.0, y = rulerHeight - 11.0, w = 12.0, h = 12.0; - - // Align the First Line Indent (upper half) and Head Indent (lower half) controls - if ([aMarker representedObject] === @"CPFirstLineIndent") + + if (rep === @"CPFirstLineIndent") { y = 0.0; h = Math.floor(rulerHeight / 2.0); } - else if ([aMarker representedObject] === @"CPHeadIndent") + else if (rep === @"CPHeadIndent") { y = Math.floor(rulerHeight / 2.0); - h = rulerHeight - y - 1.0; // Subtract 1px to stay cleanly above bottom border + h = rulerHeight - y - 1.0; } else { - // Keep normal horizontal markers 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; + if (x < -6.0) x = -6.0; + else if (x + 12.0 > rulerWidth) x = rulerWidth - 12.0; } - + [aMarker setFrame:CGRectMake(x, y, w, h)]; } 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; - + + 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)]; } } -// MARK: - -// MARK: Interaction Handlers +// MARK: - Interaction Handlers - (void)mouseDown:(CPEvent)anEvent { @@ -400,7 +368,6 @@ CPRulerOrientationVertical = 1; 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) { @@ -408,7 +375,6 @@ CPRulerOrientationVertical = 1; _dragStartPoint = localPoint; _dragStartLocation = [_draggingMarker imageValue]; } - // 2. Otherwise, create a new marker dynamically where the user clicked else { var newMarker = [[CPRulerMarker alloc] initWithRulerView:self @@ -416,12 +382,11 @@ CPRulerOrientationVertical = 1; 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; @@ -438,33 +403,29 @@ CPRulerOrientationVertical = 1; isHorizontal = (_orientation === CPHorizontalRuler || _orientation === CPRulerOrientationHorizontal); var delta = isHorizontal ? (localPoint.x - _dragStartPoint.x) : (localPoint.y - _dragStartPoint.y), - newLocation = _dragStartLocation + delta; - - if (newLocation < 0) newLocation = 0; + newLocation = Math.max(0.0, _dragStartLocation + delta); [_draggingMarker setImageValue:newLocation]; - - // Smoothly redraw ruler and margin bounds on every drag step - [self updateRuler]; - - // 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); + [self _positionMarker:_draggingMarker]; + + var rep = [_draggingMarker representedObject], + isIndent = (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent"); + + var draggedOff = !isIndent && (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]; @@ -477,10 +438,12 @@ CPRulerOrientationVertical = 1; 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); + rep = [_draggingMarker representedObject], + isIndent = (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent"); + + var draggedOff = !isIndent && (isHorizontal + ? (localPoint.y < -15 || localPoint.y > CGRectGetHeight([self bounds]) + 15) + : (localPoint.x < -15 || localPoint.x > CGRectGetWidth([self bounds]) + 15)); if (draggedOff) { @@ -492,22 +455,19 @@ CPRulerOrientationVertical = 1; } 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; [self updateRuler]; } -// MARK: - -// MARK: DOM Layout Builder +// MARK: - Layout Builder - (void)updateRuler { - // Wipe subviews to redraw the dynamic visible tick lines/numbers [self setSubviews:@[]]; if (!_scrollView) @@ -526,12 +486,11 @@ CPRulerOrientationVertical = 1; 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 setHitTests:NO]; [bottomBorder setBackgroundColor:[CPColor colorWithWhite:0.75 alpha:1.0]]; [self addSubview:bottomBorder]; - // Find indent markers to determine background highlight boundaries var firstLineMarker = nil, headMarker = nil; for (var i = 0; i < [_markers count]; i++) @@ -545,31 +504,30 @@ CPRulerOrientationVertical = 1; var halfHeight = Math.floor(rulerHeight / 2.0); - // Draw First Line Indent background - top half (lighter gray) if (firstLineMarker) { var firstLineX = [firstLineMarker imageValue] - scrollPoint.x; if (firstLineX > 0) { var firstLineBg = [[CPView alloc] initWithFrame:CGRectMake(0, 0, firstLineX, halfHeight)]; + [firstLineBg setHitTests:NO]; [firstLineBg setBackgroundColor:[CPColor colorWithWhite:0.93 alpha:1.0]]; [self addSubview:firstLineBg]; } } - // Draw Head Indent background - bottom half (slightly darker gray) if (headMarker) { var headX = [headMarker imageValue] - scrollPoint.x; if (headX > 0) { var headBg = [[CPView alloc] initWithFrame:CGRectMake(0, halfHeight, headX, rulerHeight - halfHeight - 1.0)]; + [headBg setHitTests:NO]; [headBg setBackgroundColor:[CPColor colorWithWhite:0.86 alpha:1.0]]; [self addSubview:headBg]; } } - // Render ruler tick lines and labels on top of shaded areas for (var val = start; val <= end; val += 10) { if (val < 0) continue; @@ -579,30 +537,21 @@ CPRulerOrientationVertical = 1; 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 setHitTests:NO]; [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; - } + 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 setHitTests:NO]; [label setStringValue:[CPString stringWithFormat:@"%d", val]]; [label setFont:[CPFont systemFontOfSize:8.0]]; [label setTextColor:[CPColor colorWithWhite:0.4 alpha:1.0]]; @@ -659,12 +608,11 @@ CPRulerOrientationVertical = 1; } } - // Reposition and display active markers + // Add back existing markers without re-creating them for (var i = 0; i < [_markers count]; i++) { var marker = [_markers objectAtIndex:i]; - if ([marker superview] !== self) - [self addSubview:marker]; + [self addSubview:marker]; [self _positionMarker:marker]; } } diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index ad6c1cbc0..b9257fbc1 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -2472,6 +2472,10 @@ Sets the selection to a range of characters in response to user action. if (!ruler) return; + // Do not rebuild markers if user is currently dragging one + if ([ruler draggingMarker]) + return; + var selectedRange = [self selectedRange], paragraphStyle = [CPParagraphStyle defaultParagraphStyle], currentAttributes = _typingAttributes; @@ -2480,10 +2484,10 @@ Sets the selection to a range of characters in response to user action. if (textLength > 0) { var charIndex = selectedRange.location; - - // Safety bounds checks for cursor placements + if (charIndex >= textLength) charIndex = textLength - 1; + if (charIndex < 0) charIndex = 0; @@ -2497,7 +2501,6 @@ Sets the selection to a range of characters in response to user action. 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], @@ -2508,7 +2511,6 @@ Sets the selection to a range of characters in response to user action. [markers addObject:marker]; } - // B. Add Indentation Handles (First line indent & Head indent) var firstLineMarker = [[CPRulerMarker alloc] initWithRulerView:ruler markerLocation:[paragraphStyle firstLineHeadIndent] imageValue:[paragraphStyle firstLineHeadIndent] @@ -2524,7 +2526,6 @@ Sets the selection to a range of characters in response to user action. [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; @@ -2533,72 +2534,6 @@ var compareTabStops = function(obj1, obj2, context) { - (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; @@ -2610,6 +2545,7 @@ var compareTabStops = function(obj1, obj2, context) { { if (charIndex >= textLength) charIndex = textLength - 1; + if (charIndex < 0) charIndex = 0; @@ -2620,69 +2556,31 @@ var compareTabStops = function(obj1, obj2, context) { paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; var mutableStyle = [paragraphStyle mutableCopy], - oldTab = [marker representedObject]; + newTab = [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:[marker imageValue]], + tabs = [[mutableStyle tabStops] mutableCopy]; - if (!oldTab) - return; + [tabs addObject:newTab]; + [tabs sortUsingFunction:compareTabStops context:nil]; - // A. Handle standard tab stops - if ([oldTab isKindOfClass:[CPTextTab class]]) - { - var newTab = [[CPTextTab alloc] initWithType:[oldTab alignment] location:[marker imageValue]], - tabs = [[mutableStyle tabStops] mutableCopy]; + [mutableStyle setTabStops:tabs]; + [marker setRepresentedObject:newTab]; - [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) + if (targetRange.length === 0 && textLength > 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)]; + [_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]; - } - - [_layoutManager _validateLayoutAndGlyphs]; - [self sizeToFit]; - [self setNeedsDisplay:YES]; + [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; } -- (void)rulerView:(CPRulerView)rulerView didRemoveMarker:(CPRulerMarker)marker +- (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; @@ -2694,12 +2592,78 @@ var compareTabStops = function(obj1, obj2, context) { { 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], + rep = [marker representedObject]; + + if (!rep) + return; + + // A. Handle standard tab stops + if ([rep isKindOfClass:[CPTextTab class]]) + { + var newTab = [[CPTextTab alloc] initWithType:[rep alignment] location:[marker imageValue]], + tabs = [[mutableStyle tabStops] mutableCopy]; + + [tabs removeObject:rep]; + [tabs addObject:newTab]; + [tabs sortUsingFunction:compareTabStops context:nil]; + + [mutableStyle setTabStops:tabs]; + [marker setRepresentedObject:newTab]; + } + + // B. Handle Indentation Marker drags directly by string literal + else if (rep === @"CPFirstLineIndent") + [mutableStyle setFirstLineHeadIndent:[marker imageValue]]; + else if (rep === @"CPHeadIndent") + [mutableStyle setHeadIndent:[marker imageValue]]; + else if (rep === @"CPTailIndent") + [mutableStyle setTailIndent:[marker imageValue]]; + + var targetRange = selectedRange; + + if (targetRange.length === 0 && textLength > 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)]; + } + + [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; + + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self setNeedsDisplay:YES]; +} + +- (void)rulerView:(CPRulerView)rulerView didRemoveMarker:(CPRulerMarker)marker +{ + 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]; @@ -2711,31 +2675,20 @@ var compareTabStops = function(obj1, obj2, context) { 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) + if (targetRange.length === 0 && textLength > 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)]; + [_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]; - } + [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; } - (void)rulerView:(CPRulerView)rulerView didUpdateMarker:(CPRulerMarker)marker oldTab:(id)oldTab @@ -2759,19 +2712,11 @@ var compareTabStops = function(obj1, obj2, context) { [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)]; + [_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)]; if (selectedRange.length === 0) { @@ -2779,7 +2724,6 @@ var compareTabStops = function(obj1, obj2, context) { [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; } - // Force layouts and view canvas update [_layoutManager _validateLayoutAndGlyphs]; [self sizeToFit]; [self setNeedsDisplay:YES];