Skip to content

Commit 2e9b935

Browse files
douglashilllucasderraugh
authored andcommitted
Text size setting (#620)
* Add font size user defaults key Users should be able to control text size. The value in this user defaults key is the starting point for allowing any part of the GitUp UI to be scalable. One limitation of storing the font size in the standard user defaults is that this is global state so an app using GitUpKit that wants to show two diff views at the same time would not be able to make them use different font sizes. I don’t think this use case is particularly important. As a framework, GitUpKit should not register default values in user defaults, and it can’t guarantee that the app using GitUpKit registers a sensible default for this key. Therefore the font size should always be read from user defaults wrapped in a `GIFontSize()` function call, which ensures the value is greater than zero The `GIFontSize` function was put in `GIAppKit.h/m` because that’s where other GitUpKit user defaults keys are defined. I didn’t think this fits in in `GIFunctions` because it’s only available in the Mac version of GitUpKit. Putting it in `GIFunctions` wrapped in `#if __GI_HAS_APPKIT__` would work too but makes less sense to me. I thought it would be messy to do the `#ifdef __cplusplus` like in `GIFunctions` for this one function, so I used `FOUNDATION_EXPORT`. (My preferred way to do this would be to drop the `#ifdef __cplusplus` from `GIFunctions` and define a `GIEXPORT` used for every public function.) Naming of the constant: I mirrored `GICommitMessageViewUserDefaultKey_ShowMargins` but removed the `CommitMessageView` part because this one is more general. * Update MainMenu.xib Modifications made automatically by Xcode 11.3 when opening first making a change to the file. There is nothing meaningful. This has been separated from the real change that is coming in the next commit. * Make commit message view scale with font size Must call `setNeedsDisplay:` otherwise the 50 and 72 character markers are sometimes not updated (such as when there is selected text). The font size is 10% larger than the font size in user defaults, to match before where the font size here was 11 points compared to 10 in the diff view. * Add slider in Preferences to change font size Letting the user specify a scale but the application specify the typefaces and relative sizes seems like the best trade-off between allowing the user to see the app comfortably without asking them to be the designer. Also different views have use different fonts and relative sizes. Therefore, the user does not set an explicit typeface name or numerical size and instead a slider without value labels has been added to General Preferences to set the scale. This (deliberately) results in the exact size used hard to find. I decided to store the most obvious thing in user defaults: the font size used in the diff view in points. This simplicity comes at the cost of making the slider in Preferences more complicated as a linear slider would not be very good: the difference between 10 and 11 points is much more important than the difference between 18 and 19 points. A value transformer is used to map between the discrete slider value and a non-linear array of font sizes that match the preset sizes in the Mac system font picker. * Make diff view scale with font size The font size matches the font size from user defaults directly. The global variables associated with the diff view that have become properties on instances that update in `updateMetricsFromCurrentFontSize` when the user defaults change. Constants have been replaced by functions. The table view should always be reloaded when the font size changes to ensure the content size is correct. The threshold for switching from unified to split diff is now specified as a multiple of the font size. Similar scaling is used for drawing the column separators for line numbers and positioning text. This ensures the layout is the same as GitUp was designed except for being scaled.
1 parent ccd3640 commit 2e9b935

File tree

13 files changed

+400
-168
lines changed

13 files changed

+400
-168
lines changed

GitUp/Application/AppDelegate.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ + (void)initialize {
9393
GICommitMessageViewUserDefaultKey_ShowInvisibleCharacters : @(YES),
9494
GICommitMessageViewUserDefaultKey_ShowMargins : @(YES),
9595
GICommitMessageViewUserDefaultKey_EnableSpellChecking : @(YES),
96+
GIUserDefaultKey_FontSize : @(GIDefaultFontSize),
9697
kUserDefaultsKey_ReleaseChannel : kReleaseChannel_Stable,
9798
kUserDefaultsKey_CheckInterval : @(15 * 60),
9899
kUserDefaultsKey_FirstLaunch : @(YES),

GitUp/Application/Base.lproj/MainMenu.xib

Lines changed: 84 additions & 42 deletions
Large diffs are not rendered by default.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (C) 2015-2019 Pierre-Olivier Latour <[email protected]>
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#import <Foundation/Foundation.h>
17+
18+
/// Transforms between the linear slider in Preferences to a non-linear list of font sizes matching the system font picker.
19+
@interface FontSizeTransformer : NSValueTransformer
20+
21+
@end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright (C) 2015-2019 Pierre-Olivier Latour <[email protected]>
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
#import "FontSizeTransformer.h"
17+
#import <GitUpKit/GIAppKit.h>
18+
19+
static NSArray* sizes;
20+
21+
@implementation FontSizeTransformer
22+
23+
+ (void)initialize {
24+
// Match the system font picker
25+
sizes = @[ @9, @10, @11, @12, @13, @14, @18, @24 ];
26+
}
27+
28+
+ (Class)transformedValueClass {
29+
return [NSNumber class];
30+
}
31+
32+
+ (BOOL)allowsReverseTransformation {
33+
return YES;
34+
}
35+
36+
- (id)transformedValue:(id)fontSizeValue {
37+
NSUInteger idx = [sizes indexOfObject:fontSizeValue];
38+
if (idx == NSNotFound) {
39+
// If the user default is set externally, fallback to the default index.
40+
return @1;
41+
}
42+
43+
return @(idx);
44+
}
45+
46+
- (id)reverseTransformedValue:(id)indexValue {
47+
NSUInteger idx = [indexValue unsignedIntegerValue];
48+
if (idx >= sizes.count) {
49+
return @(GIDefaultFontSize);
50+
}
51+
52+
return sizes[idx];
53+
}
54+
55+
@end

GitUp/GitUp.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
0AE7F5F12312C1B000B06050 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0AE7F5ED2312C1B000B06050 /* InfoPlist.strings */; };
2020
165C32B61B95739700D2F894 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 165C32B51B95739700D2F894 /* QuartzCore.framework */; };
2121
31CD50E3203E2E2800360B3A /* ToolbarItemWrapperView.m in Sources */ = {isa = PBXBuildFile; fileRef = 31CD50E2203E2E2800360B3A /* ToolbarItemWrapperView.m */; };
22+
A53C6D0C1E61A9CF0070387E /* FontSizeTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = A53C6D0B1E61A9CF0070387E /* FontSizeTransformer.m */; };
2223
E212A6DE1B92100E00F62B18 /* libiconv.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = E212A6DD1B92100E00F62B18 /* libiconv.dylib */; };
2324
E21739F71A5080DD00EC6777 /* DocumentController.m in Sources */ = {isa = PBXBuildFile; fileRef = E21739F61A5080DD00EC6777 /* DocumentController.m */; };
2425
E21DCAEB1B253847006424E8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = E21DCAEA1B253847006424E8 /* main.m */; };
@@ -134,6 +135,8 @@
134135
165C32B51B95739700D2F894 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
135136
31CD50E1203E2E2800360B3A /* ToolbarItemWrapperView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ToolbarItemWrapperView.h; sourceTree = "<group>"; };
136137
31CD50E2203E2E2800360B3A /* ToolbarItemWrapperView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ToolbarItemWrapperView.m; sourceTree = "<group>"; };
138+
A53C6D0A1E61A9CF0070387E /* FontSizeTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FontSizeTransformer.h; sourceTree = "<group>"; };
139+
A53C6D0B1E61A9CF0070387E /* FontSizeTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FontSizeTransformer.m; sourceTree = "<group>"; };
137140
E212A6DD1B92100E00F62B18 /* libiconv.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libiconv.dylib; path = usr/lib/libiconv.dylib; sourceTree = SDKROOT; };
138141
E21739F51A5080DD00EC6777 /* DocumentController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DocumentController.h; sourceTree = "<group>"; };
139142
E21739F61A5080DD00EC6777 /* DocumentController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DocumentController.m; sourceTree = "<group>"; };
@@ -295,6 +298,8 @@
295298
0AE7F5ED2312C1B000B06050 /* InfoPlist.strings */,
296299
E2C338B319F8562F00063D95 /* main.m */,
297300
E2C338BD19F8562F00063D95 /* MainMenu.xib */,
301+
A53C6D0A1E61A9CF0070387E /* FontSizeTransformer.h */,
302+
A53C6D0B1E61A9CF0070387E /* FontSizeTransformer.m */,
298303
E2C5672B1A6D98BC00ECFE07 /* WindowController.h */,
299304
E2C5672C1A6D98BC00ECFE07 /* WindowController.m */,
300305
31CD50E1203E2E2800360B3A /* ToolbarItemWrapperView.h */,
@@ -471,6 +476,7 @@
471476
E2E3C9A01D771E7600AA9A62 /* GARawTracker.m in Sources */,
472477
E2C338B419F8562F00063D95 /* main.m in Sources */,
473478
E2C5672D1A6D98BC00ECFE07 /* WindowController.m in Sources */,
479+
A53C6D0C1E61A9CF0070387E /* FontSizeTransformer.m in Sources */,
474480
E2C338B219F8562F00063D95 /* AppDelegate.m in Sources */,
475481
0A58CD77237B4F4B00C2BDD0 /* CloneWindowController.m in Sources */,
476482
E2C338B719F8562F00063D95 /* Document.m in Sources */,

