-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathInputAttributesManager.mm
More file actions
236 lines (201 loc) · 8.59 KB
/
Copy pathInputAttributesManager.mm
File metadata and controls
236 lines (201 loc) · 8.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
#import "InputAttributesManager.h"
#import "AlignmentUtils.h"
#import "AttributeEntry.h"
#import "EnrichedTextInputView.h"
#import "ParagraphAttributesUtils.h"
#import "RangeUtils.h"
#import "StyleHeaders.h"
#import "ZeroWidthSpaceUtils.h"
@implementation InputAttributesManager {
NSMutableArray<NSValue *> *_dirtyRanges;
NSSet *_customAttributesKeys;
NSMutableSet *_removedTypingAttributes;
}
- (instancetype)initWithInput:(EnrichedTextInputView *)input {
self = [super init];
_input = input;
_dirtyRanges = [[NSMutableArray alloc] init];
_removedTypingAttributes = [[NSMutableSet alloc] init];
// setup customAttributes
NSMutableSet *_customAttrsSet = [[NSMutableSet alloc] init];
for (StyleBase *style in _input->stylesDict.allValues) {
[_customAttrsSet addObject:[style getKey]];
}
_customAttributesKeys = _customAttrsSet;
return self;
}
- (NSSet<NSString *> *)customAttributesKeys {
return _customAttributesKeys;
}
- (void)addDirtyRange:(NSRange)range {
[_dirtyRanges addObject:[NSValue valueWithRange:range]];
_dirtyRanges = [[RangeUtils connectAndDedupeRanges:_dirtyRanges] mutableCopy];
}
- (NSArray *)getDirtyRanges {
return _dirtyRanges;
}
- (void)shiftDirtyRangesWithEditedRange:(NSRange)editedRange
changeInLength:(NSInteger)delta {
if (delta == 0) {
return;
}
NSArray *shiftedRanges = [RangeUtils shiftRanges:_dirtyRanges
withEditedRange:editedRange
changeInLength:delta];
_dirtyRanges =
[[RangeUtils connectAndDedupeRanges:shiftedRanges] mutableCopy];
}
- (void)didRemoveTypingAttribute:(NSString *)key {
[_removedTypingAttributes addObject:key];
}
- (void)clearRemovedTypingAttributes {
[_removedTypingAttributes removeAllObjects];
}
- (void)handleDirtyRangesStyling {
// snapshot edited ranges (including 0-length ones) before filtering, so
// we can use them when recalculating adjacent ordered lists
NSArray<NSValue *> *editedRangesForListRecalc = [_dirtyRanges copy];
// Filter out 0 length ranges for styling.
NSPredicate *predicate = [NSPredicate
predicateWithBlock:^BOOL(NSValue *evaluatedObject, NSDictionary *_) {
return [evaluatedObject rangeValue].length > 0;
}];
[_dirtyRanges filterUsingPredicate:predicate];
for (NSValue *rangeObj in _dirtyRanges) {
NSRange dirtyRange = [rangeObj rangeValue];
// dirty range can sometimes be wrong because of apple doing some changes
// behind the scenes
if (dirtyRange.location + dirtyRange.length >
_input->textView.textStorage.string.length)
continue;
// firstly, get all styles' occurences in that dirty range
NSMutableDictionary *presentStyles = [[NSMutableDictionary alloc] init];
for (StyleBase *style in _input->stylesDict.allValues) {
// the dict has keys of StyleType NSNumber and values of an array of all
// occurences
presentStyles[@([[style class] getType])] = [style all:dirtyRange];
}
// now reset the attributes to default ones
[_input->textView.textStorage setAttributes:_input->defaultTypingAttributes
range:dirtyRange];
// Restore ZWS layout metadata that is stored in regular attributes and was
// overwritten by the default-attributes reset above.
[ZeroWidthSpaceUtils applyKernForZeroWidthSpacesInRange:dirtyRange
host:_input];
// Sort style types so paragraph styles come first. Their broad visual
// attributes (e.g. foreground color, font) are laid down before inline
// styles override them on their specific sub-ranges.
NSArray *sortedStyleTypes = [presentStyles.allKeys
sortedArrayUsingComparator:^NSComparisonResult(NSNumber *a,
NSNumber *b) {
BOOL aPara = [_input->stylesDict[a] isParagraph];
BOOL bPara = [_input->stylesDict[b] isParagraph];
if (aPara == bPara)
return NSOrderedSame;
return aPara ? NSOrderedAscending : NSOrderedDescending;
}];
// re-apply meta-attributes and apply visual styling following the saved
// occurences.
for (NSNumber *styleType in sortedStyleTypes) {
StyleBase *style = _input->stylesDict[styleType];
if (style == nullptr)
continue;
for (StylePair *stylePair in presentStyles[styleType]) {
NSRange occurenceRange = [stylePair.rangeValue rangeValue];
[style reapplyFromStylePair:stylePair];
[style applyStyling:occurenceRange];
}
}
}
// refresh ordered lists adjacent to any edit
OrderedListStyle *orderedListStyle =
(OrderedListStyle *)_input->stylesDict[@([OrderedListStyle getType])];
if (orderedListStyle != nil) {
for (NSValue *rangeObj in editedRangesForListRecalc) {
[orderedListStyle
recalculateListsAroundEditedRange:[rangeObj rangeValue]];
}
}
// do the typing attributes management, with no selection
[self manageTypingAttributesWithOnlySelection:NO];
[_dirtyRanges removeAllObjects];
}
- (void)manageTypingAttributesWithOnlySelection:(BOOL)onlySelectionChanged {
EnrichedInputTextView *textView = _input->textView;
NSRange selectedRange = textView.selectedRange;
// Typing attributes get reset (except alignment) when only selection changed
// to an empty line (or empty line with newline).
if (onlySelectionChanged) {
NSRange paragraphRange =
[textView.textStorage.string paragraphRangeForRange:selectedRange];
// User changed selection to an empty line (or empty line with a newline).
if (paragraphRange.length == 0 ||
(paragraphRange.length == 1 &&
[[NSCharacterSet newlineCharacterSet]
characterIsMember:[textView.textStorage.string
characterAtIndex:paragraphRange
.location]])) {
NSParagraphStyle *currentTypingStyle =
textView.typingAttributes[NSParagraphStyleAttributeName];
NSTextAlignment savedAlignment = currentTypingStyle
? currentTypingStyle.alignment
: NSTextAlignmentNatural;
[ParagraphAttributesUtils resetTypingAttributes:_input
preservingAlignment:savedAlignment];
return;
}
}
// General typing attributes management.
// Firstly, we make sure only default + custom + paragraph typing attribtues
// are left.
NSMutableDictionary *newAttrs = [_input->defaultTypingAttributes mutableCopy];
for (NSString *key in _input->textView.typingAttributes.allKeys) {
if ([_customAttributesKeys containsObject:key]) {
if ([key isEqualToString:NSParagraphStyleAttributeName]) {
// NSParagraphStyle for paragraph styles -> only keep the textLists
// property
NSParagraphStyle *pStyle =
(NSParagraphStyle *)_input->textView
.typingAttributes[NSParagraphStyleAttributeName];
if (pStyle != nullptr && pStyle.textLists.count >= 1) {
NSMutableParagraphStyle *newPStyle =
[[NSMutableParagraphStyle alloc] init];
newPStyle.textLists = pStyle.textLists;
newAttrs[NSParagraphStyleAttributeName] = newPStyle;
}
} else {
// Inline styles -> keep the key/value as a whole
newAttrs[key] = _input->textView.typingAttributes[key];
}
}
}
// Then, we add typingAttributes from present inline styles.
// We check for the previous character to naturally extend typing attributes.
// getEntryIfPresent properly returns nullptr for styles that we don't want to
// extend this way. Attributes from _removedTypingAttributes aren't added
// because they were just removed.
for (StyleBase *style in _input->stylesDict.allValues) {
if ([style isParagraph])
continue;
if ([_removedTypingAttributes containsObject:[style getKey]])
continue;
AttributeEntry *entry = nullptr;
if (selectedRange.location > 0) {
entry =
[style getEntryIfPresent:NSMakeRange(selectedRange.location - 1, 1)];
}
if (entry == nullptr)
continue;
newAttrs[entry.key] = entry.value;
}
// Apply active styles to typing attributes only for styles that require it so
// the cursor correctly reflects the current formatting state (e.g. heading
// size).
for (StyleBase *style in _input->stylesDict.allValues) {
if ([style appliesStylingToTyping] && [style detect:selectedRange]) {
[style applyStylingToTypingAttrs:newAttrs];
}
}
textView.typingAttributes = newAttrs;
}
@end