Skip to content

Commit

Permalink
Game-specific labels
Browse files Browse the repository at this point in the history
Track labels' module identifiers per game
Create JsonToGamesDictionaryConverter
  • Loading branch information
HebaruSan committed Aug 20, 2023
1 parent f6d439f commit 3bb37cf
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 40 deletions.
103 changes: 103 additions & 0 deletions Core/Converters/JsonToGamesDictionaryConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Linq;
using System.Collections;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace CKAN
{
/// <summary>
/// A property converter for making an old property game-specific.
/// Turns a String or Array value:
///
/// "myProperty": "a value",
/// "myOtherProperty": [ "another value" ],
///
/// into a Dictionary with the game names as keys and the original
/// value as each value:
///
/// "myProperty": {
/// "KSP": "a value",
/// "KSP2": "a value"
/// },
/// "myOtherProperty": {
/// "KSP": [ "another value" ],
/// "KSP2": [ "another value" ]
/// },
///
/// NOTE: Do NOT use with Object values because they can't
/// be distinguished from an already converted value, and will
/// just be deserialized as-is into your Dictionary!
///
/// If the value is an empty array:
///
/// "myProperty": [],
///
/// the Dictionary is left empty rather than creating multiple keys
/// with empty values:
///
/// "myProperty": {},
/// </summary>
public class JsonToGamesDictionaryConverter : JsonConverter
{
/// <summary>
/// Turn a tree of JSON tokens into a dictionary
/// </summary>
/// <param name="reader">Object that provides tokens to be translated</param>
/// <param name="objectType">The output type to be populated</param>
/// <param name="existingValue">Not used</param>
/// <param name="serializer">Generates output objects from tokens</param>
/// <returns>Dictionary of type matching the property where this converter was used, containing game-specific keys and values</returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.Load(reader);
if (token.Type == JTokenType.Object)
{
return token.ToObject(objectType);
}
var valueType = objectType.GetGenericArguments()[1];
var obj = (IDictionary)Activator.CreateInstance(objectType);
if (!IsTokenEmpty(token))
{
foreach (var gameName in GameInstanceManager.AllGameShortNames())
{
// Make a new copy of the value for each game
obj.Add(gameName, token.ToObject(valueType));
}
}
return obj;
}

/// <summary>
/// We don't want to make any changes during serialization
/// </summary>
public override bool CanWrite => false;

/// <summary>
/// We don't want to make any changes during serialization
/// </summary>
/// <param name="writer">The object writing JSON to disk</param>
/// <param name="value">A value to be written for this class</param>
/// <param name="serializer">Generates output objects from tokens</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}

/// <summary>
/// We *only* want to be triggered for types that have explicitly
/// set an attribute in their class saying they can be converted.
/// By returning false here, we declare we're not interested in participating
/// in any other conversions.
/// </summary>
/// <returns>
/// false
/// </returns>
public override bool CanConvert(Type object_type) => false;

private static bool IsTokenEmpty(JToken token)
=> token.Type == JTokenType.Null
|| (token.Type == JTokenType.Array && !token.HasValues);
}
}
7 changes: 7 additions & 0 deletions Core/GameInstanceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -649,5 +649,12 @@ public IGame DetermineGame(DirectoryInfo path, IUser user)
public static IGame GameByShortName(string shortName)
=> knownGames.FirstOrDefault(g => g.ShortName == shortName);

