forked from ttscoff/nv
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathNSString_CustomTruncation.m
223 lines (176 loc) · 10.5 KB
/
NSString_CustomTruncation.m
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
//
// NSString_CustomTruncation.m
// Notation
//
// Created by Zachary Schneirov on 1/12/11.
/*Copyright (c) 2010, Zachary Schneirov. All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted
provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions
and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of
conditions and the following disclaimer in the documentation and/or other materials provided with
the distribution.
- Neither the name of Notational Velocity nor the names of its contributors may be used to endorse
or promote products derived from this software without specific prior written permission. */
#import "NSString_CustomTruncation.h"
#import "NSString_NV.h"
#import "GlobalPrefs.h"
@implementation NSString (CustomTruncation)
static NSMutableParagraphStyle *LineBreakingStyle();
static NSDictionary *GrayTextAttributes();
static NSDictionary *LineTruncAttributes();
static size_t EstimatedCharCountForWidth(float upToWidth);
- (NSString*)truncatedPreviewStringOfLength:(NSUInteger)bodyCharCount {
//try to get the underlying C-string buffer and copy only part of it
//this won't be exact because chars != bytes, but that's alright because it is expected to be further truncated by an NSTextFieldCell
CFStringEncoding bodyPreviewEncoding = CFStringGetFastestEncoding((CFStringRef)self);
const char * cStrPtr = CFStringGetCStringPtr((CFStringRef)self, bodyPreviewEncoding);
char *bodyPreviewBuffer = calloc(bodyCharCount + 1, sizeof(char));
CFIndex usedBufLen = bodyCharCount;
if (bodyCharCount > 1) {
if (cStrPtr && kCFStringEncodingUTF8 != bodyPreviewEncoding && kCFStringEncodingUnicode != bodyPreviewEncoding) {
//only attempt to copy the buffer directly if the fastest encoding is not a unicode variant
memcpy(bodyPreviewBuffer, cStrPtr, bodyCharCount);
} else {
bodyPreviewEncoding = kCFStringEncodingUTF8;
if ([self length] == bodyCharCount) {
//if this is supposed to be the entire string, don't waffle around
const char *fullUTF8String = [self UTF8String];
if (fullUTF8String) {
usedBufLen = bodyCharCount = strlen(fullUTF8String);
bodyPreviewBuffer = realloc(bodyPreviewBuffer, bodyCharCount + 1);
memcpy(bodyPreviewBuffer, fullUTF8String, bodyCharCount + 1);
goto replace;
}
}
if (!CFStringGetBytes((CFStringRef)self, CFRangeMake(0, bodyCharCount), bodyPreviewEncoding, ' ', FALSE,
(UInt8 *)bodyPreviewBuffer, bodyCharCount + 1, &usedBufLen)) {
NSLog(@"can't get utf8 string from '%@' (charcount: %lu)", self, (unsigned long)bodyCharCount);
free(bodyPreviewBuffer);
return nil;
}
}
}
replace:
//if bodyPreviewBuffer is a UTF-8 encoded string, then examine the string one UTF-8 sequence at a time to catch multi-byte breaks
if (bodyPreviewEncoding == kCFStringEncodingUTF8) {
replace_breaks_utf8(bodyPreviewBuffer, bodyCharCount);
} else {
replace_breaks(bodyPreviewBuffer, bodyCharCount);
}
NSString* truncatedBodyString = [[NSString alloc] initWithBytesNoCopy:bodyPreviewBuffer length:usedBufLen
encoding:CFStringConvertEncodingToNSStringEncoding(bodyPreviewEncoding) freeWhenDone:YES];
if (!truncatedBodyString) {
free(bodyPreviewBuffer);
NSLog(@"can't create cfstring from '%@' (cstr lens: %lu/%ld) with encoding %u (fastest = %u)", self, (unsigned long)bodyCharCount, usedBufLen, bodyPreviewEncoding, CFStringGetFastestEncoding((CFStringRef)self));
return nil;
}
return [truncatedBodyString autorelease];
}
static NSMutableDictionary *titleTruncAttrs = nil;
void ResetFontRelatedTableAttributes() {
[titleTruncAttrs release];
titleTruncAttrs = nil;
}
static NSMutableParagraphStyle *LineBreakingStyle() {
static NSMutableParagraphStyle *lineBreaksStyle = nil;
if (!lineBreaksStyle) {
lineBreaksStyle = [[NSMutableParagraphStyle alloc] init];
[lineBreaksStyle setLineBreakMode:NSLineBreakByTruncatingTail];
[lineBreaksStyle setTighteningFactorForTruncation:0.0];
}
return lineBreaksStyle;
}
static NSDictionary *GrayTextAttributes() {
static NSDictionary *grayTextAttributes = nil;
if (!grayTextAttributes) grayTextAttributes = [[NSDictionary dictionaryWithObjectsAndKeys:[NSColor grayColor], NSForegroundColorAttributeName, nil] retain];
return grayTextAttributes;
}
static NSDictionary *LineTruncAttributes() {
static NSDictionary *lineTruncAttributes = nil;
if (!lineTruncAttributes) lineTruncAttributes = [[NSDictionary dictionaryWithObjectsAndKeys:LineBreakingStyle(), NSParagraphStyleAttributeName, nil] retain];
return lineTruncAttributes;
}
#pragma mark fix truncation here
NSDictionary *LineTruncAttributesForTitle() {
if (!titleTruncAttrs) {
GlobalPrefs *prefs = [GlobalPrefs defaultPrefs];
unsigned int bitmap = [prefs tableColumnsBitmap];
float fontSize = [prefs tableFontSize];
BOOL usesBold = ColumnIsSet(NoteLabelsColumn, bitmap) || ColumnIsSet(NoteDateCreatedColumn, bitmap) ||
ColumnIsSet(NoteDateModifiedColumn, bitmap) || [prefs tableColumnsShowPreview];
titleTruncAttrs = [[NSMutableDictionary dictionaryWithObjectsAndKeys:[[LineBreakingStyle() mutableCopy] autorelease], NSParagraphStyleAttributeName,
(usesBold ? [NSFont boldSystemFontOfSize:fontSize] : [NSFont systemFontOfSize:fontSize]), NSFontAttributeName, nil] retain];
if (ColumnIsSet(NoteDateCreatedColumn, bitmap) || ColumnIsSet(NoteDateModifiedColumn, bitmap)) {
//account for right-"aligned" date string, which will be relatively constant, so this can be cached
NSString *dateTest=[NSString relativeDateStringWithAbsoluteTime:CFDateGetAbsoluteTime((CFDateRef)[NSDate dateWithNaturalLanguageString:@"April 1, 2013"])];
CGFloat multiplier=-4.7;
if (dateTest&&(dateTest.length>8)) {
multiplier=-6.1;
}
[[titleTruncAttrs objectForKey:NSParagraphStyleAttributeName] setTailIndent: fontSize * multiplier]; //avg of -55 for ~11-12 font size
}
}
return titleTruncAttrs;
}
static size_t EstimatedCharCountForWidth(float upToWidth) {
return (size_t)(upToWidth / ([[GlobalPrefs defaultPrefs] tableFontSize] / 2.5f));
}
//LineTruncAttributesForTags would be variable, depending on the note; each preview string will have its own copy of the nsdictionary
- (NSAttributedString*)attributedMultiLinePreviewFromBodyText:(NSAttributedString*)bodyText upToWidth:(float)upToWidth intrusionWidth:(float)intWidth {
//first line is title, truncated to a shorter width to account for date/time, using a negative -[NSMutableParagraphStyle setTailIndent:] value
//next "two" lines are wrapped body text, with a character-count estimation of essentially double that of a single-line preview
//also with an independent tailindent to account for a separately-drawn tags-string, if tags exist
//upToWidth will be used to manually truncate note-bodies only, and should be the full column width available
//intWidth will typically be the width of the tags string or other representation
size_t bodyCharCount = (EstimatedCharCountForWidth(upToWidth) * 2) - EstimatedCharCountForWidth(intWidth);
bodyCharCount = MIN(bodyCharCount, [bodyText length]);
NSMutableString *unattributedPreview = [[NSMutableString alloc] initWithCapacity:bodyCharCount + [self length] + 2];
NSString *truncatedBodyString = [[bodyText string] truncatedPreviewStringOfLength:bodyCharCount];
if (!truncatedBodyString){
[unattributedPreview release];
return nil;
}
[unattributedPreview appendString:self];
[unattributedPreview appendString:@"\n"];
[unattributedPreview appendString:truncatedBodyString];
NSMutableAttributedString *attributedStringPreview = [[NSMutableAttributedString alloc] initWithString:unattributedPreview];
//title is black (no added colors) and truncated with LineTruncAttributesForTitle()
//body is gray and truncated with a variable tail indent, depending on intruding tags
NSDictionary *bodyTruncDict = [NSDictionary dictionaryWithObjectsAndKeys:[[LineBreakingStyle() mutableCopy] autorelease],
NSParagraphStyleAttributeName, [NSColor grayColor], NSForegroundColorAttributeName, nil];
//set word-wrapping to let -[NSCell setTruncatesLastVisibleLine:] work
[[bodyTruncDict objectForKey:NSParagraphStyleAttributeName] setLineBreakMode:NSLineBreakByWordWrapping];
if (intWidth > 0.0) {
//there are tags; add an appropriately-sized tail indent to the body
[[bodyTruncDict objectForKey:NSParagraphStyleAttributeName] setTailIndent:-intWidth];
}
[attributedStringPreview addAttributes:LineTruncAttributesForTitle() range:NSMakeRange(0, [self length])];
[attributedStringPreview addAttributes:bodyTruncDict range:NSMakeRange([self length] + 1, [unattributedPreview length] - ([self length] + 1))];
[unattributedPreview release];
return [attributedStringPreview autorelease];
}
- (NSAttributedString*)attributedSingleLineTitle {
//show only a single line, with a tail indent large enough for both the date and tags (if there are any)
//because this method displays the title only, manual truncation isn't really necessary
//the highlighted version of this string should be bolded
NSMutableAttributedString *titleStr = [[NSMutableAttributedString alloc] initWithString:self attributes:LineTruncAttributesForTitle()];
return [titleStr autorelease];
}
- (NSAttributedString*)attributedSingleLinePreviewFromBodyText:(NSAttributedString*)bodyText upToWidth:(float)upToWidth {
//compute the char count for this note based on the width of the title column and the length of the receiver
size_t bodyCharCount = EstimatedCharCountForWidth(upToWidth) - [self length];
bodyCharCount = MIN(bodyCharCount, [bodyText length]);
NSString *truncatedBodyString = [[bodyText string] truncatedPreviewStringOfLength:bodyCharCount];
if (!truncatedBodyString) return nil;
NSMutableString *unattributedPreview = [self mutableCopy];
NSString *delimiter = NSLocalizedString(@" option-shift-dash ", @"title/description delimiter");
[unattributedPreview appendString:delimiter];
[unattributedPreview appendString:truncatedBodyString];
NSMutableAttributedString *attributedStringPreview = [[NSMutableAttributedString alloc] initWithString:unattributedPreview attributes:LineTruncAttributes()];
[attributedStringPreview addAttributes:GrayTextAttributes() range:NSMakeRange([self length], [unattributedPreview length] - [self length])];
[unattributedPreview release];
return [attributedStringPreview autorelease];
}
@end