Skip to content

Commit

Permalink
Theme Manager In-Dev
Browse files Browse the repository at this point in the history
* In-development theme manager. It's getting close now.
  • Loading branch information
KhloeLeclair committed Nov 17, 2022
1 parent 122d6a2 commit cec9f09
Show file tree
Hide file tree
Showing 68 changed files with 2,336 additions and 1,568 deletions.
1 change: 1 addition & 0 deletions Common/Common.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)AdvancedMultipleMutexRequest.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Events\ConsoleCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)GlobalUsing.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\StackQuality\IStackQualityApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\StackQuality\SQIntegration.cs" />
Expand Down
52 changes: 52 additions & 0 deletions Common/CommonHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,58 @@ namespace Leclair.Stardew.Common;

public static class CommonHelper {

#region Equality

internal static bool ShallowEquals<TValue>(this TValue[]? input, TValue[]? other, IEqualityComparer<TValue>? comparer = null) {
if (input == other) return true;
if ((input == null) || (other == null)) return false;
if (input.Rank != other.Rank) return false;
if (input.LongLength != other.LongLength) return false;

comparer ??= EqualityComparer<TValue>.Default;

for(int i = 0; i < input.Length; i++) {
if (!comparer.Equals(input[i], other[i]))
return false;
}

return true;
}

internal static bool ShallowEquals<TValue>(this IList<TValue> input, IList<TValue> other, IEqualityComparer<TValue>? comparer = null) {
if (input == other) return true;
if ((input == null) || (other == null)) return false;
if (input.Count != other.Count) return false;

comparer ??= EqualityComparer<TValue>.Default;

for(int i = 0; i < input.Count; i++) {
if (!comparer.Equals(input[i], other[i]))
return false;
}

return true;
}

internal static bool ShallowEquals<TKey, TValue>(this Dictionary<TKey, TValue> input, Dictionary<TKey, TValue> other, IEqualityComparer<TValue>? comparer = null) where TKey : notnull {
if (input == other) return true;
if ((input == null) || (other == null)) return false;
if (input.Count != other.Count) return false;

comparer ??= EqualityComparer<TValue>.Default;

foreach(var entry in input) {
if (!other.TryGetValue(entry.Key, out TValue? value))
return false;
if (!comparer.Equals(entry.Value, value))
return false;
}

return true;
}

#endregion

#region Color Parsing

#region Color Names
Expand Down
31 changes: 31 additions & 0 deletions Common/EventHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

using Leclair.Stardew.Common.Events;

using Microsoft.Build.Utilities;

using StardewModdingAPI;

namespace Leclair.Stardew.Common;
Expand Down Expand Up @@ -63,6 +65,35 @@ private static void ScanObjectTypes(object obj, Dictionary<Type, Tuple<object, E
}
}

delegate void ConsoleCommandDelegate(string name, string[] args);

public static void RegisterConsoleCommands(object provider, ICommandHelper helper, Action<string, LogLevel>? logger) {
Type provtype = provider.GetType();

foreach(MethodInfo method in provtype.GetMethods(METHOD_FLAGS)) {
Attribute? attr = method.GetCustomAttribute(typeof(ConsoleCommand));
if (attr is not ConsoleCommand cmd)
continue;

ParameterInfo[] parms = method.GetParameters();
if (parms.Length != 2)
continue;

if (parms[0].ParameterType != typeof(string) || parms[1].ParameterType != typeof(string[]))
continue;

string name = string.IsNullOrWhiteSpace(cmd.Name) ? method.Name : cmd.Name;
string desc = string.IsNullOrWhiteSpace(cmd.Description) ? string.Empty : cmd.Description;

try {
ConsoleCommandDelegate del = method.CreateDelegate<ConsoleCommandDelegate>(provider);
helper.Add(name, desc, new Action<string, string[]>(del));
} catch (Exception ex) {
logger?.Invoke($"Failed to register console command {name}: {ex}", LogLevel.Error);
}
}
}