/// <summary>
/// Return the short names of all known games
/// </summary>
/// <returns>Sequence of short name strings</returns>
public static IEnumerable<string> AllGameShortNames()
=> knownGames.Select(g => g.ShortName);

}
}
3 changes: 2 additions & 1 deletion GUI/Controls/Changeset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ private ListViewItem makeItem(ModChange change, Dictionary<CkanModule, string> c
{
var descr = change.Description;
CkanModule m = change.Mod;
ModuleLabel warnLbl = alertLabels?.FirstOrDefault(l => l.ModuleIdentifiers.Contains(m.identifier));
ModuleLabel warnLbl = alertLabels?.FirstOrDefault(l =>
l.ContainsModule(Main.Instance.CurrentInstance.game, m.identifier));
return new ListViewItem(new string[]
{
change.NameAndStatus,
Expand Down
22 changes: 13 additions & 9 deletions GUI/Controls/ManageMods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ private void ConflictsUpdated(Dictionary<GUIMod, string> prevConflicts)
{
// Mark old conflicts as non-conflicted
// (rows that are _still_ conflicted will be marked as such in the next loop)
var inst = Main.Instance.CurrentInstance;
foreach (GUIMod guiMod in prevConflicts.Keys)
{
DataGridViewRow row = mainModList.full_list_of_mod_rows[guiMod.Identifier];
Expand All @@ -174,7 +175,7 @@ private void ConflictsUpdated(Dictionary<GUIMod, string> prevConflicts)
{
cell.ToolTipText = null;
}
mainModList.ReapplyLabels(guiMod, false, Main.Instance.CurrentInstance.Name);
mainModList.ReapplyLabels(guiMod, false, inst.Name, inst.game);
if (row.Visible)
{
ModGrid.InvalidateRow(row.Index);
Expand Down Expand Up @@ -247,7 +248,7 @@ private void FilterLabelsToolButton_DropDown_Opening(object sender, CancelEventA
foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(Main.Instance.CurrentInstance.Name))
{
FilterLabelsToolButton.DropDownItems.Add(new ToolStripMenuItem(
$"{mlbl.Name} ({mlbl.ModuleIdentifiers.Count})",
$"{mlbl.Name} ({mlbl.ModuleCount(Main.Instance.CurrentInstance.game)})",
null, customFilterButton_Click
)
{
Expand All @@ -271,7 +272,7 @@ private void LabelsContextMenuStrip_Opening(object sender, CancelEventArgs e)
LabelsContextMenuStrip.Items.Add(
new ToolStripMenuItem(mlbl.Name, null, labelMenuItem_Click)
{
Checked = mlbl.ModuleIdentifiers.Contains(module.Identifier),
Checked = mlbl.ContainsModule(Main.Instance.CurrentInstance.game, module.Identifier),
CheckOnClick = true,
Tag = mlbl,
}
Expand All @@ -289,18 +290,19 @@ private void labelMenuItem_Click(object sender, EventArgs e)
var module = SelectedModule;
if (item.Checked)
{
mlbl.Add(module.Identifier);
mlbl.Add(Main.Instance.CurrentInstance.game, module.Identifier);
}
else
{
mlbl.Remove(module.Identifier);
mlbl.Remove(Main.Instance.CurrentInstance.game, module.Identifier);
}
if (mlbl.HoldVersion)
{
UpdateAllToolButton.Enabled = mainModList.Modules.Any(mod =>
mod.HasUpdate && !Main.Instance.LabelsHeld(mod.Identifier));
}
mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, Main.Instance.CurrentInstance.Name);
var inst = Main.Instance.CurrentInstance;
mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game);
mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath);
}

Expand All @@ -310,9 +312,10 @@ private void editLabelsToolStripMenuItem_Click(object sender, EventArgs e)
eld.ShowDialog(this);
eld.Dispose();
mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath);
var inst = Main.Instance.CurrentInstance;
foreach (GUIMod module in mainModList.Modules)
{
mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, Main.Instance.CurrentInstance.Name);
mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, inst.Name, inst.game);
}
}

Expand Down Expand Up @@ -1129,7 +1132,8 @@ private void _UpdateFilters()
foreach (var row in rows)
{
var mod = ((GUIMod) row.Tag);
row.Visible = mainModList.IsVisible(mod, Main.Instance.CurrentInstance.Name);
var inst = Main.Instance.CurrentInstance;
row.Visible = mainModList.IsVisible(mod, inst.Name, inst.game);
}

ApplyHeaderGlyphs();
Expand Down Expand Up @@ -1221,7 +1225,7 @@ private void _UpdateModsList(Dictionary<string, bool> old_modules = null)

Main.Instance.Wait.AddLogMessage(Properties.Resources.MainModListPopulatingList);
// Update our mod listing
mainModList.ConstructModList(gui_mods, Main.Instance.CurrentInstance.Name, ChangeSet);
mainModList.ConstructModList(gui_mods, Main.Instance.CurrentInstance.Name, Main.Instance.CurrentInstance.game, ChangeSet);

// C# 7.0: Executes the task and discards it
_ = UpdateChangeSetAndConflicts(Main.Instance.CurrentInstance, registry);
Expand Down
2 changes: 1 addition & 1 deletion GUI/Controls/ModInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ private void UpdateTagsAndLabels(CkanModule mod)
}
}
var labels = ModuleLabels?.LabelsFor(manager.CurrentInstance.Name)
.Where(l => l.ModuleIdentifiers.Contains(mod.identifier))
.Where(l => l.ContainsModule(Main.Instance.CurrentInstance.game, mod.identifier))
.OrderBy(l => l.Name);
if (labels != null)
{
Expand Down
54 changes: 49 additions & 5 deletions GUI/Labels/ModuleLabel.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
using System;
using System.Linq;
using System.Drawing;
using System.ComponentModel;
using System.Collections;
using System.Collections.Generic;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using CKAN.Games;

namespace CKAN.GUI
{
Expand Down Expand Up @@ -43,7 +50,30 @@ public class ModuleLabel
public bool HoldVersion;

[JsonProperty("module_identifiers_by_game", NullValueHandling = NullValueHandling.Ignore)]
public HashSet<string> ModuleIdentifiers = new HashSet<string>();
[JsonConverter(typeof(JsonToGamesDictionaryConverter))]
private Dictionary<string, HashSet<string>> ModuleIdentifiers =
new Dictionary<string, HashSet<string>>();

/// <summary>
/// Return the number of modules associated with this label for a given game
/// </summary>
/// <param name="game">Game to check</param>
/// <returns>Number of modules</returns>
public int ModuleCount(IGame game)
=> ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet<string> identifiers)
? identifiers.Count
: 0;

/// <summary>
/// Return whether a given identifier is associated with this label for a given game
/// </summary>
/// <param name="game">The game to check</param>
/// <param name="identifier">The identifier to check</param>
/// <returns>true if this label applies to this identifier, false otherwise</returns>
public bool ContainsModule(IGame game, string identifier)
=> ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet<string> identifiers)
? identifiers.Contains(identifier)
: false;

/// <summary>
/// Check whether this label is active for a given game instance
Expand All @@ -59,18 +89,32 @@ public bool AppliesTo(string instanceName)
/// Add a module to this label's group
/// </summary>
/// <param name="identifier">The identifier of the module to add</param>
public void Add(string identifier)
public void Add(IGame game, string identifier)
{
ModuleIdentifiers.Add(identifier);
if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet<string> identifiers))
{
identifiers.Add(identifier);
}
else
{
ModuleIdentifiers.Add(game.ShortName, new HashSet<string> {identifier});
}
}

