Skip to content
This repository has been archived by the owner on May 8, 2024. It is now read-only.

Commit

Permalink
Patch support for symbol overrides
Browse files Browse the repository at this point in the history
What a mess. Not sure it works in call cases, but good start
  • Loading branch information
rodionovd committed May 7, 2024
1 parent 0318e21 commit 10fb9e7
Showing 1 changed file with 77 additions and 24 deletions.
101 changes: 77 additions & 24 deletions LippyPatcher.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,42 @@

@implementation LippyPatcher

// FIXME: <dmitry.rodionov> Lippy won't hold a strong reference to its activeTextOverride, so here we are, keeping it alive just like this
static NSArray *kSelectedTextOverrides = nil;

static NSValue *MSLayerAbsoluteRect(id /* __kindof MSLayer */ layer) {
id parent = [layer parentObject];
NSRect frame = [[layer valueForKeyPath:@"frame.rect"] rectValue];
NSRect absoluteFrame = [parent convertRect:frame toCoordinateSpace:nil];
return [NSValue valueWithPoint:absoluteFrame.origin];
};

static NSArray *MSSymbolInstanceSelectedTextOverrides(id /* MSSymbolInstance */ instance) {
// We filter for the currently selected text overrides on the given symbol instance
NSSet *selection = [[instance valueForKeyPath:@"documentData.currentPage.currentSelection"]
filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id item, id _Nullable bindings) {
return [item valueForKey:@"primarySelection"] == instance && [item canOverrideTextStringValue];
}]];
NSMutableArray *result = [NSMutableArray arrayWithCapacity:selection.count];
for (id item in selection) {
// FIXME: <dmitry.rodionov> this should've been a proper object, but who cares as long as the interface is the same
[result addObject:@{
@"selectionID": @"stringValue",
@"availableOverride": @{
@"overrideValue": [item valueForKeyPath:@"selectedLayer.stringValue"] ?: @"",
@"overridePoint": [item textStringOverridePoint],
@"affectedLayer": @{
@"rect": ({
NSRect absoluteRect = [[item valueForKey:@"absoluteAlignmentRect"] rectValue];
@([instance convertRect:absoluteRect fromCoordinateSpace:nil]);
})
}
}
}];
}
return result;
};

+ (void)load
{
NSBundle *originalLippyBundle = ^NSBundle *(void) {
Expand All @@ -26,26 +62,37 @@ + (void)load
return;
}

// See kSelectedOverrides definition at the top of this file
Class LippyMainViewController = NSClassFromString(@"LippyMainViewController");
SEL showEditorForContextSel = NSSelectorFromString(@"showEditorForContext:");
[LippyMainViewController aspect_hookSelector:showEditorForContextSel withOptions:AspectPositionBefore usingBlock:^() {
kSelectedTextOverrides = nil;
} error:nil];

Class MSDocument = NSClassFromString(@"MSDocument");
NSCAssert(MSDocument != nil, @"");
[MSDocument aspect_hookSelector:@selector(valueForKeyPath:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo, NSString *keyPath) {
NSInvocation *invocation = aspectInfo.originalInvocation;
// Replace the key path in [MSDocument valueForKeyPath:@"selectedLayers.layers"] to just "selectedLayers"
// -[MSDocument selectedLayers] no longer contains symbol instances for which one of the overrides
// is selected, so we use a different API (which also works for plain selection)
if ([keyPath isEqualToString:@"selectedLayers.layers"]) {
NSString *patchedKeyPath = @"selectedLayers";
[invocation setArgument:&patchedKeyPath atIndex:2];
NSArray *primarySelection = [[aspectInfo.instance valueForKeyPath:@"documentData.currentPage.currentSelection.primarySelection"] allObjects];
void *result = (__bridge void *)primarySelection;
[invocation setReturnValue:&result];
return;
}
// Lippy's Sketch version number parser has a common flaw: it doesn't support 3-digit major versions,
// which makes it believe it's running on e.g. Sketch 10.0 instead of 100, which in turn makes it
// choose a wrong API to use in this case. We fix it back to the correct one
// TODO: <dmitry.rodionov> patch -[Lippy sketchVersion] directly to return a correct value?
if ([keyPath isEqualToString:@"currentContentViewController.contentDrawView"]) {
NSString *patchedKeyPath = @"currentContentViewController.canvasView";
[invocation setArgument:&patchedKeyPath atIndex:2];
}
[invocation invoke];
} error:nil];

// Fix +[Lippy colorFromHex:] to use modern Sketch API
// Fix +[Lippy colorFromHex:] to use an up-to-date Sketch API
Class LippyParentClass = object_getClass(NSClassFromString(@"Lippy"));
NSCAssert(LippyParentClass != nil, @"");
SEL colorFromHexSel = NSSelectorFromString(@"colorFromHex:");
Expand All @@ -57,34 +104,36 @@ + (void)load
[invocation setReturnValue:&result];
} error:nil];