public static Dictionary<MethodInfo, RegisteredEvent> RegisterEvents(object subscriber, object eventBus, Dictionary<MethodInfo, RegisteredEvent>? existing, Action<string, LogLevel>? logger) {
Dictionary<Type, Tuple<object, EventInfo>?> typeToEvent = GetTypes(eventBus);
Expand Down
17 changes: 17 additions & 0 deletions Common/Events/ConsoleCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#nullable enable

using System;

namespace Leclair.Stardew.Common.Events;

[AttributeUsage(AttributeTargets.Method)]
public class ConsoleCommand : Attribute {

public string Name { get; }
public string? Description { get; }
public ConsoleCommand(string name, string? description = null) {
Name = name;
Description = description;
}

}
6 changes: 6 additions & 0 deletions Common/Events/ModSubscriber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using StardewModdingAPI;

using Leclair.Stardew.Common.Types;
using StardewModdingAPI.Events;

namespace Leclair.Stardew.Common.Events;

Expand All @@ -16,6 +17,7 @@ public abstract class ModSubscriber : Mod {

public override void Entry(IModHelper helper) {
RegisterEvents();
Helper.Events.GameLoop.GameLaunched += OnGameLaunched;
}

public virtual void Log(string message, LogLevel level = LogLevel.Debug, Exception? ex = null, LogLevel? exLevel = null) {
Expand All @@ -41,6 +43,10 @@ public void UnregisterEvents() {
Events = null;
}

private void OnGameLaunched(object? sender, GameLaunchedEventArgs e) {
EventHelper.RegisterConsoleCommands(this, Helper.ConsoleCommands, (msg, level) => Log(msg, level));
}

public void CheckRecommendedIntegrations() {
// Missing Integrations?
RecommendedIntegration[]? integrations;
Expand Down
2 changes: 1 addition & 1 deletion ThemeManager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## 0.1.0
Released on May 22nd, 2022.
Released on ???.

* Initial release.
82 changes: 40 additions & 42 deletions ThemeManager/IThemeManagerApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,10 @@ namespace Leclair.Stardew.ThemeManager;
/// </summary>
public interface IBaseTheme {

Color? TextColor { get; }

Color? TextShadowColor { get; }

Color? TextShadowAltColor { get; }

Color? ErrorTextColor { get; }

Color? HoverColor { get; }

Color? ButtonHoverColor { get; }
Dictionary<string, Color> Variables { get; }

Dictionary<int, Color> SpriteTextColors { get; }

}

/// <summary>
Expand All @@ -47,6 +38,16 @@ public interface IThemeChangedEvent<DataT> {
/// </summary>
string NewId { get; }

/// <summary>
/// The manifest of the previously active theme.
/// </summary>
IThemeManifest? OldManifest { get; }

/// <summary>
/// The manifest of the newly active theme.
/// </summary>
IThemeManifest? NewManifest { get; }

/// <summary>
/// The theme data of the previously active theme.
/// </summary>
Expand All @@ -56,13 +57,20 @@ public interface IThemeChangedEvent<DataT> {
/// The theme data of the newly active theme.
/// </summary>
DataT NewData { get; }
}

public interface IThemesDiscoveredEvent<DataT> {

IReadOnlyDictionary<string, IThemeManifest> Manifests { get; }

IReadOnlyDictionary<string, DataT> Data { get; }

}

/// <summary>
/// A manifest has necessary metadata for a theme for display in theme
/// selection UI, for performing automatic theme selection, and for
/// loading assets correctly from the filesystem.
/// loading assets correctly from the file system.
/// </summary>
public interface IThemeManifest {

Expand Down Expand Up @@ -409,6 +417,13 @@ void Discover(
/// </summary>
event EventHandler<IThemeChangedEvent<DataT>>? ThemeChanged;

/// <summary>
/// This event is fired whenever themes are discovered and theme data has
/// been loaded, but before theme selection runs. This can be used to
/// perform any extra processing of theme data.
/// </summary>
event EventHandler<IThemesDiscoveredEvent<DataT>>? ThemesDiscovered;

#endregion
}

Expand All @@ -432,10 +447,21 @@ public interface IThemeManagerApi {
/// rather than throwing an <see cref="InvalidCastException"/>.
/// </summary>
/// <typeparam name="DataT">The type for the mod's theme data.</typeparam>
/// <param name="modManifest">The mod's manifest.</param>
/// <param name="themeManager">The <see cref="ITypedThemeManager{DataT}"/>
/// instance, if one exists.</param>
bool TryGetManager<DataT>(IManifest modManifest, [NotNullWhen(true)] out ITypedThemeManager<DataT>? themeManager) where DataT : class, new();
/// <param name="forMod">An optional manifest to get the theme manager
/// for a specific mod.</param>
bool TryGetManager<DataT>([NotNullWhen(true)] out ITypedThemeManager<DataT>? themeManager, IManifest? forMod = null) where DataT : class, new();

/// <summary>
/// Try to get an existing <see cref="IThemeManager"/> instance for a mod.
/// This will never create a new instance.
/// </summary>
/// <param name="themeManager">The <see cref="IThemeManager"/>
/// instance, if one exists.</param>
/// <param name="forMod">An optional manifest to get the theme manager
/// for a specific mod.</param>
bool TryGetManager([NotNullWhen(true)] out IThemeManager? themeManager, IManifest? forMod = null);

/// <summary>
/// Get an <see cref="ITypedThemeManager{DataT}"/> for a mod. If there is no
Expand All @@ -444,7 +470,6 @@ public interface IThemeManagerApi {
/// If there is an existing instance, the parameters are ignored.
/// </summary>
/// <typeparam name="DataT">The type for the mod's theme data.</typeparam>
/// <param name="modManifest">The mod's manifest.</param>
/// <param name="defaultTheme">A <typeparamref name="DataT"/> instance to
/// use for the <c>default</c> theme. If one is not provided, a new
/// instance will be created.</param>
Expand All @@ -462,40 +487,13 @@ public interface IThemeManagerApi {
/// manager with a different <typeparamref name="DataT"/> than it was
/// created with.</exception>
ITypedThemeManager<DataT> GetOrCreateManager<DataT>(
IManifest modManifest,
DataT? defaultTheme = null,
string? embeddedThemesPath = "assets/themes",
string? assetPrefix = "assets",
string? assetLoaderPrefix = null,
bool? forceAssetRedirection = null
) where DataT : class, new();

/// <summary>
/// Manage a <typeparamref name="DataT"/> instance for a mod using a
/// <see cref="ITypedThemeManager{DataT}"/>. This uses a <c>ref</c> parameter
/// to replace the existing theme instance with a new one when the
/// theme is changed.
///
/// If you need to change any of the parameters used to create a theme
/// manager, you should first call <see cref="GetOrCreateManager{DataT}(IManifest, DataT?, string?, string?, string?, bool?)"/>
/// before using this method.
/// </summary>
/// <typeparam name="DataT">The type for the mod's theme data.</typeparam>
/// <param name="modManifest">The mod's manifest.</param>
/// <param name="theme">The default <typeparamref name="DataT"/> instance.
/// If the <see cref="ITypedThemeManager{DataT}"/> instance was not already
/// created, this will be used as the <c>default</c> theme's data.</param>
/// <param name="onThemeChanged">An optional action to be called whenever
/// the theme is changed or reloaded.</param>
/// <exception cref="InvalidCastException">Thrown when attempting to get a
/// manager with a different <typeparamref name="DataT"/> than it was
/// created with.</exception>
ITypedThemeManager<DataT> ManageTheme<DataT>(
IManifest modManifest,
ref DataT theme,
EventHandler<IThemeChangedEvent<DataT>>? onThemeChanged = null
) where DataT : class, new();

#endregion

#region Color Parsing
Expand Down
31 changes: 25 additions & 6 deletions ThemeManager/Manager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ public void Discover(
}
}

// Invoke the discovery event.
ThemesDiscovered?.Invoke(this, new ThemesDiscoveredEventArgs<DataT>(Themes));

// Store our currently selected theme.
string? oldKey = SelectedThemeId;

Expand Down Expand Up @@ -451,7 +454,7 @@ public string GetThemeName(string themeId, string? locale = null) {
return Mod.Helper.Translation.Get($"theme.default").ToString();

// Get the theme data. If the theme is the active theme, don't
// bother with a dictionary lookp.
// bother with a dictionary lookup.
Theme<DataT>? theme;
if (themeId == ActiveThemeId)
theme = BaseThemeData;
Expand Down Expand Up @@ -620,7 +623,13 @@ public void _SelectTheme(string? themeId, bool postReload = false) {
Invalidate(postReload ? null : old_active);

// And emit our event.
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs<DataT>(old_active, old_data?.Data, ActiveThemeId, Theme));
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs<DataT>(
old_active,
old_data?.Manifest,
old_data?.Data,
ActiveThemeId,
ActiveThemeManifest,
Theme));
}
}

Expand All @@ -636,7 +645,14 @@ public DataT DefaultTheme {
DataT? oldData = Theme;
_DefaultTheme = value ?? new DataT();
if (is_default)
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs<DataT>("default", oldData, "default", _DefaultTheme));
ThemeChanged?.Invoke(this, new ThemeChangedEventArgs<DataT>(
"default",
null,
oldData,
"default",
null,
_DefaultTheme
));
}
}

Expand All @@ -655,6 +671,9 @@ public DataT DefaultTheme {
/// <inheritdoc />
public event EventHandler<IThemeChangedEvent<DataT>>? ThemeChanged;

/// <inheritdoc />
public event EventHandler<IThemesDiscoveredEvent<DataT>>? ThemesDiscovered;

#endregion

#region Asset Loading
Expand All @@ -681,7 +700,7 @@ public T Load<T>(string path, string? themeId = null) where T : notnull {

// Does this theme have this file?
if (theme is not null && !HasFile(path, themeId, false, false)) {
// If not, does the fallback theme have it? If so, then load it.
// If not, does the fall back theme have it? If so, then load it.
if (!string.IsNullOrEmpty(theme.Manifest.FallbackTheme) && HasFile(path, theme.Manifest.FallbackTheme, false, false))
return Load<T>(path, theme.Manifest.FallbackTheme);
}
Expand Down Expand Up @@ -732,7 +751,7 @@ public bool HasFile(string path, string? themeId = null, bool useFallback = true
if (theme.Content.HasFile(lpath))
return true;

// Only fall-back once when using a fallback theme.
// Only fall-back once when using a fall back theme.
if (useFallback && !string.IsNullOrEmpty(theme.Manifest.FallbackTheme) && HasFile(path, theme.Manifest.FallbackTheme, false, false))
return true;
}
Expand Down Expand Up @@ -775,7 +794,7 @@ private T InternalLoad<T>(string path, string? themeId = null) where T : notnull
}
}

// Now fallback to the default theme
// Now fall back to the default theme
if (!string.IsNullOrEmpty(DefaultAssetPrefix))
path = Path.Join(DefaultAssetPrefix, path);

Expand Down
Loading

0 comments on commit cec9f09

Please sign in to comment.