using System;
using System.Drawing;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif
using Autofac;
using log4net;
using CKAN.Configuration;
using CKAN.Versioning;
using CKAN.Games;
using CKAN.Extensions;
namespace CKAN.GUI
{
/// <summary>
/// The holder of the list of mods to be shown.
/// Should be a pure data model and avoid UI stuff, but it's not there yet.
/// </summary>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public class ModList
{
/// <summary>
/// Constructs the mod list suitable for display to the user.
/// </summary>
/// <param name="modules">A list of modules that may require updating</param>
/// <param name="instance">Game instance for getting labels</param>
/// <param name="allLabels">All label definitions</param>
/// <param name="allTags">All tag definitions</param>
/// <param name="guiConfig">GUI configuration</param>
/// <param name="graphics">Graphics object for calculating word wrap</param>
/// <param name="mc">Changes the user has made</param>
/// <returns>The mod list</returns>
public ModList(IReadOnlyCollection<GUIMod> modules,
GameInstance instance,
ModuleLabelList allLabels,
ModuleTagList allTags,
GUIConfiguration guiConfig,
Graphics graphics,
List<ModChange>? mc = null)
{
this.allLabels = allLabels;
this.allTags = allTags;
this.guiConfig = guiConfig;
this.graphics = graphics;
activeSearches = guiConfig.DefaultSearches
?.Select(s => ModSearch.Parse(allLabels, instance, s))
.OfType<ModSearch>()
.ToArray()
?? Array.Empty<ModSearch>();
Modules = modules;
HasAnyInstalled = Modules.Any(m => m.IsInstalled);
full_list_of_mod_rows = Modules.AsParallel()
.ToDictionary(gm => gm.Identifier,
gm => MakeRow(gm, mc, instance));
}
// identifier => row
public readonly IReadOnlyCollection<GUIMod> Modules;
public readonly bool HasAnyInstalled;
public readonly Dictionary<string, DataGridViewRow> full_list_of_mod_rows;
public event Action? ModFiltersUpdated;
#region Building the mod list
/// <summary>
/// Get all the GUI mods for the given instance.
/// </summary>
/// <param name="registry">Registry of the instance</param>
/// <param name="repoData">Repo data of the instance</param>
/// <param name="inst">Game instance</param>
/// <param name="allLabels">All label definitions</param>
/// <param name="cache">Cache for checking if mods are cached</param>
/// <param name="config">GUI config to use</param>
/// <returns>Sequence of GUIMods</returns>
public static IEnumerable<GUIMod> GetGUIMods(IRegistryQuerier registry,
RepositoryDataManager repoData,
GameInstance inst,
ModuleLabelList allLabels,
NetModuleCache cache,
GUIConfiguration? config)
=> GetGUIMods(registry, repoData, inst,
registry.InstalledModules.Select(im => im.identifier)
.ToHashSet(),
allLabels,
cache,
config?.HideEpochs ?? false, config?.HideV ?? false);
private static IEnumerable<GUIMod> GetGUIMods(IRegistryQuerier registry,
RepositoryDataManager repoData,
GameInstance inst,
HashSet<string> installedIdents,
ModuleLabelList allLabels,
NetModuleCache cache,
bool hideEpochs,
bool hideV)
=> registry.InstalledModulesByUpgradeability(inst,
allLabels.HeldIdentifiers(inst)
.ToHashSet(),
allLabels.IgnoreMissingIdentifiers(inst)
.ToHashSet())
.Select(tuple => registry.IsAutodetected(tuple.module.identifier)
? new GUIMod(tuple.module, repoData, registry,
inst.StabilityToleranceConfig,
inst, cache, null,
hideEpochs, hideV)
{
HasUpdate = tuple.upgradeable,
}
: registry.InstalledModule(tuple.module.identifier)
is InstalledModule found
? new GUIMod(found, repoData, registry,
inst.StabilityToleranceConfig,
inst, cache, null,
hideEpochs, hideV)
{
HasUpdate = tuple.upgradeable,
}
: null)
.OfType<GUIMod>()
.Concat(registry.CompatibleModules(inst.StabilityToleranceConfig, inst.VersionCriteria())
.Where(m => !installedIdents.Contains(m.identifier))
.AsParallel()
.Where(m => !m.IsDLC)
.Select(m => new GUIMod(m, repoData, registry,
inst.StabilityToleranceConfig,
inst, cache, null,
hideEpochs, hideV)))
.Concat(registry.IncompatibleModules(inst.StabilityToleranceConfig, inst.VersionCriteria())
.Where(m => !installedIdents.Contains(m.identifier))
.AsParallel()
.Where(m => !m.IsDLC)
.Select(m => new GUIMod(m, repoData, registry,
inst.StabilityToleranceConfig,
inst, cache, true,
hideEpochs, hideV)));
private DataGridViewRow MakeRow(GUIMod mod,
List<ModChange>? changes,
GameInstance instance)
{
DataGridViewRow item = new DataGridViewRow() { Tag = mod };
item.DefaultCellStyle.BackColor = GetRowBackground(mod, false, instance);
item.DefaultCellStyle.ForeColor = item.DefaultCellStyle.BackColor.ForeColorForBackColor()
?? SystemColors.WindowText;
item.DefaultCellStyle.SelectionBackColor = SelectionBlend(item.DefaultCellStyle.BackColor);
item.DefaultCellStyle.SelectionForeColor = item.DefaultCellStyle.SelectionBackColor.ForeColorForBackColor()
?? SystemColors.HighlightText;
var myChange = changes?.FindLast(ch => ch.Mod.Equals(mod));
var selecting = mod.IsAutodetected
? new DataGridViewTextBoxCell()
{
Value = Properties.Resources.MainModListAutoDetected
}
: mod.IsInstallable()
? (DataGridViewCell) new DataGridViewCheckBoxCell()
{
Value = myChange == null
? mod.IsInstalled
: myChange.ChangeType == GUIModChangeType.Install
|| (myChange.ChangeType != GUIModChangeType.Remove && mod.IsInstalled)
}
: new DataGridViewTextBoxCell()
{
Value = "-"
};
var autoInstalled = mod.IsInstalled && !mod.IsAutodetected
? (DataGridViewCell) new DataGridViewCheckBoxCell()
{
Value = mod.IsAutoInstalled,
ToolTipText = Properties.Resources.MainModListAutoInstalledToolTip,
}
: new DataGridViewTextBoxCell()
{
Value = "-"
};
var updating = mod.IsInstallable() && mod.HasUpdate
? (DataGridViewCell) new DataGridViewCheckBoxCell()
{
Value = myChange?.ChangeType == GUIModChangeType.Update
}
: new DataGridViewTextBoxCell()
{
Value = "-"
};
var replacing = (mod.IsInstalled && mod.HasReplacement)
? (DataGridViewCell) new DataGridViewCheckBoxCell()
{
Value = myChange?.ChangeType == GUIModChangeType.Replace
}
: new DataGridViewTextBoxCell()
{
Value = "-"
};
var name = new DataGridViewTextBoxCell { Value = ToGridText(mod.Name) };
var author = new DataGridViewTextBoxCell { Value = ToGridText(string.Join(", ", mod.Authors)) };
var installVersion = new DataGridViewTextBoxCell { Value = mod.InstalledVersion ?? "" };
var latestVersion = new DataGridViewTextBoxCell { Value = mod.LatestVersion };
var compat = new DataGridViewTextBoxCell { Value = mod.GameCompatibility ?? "" };
var downloadSize = new DataGridViewTextBoxCell { Value = mod.DownloadSize ?? "" };
var installSize = new DataGridViewTextBoxCell { Value = mod.InstallSize };
var releaseDate = new DataGridViewTextBoxCell { Value = (object?)mod.Module.release_date?.ToLocalTime() ?? "" };
var installDate = new DataGridViewTextBoxCell { Value = (object?)mod.InstallDate?.ToLocalTime() ?? "" };
var downloadCount = new DataGridViewTextBoxCell { Value = $"{mod.DownloadCount:N0}" };
var desc = new DataGridViewTextBoxCell
{
Value = ToGridText(mod.Abstract),
ToolTipText = mod.Description is { Length: > 0 }
? string.Join(Environment.NewLine,
graphics.WordWrap(mod.Description, 600)
.Prepend("")
.Prepend(mod.Abstract))
: mod.Abstract,
};
item.Cells.AddRange(selecting, autoInstalled, updating, replacing, name, author, installVersion, latestVersion, compat, downloadSize, installSize, releaseDate, installDate, downloadCount, desc);
selecting.ReadOnly = selecting is DataGridViewTextBoxCell;
autoInstalled.ReadOnly = autoInstalled is DataGridViewTextBoxCell;
updating.ReadOnly = updating is DataGridViewTextBoxCell;
return item;
}
private static string ToGridText(string text)
=> Platform.IsMono ? text.Replace("&", "&&") : text;
#endregion
#region Search & filters
public void SetSearches(List<ModSearch> newSearches)
{
if (!SearchesEqual(activeSearches, newSearches))
{
activeSearches = newSearches;
guiConfig.DefaultSearches = activeSearches.Select(s => s.Combined ?? "")
.ToList();
ModFiltersUpdated?.Invoke();
}
}
public static SavedSearch FilterToSavedSearch(GameInstance instance,
GUIModFilter filter,
ModuleLabelList allLabels,
ModuleTag? tag = null,
ModuleLabel? label = null)
=> new SavedSearch()
{
Name = FilterName(filter, tag, label),
Values = new List<string>()
{
new ModSearch(allLabels, instance,
filter, tag, label)
.Combined
?? ""
},
};
private static bool SearchesEqual(IReadOnlyCollection<ModSearch> a,
IReadOnlyCollection<ModSearch> b)
=> a.SequenceEqual(b);
private static string FilterName(GUIModFilter filter,
ModuleTag? tag = null,
ModuleLabel? label = null)
=> filter switch
{
GUIModFilter.Compatible => Properties.Resources.MainFilterCompatible,
GUIModFilter.Incompatible => Properties.Resources.MainFilterIncompatible,
GUIModFilter.Installed => Properties.Resources.MainFilterInstalled,
GUIModFilter.NotInstalled => Properties.Resources.MainFilterNotInstalled,
GUIModFilter.InstalledUpdateAvailable => Properties.Resources.MainFilterUpgradeable,
GUIModFilter.Replaceable => Properties.Resources.MainFilterReplaceable,
GUIModFilter.Cached => Properties.Resources.MainFilterCached,
GUIModFilter.Uncached => Properties.Resources.MainFilterUncached,
GUIModFilter.NewInRepository => Properties.Resources.MainFilterNew,
GUIModFilter.All => Properties.Resources.MainFilterAll,
GUIModFilter.CustomLabel => string.Format(Properties.Resources.MainFilterLabel,
label?.Name ?? "CUSTOM"),
GUIModFilter.Tag => tag == null
? Properties.Resources.MainFilterUntagged
: string.Format(Properties.Resources.MainFilterTag,
tag.Name),
_ => "",
};
#endregion
#region Visibility
// Unlike GUIMod.IsInstalled, DataGridViewRow.Visible can change on the fly without notifying us
public bool HasVisibleInstalled()
=> full_list_of_mod_rows.Values.Any(row => ((row.Tag as GUIMod)?.IsInstalled ?? false)
&& row.Visible);
public bool IsVisible(GUIMod mod, GameInstance instance, Registry registry)
=> activeSearches.IsEmptyOrAny(s => s.Matches(mod))
&& !HiddenByTagsOrLabels(mod, instance, registry);
private bool HiddenByTagsOrLabels(GUIMod m, GameInstance instance, Registry registry)
// "Hide" labels apply to all non-custom filters
=> (allLabels?.LabelsFor(instance.Name)
.Where(l => !LabelInSearches(l) && l.Hide)
.Any(l => l.ContainsModule(instance.Game, m.Identifier))
?? false)
|| (registry.Tags?.Values
.Where(t => !TagInSearches(t) && allTags.HiddenTags.Contains(t.Name))
.Any(t => t.ModuleIdentifiers.Contains(m.Identifier))
?? false);
private bool TagInSearches(ModuleTag tag)
=> activeSearches.Any(s => s.TagNames.Contains(tag.Name));
private bool LabelInSearches(ModuleLabel label)
=> activeSearches.Any(s => s.LabelNames.Contains(label.Name));
#endregion
public int CountModsBySearches(List<ModSearch> searches)
=> Modules.Count(mod => searches?.Any(s => s?.Matches(mod) ?? true) ?? true);
public int CountModsByFilter(GameInstance inst, GUIModFilter filter)
=> CountModsBySearches(new List<ModSearch>() { new ModSearch(allLabels, inst, filter, null, null) });
private Color GetRowBackground(GUIMod mod, bool conflicted, GameInstance instance)
=> conflicted
? ConflictBackColor
: Util.BlendColors(allLabels.LabelsFor(instance.Name)
.Where(l => l.ContainsModule(instance.Game,
mod.Identifier))
.Select(l => l.Color)
.OfType<Color>()
// No transparent blending
.Where(c => c.A == byte.MaxValue)
.ToArray());
/// <summary>
/// Update the color and visible state of the given row
/// after it has been added to or removed from a label group
/// </summary>
/// <param name="mod">The mod that needs an update</param>
/// <param name="conflicted">True if mod should have a red background</param>
/// <param name="instance">Game instance for finding labels</param>
/// <param name="registry">Registry for finding mods</param>
public DataGridViewRow? ReapplyLabels(GUIMod mod, bool conflicted,
GameInstance instance, Registry registry)
{
if (full_list_of_mod_rows.TryGetValue(mod.Identifier, out DataGridViewRow? row))
{
row.DefaultCellStyle.BackColor = GetRowBackground(mod, conflicted, instance);
row.DefaultCellStyle.ForeColor = row.DefaultCellStyle.BackColor.ForeColorForBackColor()
?? SystemColors.WindowText;
row.DefaultCellStyle.SelectionBackColor = SelectionBlend(row.DefaultCellStyle.BackColor);
row.DefaultCellStyle.SelectionForeColor = row.DefaultCellStyle.SelectionBackColor.ForeColorForBackColor()
?? SystemColors.HighlightText;
row.Visible = IsVisible(mod, instance, registry);
return row;
}
return null;
}
#region Changesets
/// <summary>
/// Get the changes the user has selected in the grid
/// </summary>
/// <param name="registry">Registry containing the installed and available mods</param>
/// <param name="instance">The current game instance</param>
/// <param name="upgradeCol">Column containing the upgrade checkboxes</param>
/// <param name="replaceCol">Column containing the replace checkboxes</param>
/// <returns>Hashset of changes</returns>
public HashSet<ModChange> ComputeUserChangeSet(IRegistryQuerier registry,
GameInstance instance,
DataGridViewColumn? upgradeCol,
DataGridViewColumn? replaceCol)
{
log.Debug("Computing user changeset");
var modChanges = full_list_of_mod_rows.Values
.SelectMany(row => rowChanges(registry, row, upgradeCol, replaceCol))
.ToHashSet();
// Inter-mod dependencies can block some upgrades, which can sometimes but not always
// be overcome by upgrading both mods. Try to pick the right target versions.
if (modChanges.OfType<ModUpgrade>()
// Skip reinstalls
.Where(upg => upg.Mod != upg.targetMod)
.ToArray()
is { Length: > 0 } upgrades)
{
var upgradeable = registry.UpgradeableModules(instance,
// Hold identifiers not chosen for upgrading
registry.Installed(false)
.Select(kvp => kvp.Key)
.Except(upgrades.Select(ch => ch.Mod.identifier))
.ToHashSet(),
allLabels.IgnoreMissingIdentifiers(instance)
.ToHashSet())
.ToDictionary(m => m.identifier,
m => m);
foreach (var change in upgrades)
{
change.targetMod = upgradeable.TryGetValue(change.Mod.identifier,
out CkanModule? allowedMod)
// Upgrade to the version the registry says we should
? allowedMod
// Not upgradeable!
: change.Mod;
if (change.Mod == change.targetMod)
{
// This upgrade was voided by dependencies or conflicts
modChanges.Remove(change);
}
}
}
return modChanges;
}
private static IEnumerable<ModChange> rowChanges(IRegistryQuerier registry,
DataGridViewRow row,
DataGridViewColumn? upgradeCol,
DataGridViewColumn? replaceCol)
=> row.Tag is GUIMod gmod
? gmod.GetModChanges(upgradeCol?.Visible == true
&& row.Cells[upgradeCol.Index] is DataGridViewCheckBoxCell upgradeCell
&& upgradeCell.Value is true,
replaceCol?.Visible == true
&& row.Cells[replaceCol.Index] is DataGridViewCheckBoxCell replaceCell
&& replaceCell.Value is true,
registry.MetadataChanged(gmod.Identifier, out bool installedFilesChanged),
installedFilesChanged)
: Enumerable.Empty<ModChange>();
public static Tuple<ICollection<ModChange>, Dictionary<CkanModule, string>, List<string>> ComputeFullChangeSetFromUserChangeSet(
IRegistryQuerier registry,
HashSet<ModChange> changeSet,
IConfiguration coreConfig,
GameInstance instance)
=> ComputeFullChangeSetFromUserChangeSet(registry, changeSet, coreConfig,
instance.Game, instance.StabilityToleranceConfig, instance.VersionCriteria());
/// <summary>
/// Returns a changeset and conflicts based on the selections of the user.
/// </summary>
/// <param name="registry">The registry for getting available mods</param>
/// <param name="changeSet">User's choices of installation and removal</param>
/// <param name="coreConfig">Core configuration</param>
/// <param name="game">Game of the game instance</param>
/// <param name="stabilityTolerance">Prerelease configuration</param>
/// <param name="version">The version of the current game instance</param>
/// <returns>
/// Tuple of:
/// 1. Full changeset, including auto-installed dependencies
/// 2. Mapping from conflicting mods to description of the conflict
/// 3. Descriptions of all conflicts
/// </returns>
public static Tuple<ICollection<ModChange>, Dictionary<CkanModule, string>, List<string>> ComputeFullChangeSetFromUserChangeSet(
IRegistryQuerier registry,
HashSet<ModChange> changeSet,
IConfiguration coreConfig,
IGame game,
StabilityToleranceConfig stabilityTolerance,
GameVersionCriteria version)
{
changeSet.UnionWith(changeSet.Where(ch => ch.ChangeType == GUIModChangeType.Replace)
.Select(ch => registry.GetReplacement(ch.Mod, stabilityTolerance, version))
.OfType<ModuleReplacement>()
.GroupBy(repl => repl.ReplaceWith)
.Select(grp => new ModChange(grp.Key, GUIModChangeType.Install,
grp.Select(repl => new SelectionReason.Replacement(repl.ToReplace)),
coreConfig)));
var toInstall = new List<CkanModule>();
var toRemove = new HashSet<CkanModule>();
var extraInstalls = new HashSet<CkanModule>();
foreach (var change in changeSet)
{
switch (change.ChangeType)
{
case GUIModChangeType.None:
break;
case GUIModChangeType.Update:
var mod = (change as ModUpgrade)?.targetMod ?? change.Mod;
toInstall.Add(mod);
extraInstalls.Add(mod);
break;
case GUIModChangeType.Install:
toInstall.Add(change.Mod);
break;
case GUIModChangeType.Remove:
toRemove.Add(change.Mod);
break;
case GUIModChangeType.Replace:
if (registry.GetReplacement(change.Mod, stabilityTolerance, version) is ModuleReplacement repl)
{
toRemove.Add(repl.ToReplace);
extraInstalls.Add(repl.ReplaceWith);
}
break;
}
}
// Check for depending mods if any are still left
if (!registry.InstalledModules.Select(im => im.Module)
.All(toRemove.Contains))
{
var installedModules = registry.InstalledModules.ToDictionary(
imod => imod.Module.identifier,
imod => imod.Module);
foreach (var dependent in registry.FindReverseDependencies(
toRemove.Select(mod => mod.identifier)
.Except(toInstall.Select(m => m.identifier))
.ToList(),
toInstall))
{
if (!changeSet.Any(ch => ch.ChangeType == GUIModChangeType.Replace
&& ch.Mod.identifier == dependent)
&& installedModules.TryGetValue(dependent, out CkanModule? depMod)
&& (registry.GetModuleByVersion(depMod.identifier, depMod.version)
?? registry.InstalledModule(dependent)?.Module)
is CkanModule modByVer)
{
changeSet.Add(new ModChange(modByVer, GUIModChangeType.Remove,
new SelectionReason.DependencyRemoved(),
coreConfig));
toRemove.Add(modByVer);
}
}
// Check for auto-installed dependencies if any mods are still left
if (!registry.InstalledModules.Select(im => im.Module)
.All(toRemove.Contains))
{
foreach (var im in registry.FindRemovableAutoInstalled(
InstalledAfterChanges(registry, changeSet).ToArray(),
Array.Empty<CkanModule>(), game, stabilityTolerance, version))
{
changeSet.Add(new ModChange(im.Module, GUIModChangeType.Remove, new SelectionReason.NoLongerUsed(),
coreConfig));
toRemove.Add(im.Module);
}
}
}
// Get as many dependencies as we can, but leave decisions and prompts for installation time
var resolver = new RelationshipResolver(toInstall, toRemove,
conflictOptions(stabilityTolerance),
registry, game, version);
// Replace Install entries in changeset with the ones from resolver to get all the reasons
return new Tuple<ICollection<ModChange>, Dictionary<CkanModule, string>, List<string>>(
changeSet.Where(ch => !(ch.ChangeType is GUIModChangeType.Install
// Leave in replacements
&& !ch.Reasons.Any(r => r is SelectionReason.Replacement)))
.OrderBy(ch => ch.Mod.identifier)
.Union(resolver.ModList()
// Changeset already contains changes for these
.Except(extraInstalls)
.Select(m => new ModChange(m, GUIModChangeType.Install, resolver.ReasonsFor(m),
coreConfig)))
.ToArray(),
resolver.ConflictList,
resolver.ConflictDescriptions.ToList());
}
/// <summary>
/// Get the InstalledModules that we'll have after the changeset,
/// not including dependencies
/// </summary>
/// <param name="registry">Registry with currently installed modules</param>
/// <param name="changeSet">Changes to be made to the installed modules</param>
private static IEnumerable<InstalledModule> InstalledAfterChanges(
IRegistryQuerier registry,
IReadOnlyCollection<ModChange> changeSet)
{
var removingIdents = changeSet
.Where(ch => ch.ChangeType != GUIModChangeType.Install)
.Select(ch => ch.Mod.identifier)
.ToHashSet();
return registry.InstalledModules
.Where(im => !removingIdents.Contains(im.identifier))
.Concat(changeSet
.Where(ch => ch.ChangeType is not GUIModChangeType.Remove
and not GUIModChangeType.Replace)
.Select(ch => new InstalledModule(
null,
(ch as ModUpgrade)?.targetMod ?? ch.Mod,
Enumerable.Empty<string>(),
false)));
}
private static RelationshipResolverOptions conflictOptions(StabilityToleranceConfig stabilityTolerance)
=> new RelationshipResolverOptions(stabilityTolerance)
{
without_toomanyprovides_kraken = true,
proceed_with_inconsistencies = true,
without_enforce_consistency = true,
with_recommends = false
};
#endregion
#region Upgradeability
/// <summary>
/// Check upgradeability of all rows and set GUIMod.HasUpdate appropriately
/// </summary>
/// <param name="inst">Current game instance</param>
/// <param name="registry">Current instance's registry</param>
/// <param name="ChangeSet">Currently pending changeset</param>
/// <param name="rows">The grid rows in case we need to replace some</param>
/// <returns>true if any mod can be updated, false otherwise</returns>
public bool ResetHasUpdate(GameInstance inst,
IRegistryQuerier registry,
List<ModChange>? ChangeSet,
DataGridViewRowCollection rows)
{
var dlls = registry.InstalledDlls.ToList();
bool hasUpgradeable = false;
foreach (var (upgradeable, module) in registry.InstalledModulesByUpgradeability(
inst,
allLabels.HeldIdentifiers(inst)
.ToHashSet(),
allLabels.IgnoreMissingIdentifiers(inst)
.ToHashSet()))
{
hasUpgradeable = hasUpgradeable || upgradeable;
dlls.Remove(module.identifier);
CheckRowUpgradeable(inst, ChangeSet, rows, module.identifier, upgradeable);
}
// AD mods don't have CkanModules in the return value of CheckUpgradeable
foreach (var ident in dlls)
{
CheckRowUpgradeable(inst, ChangeSet, rows, ident, false);
}
return hasUpgradeable;
}
private void CheckRowUpgradeable(GameInstance inst,
List<ModChange>? ChangeSet,
DataGridViewRowCollection rows,
string ident,
bool upgradeable)
{
if (full_list_of_mod_rows.TryGetValue(ident, out DataGridViewRow? row)
&& row.Tag is GUIMod gmod
&& gmod.HasUpdate != upgradeable)
{
gmod.HasUpdate = upgradeable;
if (row.Visible)
{
// Swap whether the row has an upgrade checkbox
var newRow = full_list_of_mod_rows[ident] = MakeRow(gmod, ChangeSet, inst);
var rowIndex = row.Index;
var selected = row.Selected;
rows.Remove(row);
rows.Insert(rowIndex, newRow);
if (selected)
{
rows[rowIndex].Selected = true;
}
}
}
}
#endregion
private static Color SelectionBlend(Color c)
=> c == Color.Empty
? SystemColors.Highlight
: SystemColors.Highlight.AlphaBlendWith(selectionAlpha, c);
private const float selectionAlpha = 0.4f;
private readonly ModuleLabelList allLabels;
private readonly ModuleTagList allTags;
private readonly GUIConfiguration guiConfig;
private readonly Graphics graphics;
private IReadOnlyCollection<ModSearch> activeSearches;
public static readonly Color ConflictBackColor = Color.FromArgb(255, 64, 64);
public static readonly Color ConflictForeColor = ConflictBackColor.ForeColorForBackColor() ?? SystemColors.WindowText;
private static readonly ILog log = LogManager.GetLogger(typeof(ModList));
}
}