mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-08-25 04:57:03 +00:00
improved: tabstop handles
This commit is contained in:
+124
-10
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user