From e7d5afe90e85d01c4c460a8eec0e175eee8d28de Mon Sep 17 00:00:00 2001 From: Didier Korthoudt Date: Mon, 6 Apr 2020 13:10:34 +0200 Subject: [PATCH] New: Added dynamic attributes set for CSS based themes (#2701) --- AppKit/CPMenu/_CPMenuWindow.j | 32 ++++ AppKit/CPMenuItem/CPMenuItem.j | 14 ++ AppKit/CPMenuItem/_CPMenuItemStandardView.j | 16 ++ AppKit/CPMenuItem/_CPMenuItemView.j | 25 +++ AppKit/CPTheme.j | 175 ++++++++++++++++++++ AppKit/CPThemeBlend.j | 2 +- AppKit/CPView.j | 1 + AppKit/CPWindow/CPWindow.j | 13 ++ 8 files changed, 277 insertions(+), 1 deletion(-) diff --git a/AppKit/CPMenu/_CPMenuWindow.j b/AppKit/CPMenu/_CPMenuWindow.j index 681ee5af6..91672280f 100644 --- a/AppKit/CPMenu/_CPMenuWindow.j +++ b/AppKit/CPMenu/_CPMenuWindow.j @@ -26,6 +26,7 @@ @import "_CPMenuManager.j" @class _CPMenuView +@class CPMenuItem var _CPMenuWindowPool = [], _CPMenuWindowPoolCapacity = 5, @@ -419,6 +420,21 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2; @end +#pragma mark - + +@implementation _CPMenuWindow (CSSTheming) + +- (void)_setThemeIncludingDescendants:(CPTheme)aTheme +{ + [[self contentView] _setThemeIncludingDescendants:aTheme]; + [_menuView _setThemeIncludingDescendants:aTheme]; + [_menuView tile]; +} + +@end + +#pragma mark - + /* @ignore */ @@ -552,6 +568,7 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2; [view setFrameOrigin:CGPointMake(0.0, y)]; + [view _setThemeIncludingDescendants:[CPTheme defaultTheme]]; [self addSubview:view]; var size = [view minSize], @@ -582,3 +599,18 @@ _CPMenuWindowAttachedMenuBackgroundStyle = 2; } @end + +#pragma mark - + +@implementation _CPMenuView (CSSTheming) + +- (void)_setThemeIncludingDescendants:(CPTheme)aTheme +{ + [self setTheme:aTheme]; + [_menuItemViews makeObjectsPerformSelector:@selector(_setThemeIncludingDescendants:) withObject:aTheme]; + + for (var i = 0, items = [[self menu] itemArray], count = [items count]; i < count; i++) + [[items[i] _menuItemView] _setThemeIncludingDescendants:aTheme]; +} + +@end diff --git a/AppKit/CPMenuItem/CPMenuItem.j b/AppKit/CPMenuItem/CPMenuItem.j index 9241938b0..a7a54b511 100644 --- a/AppKit/CPMenuItem/CPMenuItem.j +++ b/AppKit/CPMenuItem/CPMenuItem.j @@ -897,6 +897,20 @@ CPControlKeyMask @end +#pragma mark - + +@implementation CPMenuItem (CSSTheming) + +- (void)_setThemeIncludingDescendants:(CPTheme)aTheme +{ + [_view _setThemeIncludingDescendants:aTheme]; + [_menuItemView _setThemeIncludingDescendants:aTheme]; +} + +@end + +#pragma mark - + var CPMenuItemIsSeparatorKey = @"CPMenuItemIsSeparatorKey", CPMenuItemTitleKey = @"CPMenuItemTitleKey", diff --git a/AppKit/CPMenuItem/_CPMenuItemStandardView.j b/AppKit/CPMenuItem/_CPMenuItemStandardView.j index 5a15a88d2..76453ac4a 100644 --- a/AppKit/CPMenuItem/_CPMenuItemStandardView.j +++ b/AppKit/CPMenuItem/_CPMenuItemStandardView.j @@ -352,6 +352,22 @@ @end +#pragma mark - + +@implementation _CPMenuItemStandardView (CSSTheming) + +#pragma mark Override + +- (void)_setThemeIncludingDescendants:(CPTheme)aTheme +{ + [self setTheme:aTheme]; + [[self subviews] makeObjectsPerformSelector:@selector(_setThemeIncludingDescendants:) withObject:aTheme]; +} + +@end + +#pragma mark - + @implementation _CPMenuItemSubmenuIndicatorView : CPView { CPColor _color; diff --git a/AppKit/CPMenuItem/_CPMenuItemView.j b/AppKit/CPMenuItem/_CPMenuItemView.j index 8d6b8b2cf..25ec9e878 100644 --- a/AppKit/CPMenuItem/_CPMenuItemView.j +++ b/AppKit/CPMenuItem/_CPMenuItemView.j @@ -252,6 +252,31 @@ @end +#pragma mark - + +@implementation _CPMenuItemView (CSSTheming) + +#pragma mark Override + +- (void)_setThemeIncludingDescendants:(CPTheme)aTheme +{ + [self setTheme:aTheme]; + [_view _setThemeIncludingDescendants:aTheme]; + + // Items must also perform this (without this, only the selected item does) + + [_imageAndTextView _setThemeIncludingDescendants:aTheme]; + [_submenuView _setThemeIncludingDescendants:aTheme]; + [[_menuItem view] _setThemeIncludingDescendants:aTheme]; + + if ([_view respondsToSelector:@selector(update)]) + [_view update]; +} + +@end + +#pragma mark - + @implementation _CPMenuItemArrowView : CPView { CPColor _color; diff --git a/AppKit/CPTheme.j b/AppKit/CPTheme.j index 261a74130..f0e66815f 100644 --- a/AppKit/CPTheme.j +++ b/AppKit/CPTheme.j @@ -30,6 +30,7 @@ @class _CPThemeAttribute @class CPImage @class CPColor +@class CPApplication var CPThemesByName = { }, CPThemeDefaultTheme = nil, @@ -339,9 +340,143 @@ var CPThemeNameKey = @"CPThemeNameKey", // // The method -(void)setCSSResourcesPath is meant to be used only by CPThemeBlend during theme loading in order to // replace the special path "%%" in CSS components (like in url(%%packed.png) ) with the path to the theme blend resources folder. +// +// DYNAMIC ATTRIBUTES SET: +// +// If the default theme is CSS based and exposes some dynamic attributes (like Aristo3 does for color schema), you can switch "live" +// to another set of values by calling [CPTheme updateDefaultThemeDynamicAttributesSetWithSet:aDynamicSet] where aDynamicSet is a +// dictionary with keys being the names of exposed dynamic attributes (see the themeDescriptor.j of the CSS based theme you use) +// and values, accordingly to the type of the corresponding default values provided by the theme (CPColor, a CSS color string, ...). +// You can provide key-value pairs for a subset of the exposed dynamic attributes. If no key-value pair is specified for a dynamic +// attribute, the theme default value will be used. +// +// To restore default theme values, simply pass nil as dynamic set : [CPTheme updateDefaultThemeDynamicAttributesSetWithSet:nil] +// +// During theme loading, if your application responds to -(CPDictionary)themeDynamicAttributesSet, the returned values set is directly +// applied to the default set. This avoids displaying the theme with its default values and then redisplaying it with your values. +// The -(CPDictionary)themeDynamicAttributesSet must be declared in a CPApplication category (and not in your application delegate) as +// it will be called during application initialisation, before the delegate has been defined. +// +// To use default theme values, simply return nil. +// +// +// TYPICAL USAGE: +// +// One can imagine that a user can choose between some predefined color schemas. You can store this user preference in standard user defaults: +// +// [[CPUserDefaults standardUserDefaults] setObject:ConstantRepresentingAColorSchema forKey:@"userTheme"]; +// [[CPUserDefaults standardUserDefaults] synchronize]; +// +// As you want to use the choosen color schema at run time, you've implemented -(CPDictionary)themeDynamicAttributesSet (see below), so updating +// the UI as soon as the user has made his choice is easy : +// +// [CPTheme updateDefaultThemeDynamicAttributesSetWithSet:[[CPApplication sharedApplication] themeDynamicAttributesSet]]; +// +// If the user chooses the theme default color schema, you can then simply remove the user default: +// +// [[CPUserDefaults standardUserDefaults] removeObjectForKey:@"userTheme"]; +// [[CPUserDefaults standardUserDefaults] synchronize]; +// +// Your CPApplication category could then be something like: +// +// @implementation CPApplication (themeColorSchema) +// +// - (CPDictionary)themeDynamicAttributesSet +// { +// var userTheme = [[CPUserDefaults standardUserDefaults] objectForKey:@"userTheme"]; +// +// if (!userTheme) + +// // Return nil to have the theme default color schema +// return nil; +// +// switch (userTheme) +// { +// case ConstantRepresentingAColorSchema: +// return @{ +// @"ExposedDynamicAttribute1": [CPColor colorWithRed:245.0/255.0 green:164.0/255.0 blue:9.0/255.0 alpha:0.85], +// @"ExposedDynamicAttribute2": [CPColor colorWithRed:245.0/255.0 green:164.0/255.0 blue:9.0/255.0 alpha:0.85], +// @"ExposedDynamicAttribute3": @"rgb(245,164,9)", +// @"ExposedDynamicAttribute4": @"url(./Resources/mypacked.png)" +// }; +// break; +// +// case AnotherConstantRepresentingAColorSchema: +// return @{ +// @"ExposedDynamicAttribute1": [CPColor colorWithRed:39.0/255.0 green:216.0/255.0 blue:93.0/255.0 alpha:0.85], +// @"ExposedDynamicAttribute3": @"rgb(39,216,93)" +// }; +// break; +// +// ... +// } +// } +// +// @end + +var _savedThemesByName = { }; @implementation CPTheme (CSSTheming) +- (void)_initializeDynamicAttributesAndResourcesPathWithBundle:(CPBundle)aBundle +{ + // First, save a template of the theme so dynamic attributes and images placeholders are preserved + _savedThemesByName[_name] = [CPKeyedArchiver archivedDataWithRootObject:self]; + + // Then, compute the dynamic attributes set that will be applied to the theme + // Does the application delegate provide a dynamic attributes set ? + var dynamicSet = ([[CPApplication sharedApplication] respondsToSelector:@selector(themeDynamicAttributesSet)]) ? [[CPApplication sharedApplication] themeDynamicAttributesSet] : nil; + + // Take the one defined in the theme + var defaultDynamicSet = [self valueForAttributeWithName:@"dynamic-set" forClass:[CPView class]]; + + // Update the default dynamic attributes set with the one provided by the application + [dynamicSet enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop) + { + [defaultDynamicSet setObject:anObject forKey:aKey]; + }]; + + // Apply the resulting set + [self applyDynamicSet:defaultDynamicSet]; + + // Then fix resources path in CSS directives + [self setCSSResourcesPath:[aBundle resourcePath]]; +} + ++ (void)updateDefaultThemeDynamicAttributesSetWithSet:(CPDictionary)dynamicSet +{ + // First, get a copy of the theme template + var newTheme = [CPKeyedUnarchiver unarchiveObjectWithData:_savedThemesByName[[CPThemeDefaultTheme name]]]; + + // Take the dynamic set defined in the theme + var defaultDynamicSet = [newTheme valueForAttributeWithName:@"dynamic-set" forClass:[CPView class]]; + + // Update the default dynamic attributes set with the one provided by the application + [dynamicSet enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop) + { + [defaultDynamicSet setObject:anObject forKey:aKey]; + }]; + + // Apply the resulting set to the theme + [newTheme applyDynamicSet:defaultDynamicSet]; + + // Then, fix resources path in CSS directives + [newTheme setCSSResourcesPath:[[[[CPApplication sharedApplication] themeBlend] bundle] resourcePath]]; + + // Now apply the new theme variation + [CPTheme setDefaultTheme:newTheme]; + + var windows = [[CPApplication sharedApplication] windows]; + + for (var i = 0, count = windows.length; i < count; i++) + { + [windows[i] _setThemeIncludingDescendants:newTheme]; + } + + // Update the menu bar + [CPMenu updateMenuBarAttributesWithTheme:newTheme]; +} + - (void)setCSSResourcesPath:(CPString)pathToResources { [_attributes enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop) @@ -372,6 +507,46 @@ var CPThemeNameKey = @"CPThemeNameKey", }]; } +- (void)applyDynamicSet:(CPDictionary)dynamicSet +{ + [_attributes enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop) + { + [anObject enumerateKeysAndObjectsUsingBlock:function(aKey2, anObject2, stop2) + { + var anObject2Values = [anObject2 values]; + + [anObject2Values enumerateKeysAndObjectsUsingBlock:function(aKey3, anObject3, stop3) + { + if (anObject3.isa) + { + if (([anObject3 isKindOfClass:CPImage] || [anObject3 isKindOfClass:CPColor]) && [anObject3 cssDictionary]) + { + // We have a CSS defined image or color + [self _applyDynamicSet:dynamicSet toCSSDictionary:[anObject3 cssDictionary]]; + [self _applyDynamicSet:dynamicSet toCSSDictionary:[anObject3 cssBeforeDictionary]]; + [self _applyDynamicSet:dynamicSet toCSSDictionary:[anObject3 cssAfterDictionary]]; + } + else if ([anObject3 isKindOfClass:CPString]) + { + // We have a direct string value + if ([dynamicSet containsKey:anObject3]) + [anObject2Values setObject:[dynamicSet objectForKey:anObject3] forKey:aKey3]; + } + } + }]; + }]; + }]; +} + +- (void)_applyDynamicSet:(CPDictionary)dynamicSet toCSSDictionary:(CPDictionary)aDictionary +{ + [aDictionary enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop) + { + if ([dynamicSet containsKey:anObject]) + [aDictionary setObject:[dynamicSet objectForKey:anObject] forKey:aKey]; + }]; +} + - (BOOL)isCSSBased { return !![self valueForAttributeWithName:@"css-based" forClass:[CPView class]]; diff --git a/AppKit/CPThemeBlend.j b/AppKit/CPThemeBlend.j index da3e33f1c..6c6fb02b5 100644 --- a/AppKit/CPThemeBlend.j +++ b/AppKit/CPThemeBlend.j @@ -103,7 +103,7 @@ aTheme = [CPTheme themeNamed:aThemeName]; if ([aTheme isCSSBased]) - [aTheme setCSSResourcesPath:[aBundle resourcePath]]; + [aTheme _initializeDynamicAttributesAndResourcesPathWithBundle:aBundle]; } [_loadDelegate blendDidFinishLoading:self]; diff --git a/AppKit/CPView.j b/AppKit/CPView.j index e0e637e17..09e9ae85c 100644 --- a/AppKit/CPView.j +++ b/AppKit/CPView.j @@ -333,6 +333,7 @@ var CPViewHighDPIDrawingEnabled = YES; { return @{ @"css-based": NO, + @"dynamic-set": [CPNull null], @"nib2cib-adjustment-frame": CGRectMakeZero() }; } diff --git a/AppKit/CPWindow/CPWindow.j b/AppKit/CPWindow/CPWindow.j index acb0028e8..3f1c742d6 100644 --- a/AppKit/CPWindow/CPWindow.j +++ b/AppKit/CPWindow/CPWindow.j @@ -4389,6 +4389,19 @@ var interpolate = function(fromValue, toValue, progress) @end +#pragma mark - + +@implementation CPWindow (CSSTheming) + +- (void)_setThemeIncludingDescendants:(CPTheme)aTheme +{ + [[self contentView] _setThemeIncludingDescendants:aTheme]; +} + +@end + +#pragma mark - + function _CPWindowFullPlatformWindowSessionMake(aWindowView, aContentRect, hasShadow, aLevel) { return { windowView:aWindowView, contentRect:aContentRect, hasShadow:hasShadow, level:aLevel };