/// <summary>
/// Remove a module from this label's group
/// </summary>
/// <param name="identifier">The identifier of the module to remove</param>
public void Remove(string identifier)
public void Remove(IGame game, string identifier)
{
ModuleIdentifiers.Remove(identifier);
if (ModuleIdentifiers.TryGetValue(game.ShortName, out HashSet<string> identifiers))
{
identifiers.Remove(identifier);
if (identifiers.Count < 1)
{
ModuleIdentifiers.Remove(game.ShortName);
}
}
}
}

Expand Down
16 changes: 7 additions & 9 deletions GUI/Main/MainLabels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable<GUIMod> mods)
var toNotif = mods
.Where(m =>
notifLabs.Any(l =>
l.ModuleIdentifiers.Contains(m.Identifier)))
l.ContainsModule(CurrentInstance.game, m.Identifier)))
.Select(m => m.Name)
.Memoize();
if (toNotif.Any())
Expand All @@ -39,9 +39,9 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable<GUIMod> mods)
{
foreach (ModuleLabel l in ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)
.Where(l => l.RemoveOnChange
&& l.ModuleIdentifiers.Contains(mod.Identifier)))
&& l.ContainsModule(CurrentInstance.game, mod.Identifier)))
{
l.Remove(mod.Identifier);
l.Remove(CurrentInstance.game, mod.Identifier);
}
}
});
Expand All @@ -50,17 +50,15 @@ private void ManageMods_LabelsAfterUpdate(IEnumerable<GUIMod> mods)
private void LabelsAfterInstall(CkanModule mod)
{
foreach (ModuleLabel l in ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)
.Where(l => l.RemoveOnInstall && l.ModuleIdentifiers.Contains(mod.identifier)))
.Where(l => l.RemoveOnInstall && l.ContainsModule(CurrentInstance.game, mod.identifier)))
{
l.Remove(mod.identifier);
l.Remove(CurrentInstance.game, mod.identifier);
}
}

public bool LabelsHeld(string identifier)
{
return ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)
.Any(l => l.HoldVersion && l.ModuleIdentifiers.Contains(identifier));
}
=> ManageMods.mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)
.Any(l => l.HoldVersion && l.ContainsModule(CurrentInstance.game, identifier));

#endregion
}
Expand Down
Loading

0 comments on commit 3bb37cf

Please sign in to comment.