GitUpKit/Components/GIDiffContentsViewController.m

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
#import "GCRepository+Index.h"
2525
#import "XLFacilityMacros.h"
2626

27-
#define kMinSplitDiffViewWidth 1000
27+
// Units ems: a multiple of the font point size, so the width threshold is 100 * 10 = 1000 for a 10 point font.
28+
#define kMinSplitDiffViewWidthEms 100
2829

2930
#define kContextualMenuOffsetX 0
3031
#define kContextualMenuOffsetY -6
@@ -189,13 +190,15 @@ + (void)initialize {
189190
- (instancetype)initWithRepository:(GCLiveRepository*)repository {
190191
if ((self = [super initWithRepository:repository])) {
191192
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:GIDiffContentsViewControllerUserDefaultKey_DiffViewMode options:0 context:(__bridge void*)[GIDiffContentsViewController class]];
193+
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:GIUserDefaultKey_FontSize options:0 context:(__bridge void*)[GIDiffContentsViewController class]];
192194
}
193195
return self;
194196
}
195197

196198
- (void)dealloc {
197199
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:_tableView.superview];
198200

201+
[[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:GIUserDefaultKey_FontSize context:(__bridge void*)[GIDiffContentsViewController class]];
199202
[[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:GIDiffContentsViewControllerUserDefaultKey_DiffViewMode context:(__bridge void*)[GIDiffContentsViewController class]];
200203
}
201204

@@ -227,13 +230,13 @@ - (Class)_diffViewClassForChange:(GCFileDiffChange)change {
227230
if ((change == kGCFileDiffChange_Untracked) || (change == kGCFileDiffChange_Added) || (change == kGCFileDiffChange_Deleted)) {
228231
return [GIUnifiedDiffView class];
229232
}
230-
return self.view.bounds.size.width < kMinSplitDiffViewWidth ? [GIUnifiedDiffView class] : [GISplitDiffView class];
233+
return self.view.bounds.size.width < kMinSplitDiffViewWidthEms * GIFontSize() ? [GIUnifiedDiffView class] : [GISplitDiffView class];
231234
}
232235
return mode > 0 ? [GISplitDiffView class] : [GIUnifiedDiffView class];
233236
}
234237