// Fix -valueForKeyPath:@"absoluteRect.origin" in various classes
__auto_type absoluteRectFixup = ^(id<AspectInfo> aspectInfo, NSString *keyPath) {
// Fix absoluteRect calculation for MSTextLayers
Class MSTextLayer = NSClassFromString(@"MSTextLayer");
[MSTextLayer aspect_hookSelector:@selector(valueForKeyPath:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo, NSString *keyPath) {
NSInvocation *invocation = aspectInfo.originalInvocation;
[invocation invoke];
if ([keyPath isEqualToString:@"absoluteRect.origin"]) {
id parent = [aspectInfo.instance parentObject];
NSRect frame = [[aspectInfo.instance valueForKeyPath:@"frame.rect"] rectValue];
NSRect absoluteFrame = [parent convertRect:frame toCoordinateSpace:nil];

NSValue *result = [NSValue valueWithPoint:absoluteFrame.origin];
NSValue *result = MSLayerAbsoluteRect(aspectInfo.instance);
[invocation setReturnValue:&result];
}
};

// MARK: - Text Layers
Class MSTextLayer = NSClassFromString(@"MSTextLayer");
[MSTextLayer aspect_hookSelector:@selector(valueForKeyPath:) withOptions:AspectPositionInstead usingBlock:absoluteRectFixup error:nil];
} error:nil];

// MARK: - Symbol Instances (WIP)
Class MSSymbolInstance = NSClassFromString(@"MSSymbolInstance");
[MSSymbolInstance aspect_hookSelector:@selector(valueForKeyPath:) withOptions:AspectPositionInstead usingBlock:absoluteRectFixup error:nil];

// TODO: <dmitry.rodionov> fix [MSSymbolInstance valueForKeyPath:@"overrideContainer.selectedOverrides"]
// -> an array of MSOverrideRepresentation
// TODO: <dmitry.rodionov> fix [<text override class> valueForKeyPath:@"availableOverride.affectedLayer.rect"];
// TODO: <dmitry.rodionov> fix [<text override class> valueForKeyPath:@"availableOverride.overrideValue"];
[MSSymbolInstance aspect_hookSelector:@selector(valueForKeyPath:) withOptions:AspectPositionInstead usingBlock:^(id<AspectInfo> aspectInfo, NSString *keyPath) {
NSInvocation *invocation = aspectInfo.originalInvocation;
[invocation invoke];

// to get an override point => [activeTextOverride valueForKeyPath:@"availableOverride.overridePoint"];
// Fix absoluteRect calculation for MSSymbolInstances
if ([keyPath isEqualToString:@"absoluteRect.origin"]) {
NSValue *result = MSLayerAbsoluteRect(aspectInfo.instance);
[invocation setReturnValue:&result];
}
// Prepare an artificial array of selected overrides on the given symbol instance
// in a format expected by Lippy's routines
if ([keyPath isEqualToString:@"overrideContainer.selectedOverrides"]) {
// See kSelectedOverrides definition at the top of this file
kSelectedTextOverrides = MSSymbolInstanceSelectedTextOverrides(aspectInfo.instance);
void *result = (__bridge void *)kSelectedTextOverrides;
[invocation setReturnValue:&result];
}
} error:nil];
}


Expand All @@ -95,5 +144,9 @@ + (id)colorWithHex:(NSString *)hex alpha:(CGFloat)alpha { return nil; }
- (id)NSColorWithColorSpace:(CGColorSpaceRef)colorSpace { return nil; }
- (id)parentObject { return nil; }
- (NSRect)convertRect:(NSRect)rect toCoordinateSpace:(id)space { return NSZeroRect; }
- (NSRect)convertRect:(NSRect)rect fromCoordinateSpace:(id)space { return NSZeroRect; }
- (NSSet *)primarySelection { return nil; }
- (BOOL)canOverrideTextStringValue { return NO; }
- (id)textStringOverridePoint { return nil; }

@end

0 comments on commit 10fb9e7

Please sign in to comment.