235-
- (void)_updateDiffViews {
236-
BOOL reload = NO;
238+
- (void)_updateDiffViewsForcingReload:(BOOL)forceReload {
239+
BOOL reload = forceReload;
237240
for (GIDiffContentData* data in _data) {
238241
if (!data.diffView) {
239242
continue;
@@ -256,7 +259,7 @@ - (void)_updateDiffViews {
256259

257260
- (void)viewDidResize {
258261
if (self.viewVisible && !self.liveResizing) {
259-
[self _updateDiffViews];
262+
[self _updateDiffViewsForcingReload:NO];
260263
[NSAnimationContext beginGrouping];
261264
[[NSAnimationContext currentContext] setDuration:0.0]; // Prevent animations in case the view is actually not on screen yet (e.g. in a hidden tab)
262265
[_tableView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRowsInTableView:_tableView])]];
@@ -265,15 +268,16 @@ - (void)viewDidResize {
265268
}
266269

267270
- (void)viewDidFinishLiveResize {
268-
[self _updateDiffViews];
271+
[self _updateDiffViewsForcingReload:NO];
269272
[_tableView noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self numberOfRowsInTableView:_tableView])]];
270273
}
271274

272275
// WARNING: This is called *several* times when the default has been changed
273276
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
274277
if (context == (__bridge void*)[GIDiffContentsViewController class]) {
275-
if (self.viewVisible) {
276-
[self _updateDiffViews]; // This is idempotent
278+
BOOL isFontSizeChange = [keyPath isEqualToString:GIUserDefaultKey_FontSize];
279+
if (self.viewVisible || isFontSizeChange) {
280+
[self _updateDiffViewsForcingReload:isFontSizeChange]; // This is idempotent
277281
}
278282
} else {
279283
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];

GitUpKit/Interface/GIDiffView.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@
3232
@property(nonatomic, readonly, getter=isEmpty) BOOL empty;
3333
- (CGFloat)updateLayoutForWidth:(CGFloat)width;
3434

35+
@property(nonatomic, readonly) CFDictionaryRef textAttributes;
36+
@property(nonatomic, readonly) CTLineRef addedLine;
37+
@property(nonatomic, readonly) CTLineRef deletedLine;
38+
@property(nonatomic, readonly) CGFloat lineHeight;
39+
@property(nonatomic, readonly) CGFloat lineDescent;
40+
3541
@property(nonatomic, readonly) BOOL hasSelection;
3642
@property(nonatomic, readonly) BOOL hasSelectedText;
3743
@property(nonatomic, readonly) BOOL hasSelectedLines;

GitUpKit/Interface/GIDiffView.m

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,67 @@
1818
#endif
1919

2020
#import "GIPrivate.h"
21+
#import "GIAppKit.h"
2122

22-
#define kTextFontSize 10
2323
#define kTextLineHeightPadding 3
2424
#define kTextLineDescentAdjustment 1
2525

26-
CFDictionaryRef GIDiffViewAttributes = nil;
26+
const char* GIDiffViewMissingNewlinePlaceholder = "🚫\n";
2727

28-
CTLineRef GIDiffViewAddedLine = NULL;
29-
CTLineRef GIDiffViewDeletedLine = NULL;
28+
@interface GIDiffView ()
3029

31-
CGFloat GIDiffViewLineHeight = 0.0;
32-
CGFloat GIDiffViewLineDescent = 0.0;
30+
@property(nonatomic, assign) CGFloat lastFontSize;
3331

34-
const char* GIDiffViewMissingNewlinePlaceholder = "🚫\n";
32+
@end
3533

3634
@implementation GIDiffView
3735

38-
+ (void)initialize {
39-
GIDiffViewAttributes = CFBridgingRetain(@{(id)kCTFontAttributeName : [NSFont userFixedPitchFontOfSize:kTextFontSize], (id)kCTForegroundColorFromContextAttributeName : (id)kCFBooleanTrue});
36+
- (void)updateMetricsFromCurrentFontSize {
37+
CGFloat newSize = GIFontSize();
38+
// This comparison is safe because the values being compared are both read from user defaults with no additional floating point operations.
39+
#pragma clang diagnostic push
40+
#pragma clang diagnostic ignored "-Wfloat-equal"
41+
if (newSize == _lastFontSize) {
42+
#pragma clang diagnostic pop
43+
return;
44+
}
45+
_lastFontSize = newSize;
46+
47+
NSFont* font = [NSFont userFixedPitchFontOfSize:newSize];
48+
if (_textAttributes) CFRelease(_textAttributes);
49+
_textAttributes = CFBridgingRetain(@{(id)kCTFontAttributeName : font, (id)kCTForegroundColorFromContextAttributeName : (id)kCFBooleanTrue});
4050

41-
CFAttributedStringRef addedString = CFAttributedStringCreate(kCFAllocatorDefault, CFSTR("+"), GIDiffViewAttributes);
42-
GIDiffViewAddedLine = CTLineCreateWithAttributedString(addedString);
51+
CFAttributedStringRef addedString = CFAttributedStringCreate(kCFAllocatorDefault, CFSTR("+"), _textAttributes);
52+
if (_addedLine) CFRelease(_addedLine);
53+
_addedLine = CTLineCreateWithAttributedString(addedString);
4354
CFRelease(addedString);
4455

45-
CFAttributedStringRef deletedString = CFAttributedStringCreate(kCFAllocatorDefault, CFSTR("-"), GIDiffViewAttributes);
46-
GIDiffViewDeletedLine = CTLineCreateWithAttributedString(deletedString);
56+
CFAttributedStringRef deletedString = CFAttributedStringCreate(kCFAllocatorDefault, CFSTR("-"), _textAttributes);
57+
if (_deletedLine) CFRelease(_deletedLine);
58+
_deletedLine = CTLineCreateWithAttributedString(deletedString);
4759
CFRelease(deletedString);
4860

4961
CGFloat ascent;
5062
CGFloat descent;
5163
CGFloat leading;
52-
CTLineGetTypographicBounds(GIDiffViewAddedLine, &ascent, &descent, &leading);
53-
GIDiffViewLineHeight = ceilf(ascent + descent + leading) + kTextLineHeightPadding;
54-
GIDiffViewLineDescent = ceilf(descent) + kTextLineDescentAdjustment;
64+
CTLineGetTypographicBounds(_addedLine, &ascent, &descent, &leading);
65+
_lineHeight = ceilf(ascent + descent + leading) + kTextLineHeightPadding;
66+
_lineDescent = ceilf(descent) + kTextLineDescentAdjustment;
67+
68+
[self setNeedsDisplay:YES];
69+
}
70+
71+
// WARNING: This is called *several* times when the default has been changed
72+
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context {
73+
if (context == (__bridge void*)[GIDiffView class]) {
74+
if ([keyPath isEqualToString:GIUserDefaultKey_FontSize]) {
75+
[self updateMetricsFromCurrentFontSize];
76+
} else {
77+
XLOG_UNREACHABLE();
78+
}
79+
} else {
80+
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
81+
}
5582
}
5683

5784
- (void)_windowKeyDidChange:(NSNotification*)notification {
@@ -74,6 +101,7 @@ - (void)viewDidMoveToWindow {
74101

75102
- (void)didFinishInitializing {
76103
_backgroundColor = NSColor.textBackgroundColor;
104+
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:GIUserDefaultKey_FontSize options:NSKeyValueObservingOptionInitial context:(__bridge void*)[GIDiffView class]];
77105
}
78106

79107
- (instancetype)initWithFrame:(NSRect)frameRect {
@@ -93,6 +121,12 @@ - (instancetype)initWithCoder:(NSCoder*)coder {
93121
- (void)dealloc {
94122
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowDidResignKeyNotification object:nil];
95123
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowDidBecomeKeyNotification object:nil];
124+
125+
[[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:GIUserDefaultKey_FontSize context:(__bridge void*)[GIDiffView class]];
126+
127+
if (_textAttributes) CFRelease(_textAttributes);
128+
if (_addedLine) CFRelease(_addedLine);
129+
if (_deletedLine) CFRelease(_deletedLine);
96130
}
97131

98132
- (BOOL)isOpaque {

GitUpKit/Interface/GIPrivate.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@
2020

2121
#if __GI_HAS_APPKIT__
2222

23-
extern CFDictionaryRef GIDiffViewAttributes;
24-
25-
extern CTLineRef GIDiffViewAddedLine;
26-
extern CTLineRef GIDiffViewDeletedLine;
27-
28-
extern CGFloat GIDiffViewLineHeight;
29-
extern CGFloat GIDiffViewLineDescent;
30-
3123
extern const char* GIDiffViewMissingNewlinePlaceholder;
3224

3325
#endif

0 commit comments

Comments
 (0)