diff --git a/Core/Extensions/DictionaryExtensions.cs b/Core/Extensions/DictionaryExtensions.cs new file mode 100644 index 0000000000..62fcb8f18a --- /dev/null +++ b/Core/Extensions/DictionaryExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace CKAN.Extensions +{ + public static class DictionaryExtensions + { + + public static V GetOrDefault(this Dictionary dict, K key) + { + V val = default(V); + if (key != null) + { + dict.TryGetValue(key, out val); + } + return val; + } + + } +} diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index efb9baf0e2..b3b7c926fd 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -64,7 +64,8 @@ public void SetDownloadCounts(SortedDictionary counts) // Index of which mods provide what, format: // providers[provided] = { provider1, provider2, ... } // Built by BuildProvidesIndex, makes LatestAvailableWithProvides much faster. - [JsonIgnore] private Dictionary> providers + [JsonIgnore] + private Dictionary> providers = new Dictionary>(); /// @@ -686,6 +687,16 @@ private void BuildProvidesIndexFor(AvailableModule am) } } + public void BuildTagIndex(ModuleTagList tags) + { + tags.Tags.Clear(); + tags.Untagged.Clear(); + foreach (AvailableModule am in available_modules.Values) + { + tags.BuildTagIndexFor(am); + } + } + /// /// /// diff --git a/Core/Registry/Tags/ModuleTag.cs b/Core/Registry/Tags/ModuleTag.cs new file mode 100644 index 0000000000..a5bd7979a1 --- /dev/null +++ b/Core/Registry/Tags/ModuleTag.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace CKAN +{ + public class ModuleTag + { + public string Name; + public bool Visible; + public HashSet ModuleIdentifiers = new HashSet(); + + /// + /// Add a module to this label's group + /// + /// The identifier of the module to add + public void Add(string identifier) + { + ModuleIdentifiers.Add(identifier); + } + + /// + /// Remove a module from this label's group + /// + /// The identifier of the module to remove + public void Remove(string identifier) + { + ModuleIdentifiers.Remove(identifier); + } + } +} diff --git a/Core/Registry/Tags/ModuleTagList.cs b/Core/Registry/Tags/ModuleTagList.cs new file mode 100644 index 0000000000..83865b1e67 --- /dev/null +++ b/Core/Registry/Tags/ModuleTagList.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.IO; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CKAN +{ + public class ModuleTagList + { + [JsonIgnore] + public Dictionary Tags = new Dictionary(); + + [JsonIgnore] + public HashSet Untagged = new HashSet(); + + [JsonProperty("hidden_tags")] + public HashSet HiddenTags = new HashSet(); + + public static readonly string DefaultPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "CKAN", + "tags.json" + ); + + public void BuildTagIndexFor(AvailableModule am) + { + bool tagged = false; + foreach (CkanModule m in am.AllAvailable()) + { + if (m.Tags != null) + { + tagged = true; + foreach (string tagName in m.Tags) + { + ModuleTag tag = null; + if (Tags.TryGetValue(tagName, out tag)) + tag.Add(m.identifier); + else + Tags.Add(tagName, new ModuleTag() + { + Name = tagName, + Visible = !HiddenTags.Contains(tagName), + ModuleIdentifiers = new HashSet() { m.identifier }, + }); + } + } + } + if (!tagged) + { + Untagged.Add(am.AllAvailable().First().identifier); + } + } + + public static ModuleTagList Load(string path) + { + try + { + return JsonConvert.DeserializeObject(File.ReadAllText(path)); + } + catch (FileNotFoundException ex) + { + return null; + } + } + + public bool Save(string path) + { + try + { + File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented)); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/Core/Types/CkanModule.cs b/Core/Types/CkanModule.cs index 4014012069..034bccbf56 100644 --- a/Core/Types/CkanModule.cs +++ b/Core/Types/CkanModule.cs @@ -435,7 +435,7 @@ public class CkanModule : IEquatable [JsonProperty("install", NullValueHandling = NullValueHandling.Ignore)] public ModuleInstallDescriptor[] install; - + [JsonProperty("localizations", NullValueHandling = NullValueHandling.Ignore)] public string[] localizations; @@ -469,6 +469,9 @@ public ModuleVersion spec_version } } + [JsonProperty("tags", NullValueHandling = NullValueHandling.Ignore)] + public HashSet Tags; + // A list of eveything this mod provides. public List ProvidesList { diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index cdce598e77..708cf48484 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -116,6 +116,12 @@ Component + + Form + + + EditLabelsDialog.cs + Form @@ -144,6 +150,8 @@ KSPCommandLineOptionsDialog.cs + + Form @@ -168,6 +176,7 @@ Form + UserControl @@ -287,6 +296,12 @@ ..\..\CompatibleKspVersionsDialog.cs + + EditLabelsDialog.cs + + + ..\..\EditLabelsDialog.cs + ErrorDialog.cs diff --git a/GUI/EditLabelsDialog.Designer.cs b/GUI/EditLabelsDialog.Designer.cs new file mode 100644 index 0000000000..b87f5dd6c2 --- /dev/null +++ b/GUI/EditLabelsDialog.Designer.cs @@ -0,0 +1,315 @@ +namespace CKAN +{ + partial class EditLabelsDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(EditLabelsDialog)); + this.ToolTip = new System.Windows.Forms.ToolTip(); + this.LabelSelectionTree = new System.Windows.Forms.TreeView(); + this.SelectOrCreateLabel = new System.Windows.Forms.Label(); + this.EditDetailsPanel = new System.Windows.Forms.Panel(); + this.NameLabel = new System.Windows.Forms.Label(); + this.NameTextBox = new System.Windows.Forms.TextBox(); + this.ColorLabel = new System.Windows.Forms.Label(); + this.ColorButton = new System.Windows.Forms.Button(); + this.InstanceNameLabel = new System.Windows.Forms.Label(); + this.InstanceNameComboBox = new System.Windows.Forms.ComboBox(); + this.HideFromOtherFiltersCheckBox = new System.Windows.Forms.CheckBox(); + this.NotifyOnChangesCheckBox = new System.Windows.Forms.CheckBox(); + this.RemoveOnChangesCheckBox = new System.Windows.Forms.CheckBox(); + this.AlertOnInstallCheckBox = new System.Windows.Forms.CheckBox(); + this.RemoveOnInstallCheckBox = new System.Windows.Forms.CheckBox(); + this.CreateButton = new System.Windows.Forms.Button(); + this.CloseButton = new System.Windows.Forms.Button(); + this.SaveButton = new System.Windows.Forms.Button(); + this.CancelButton = new System.Windows.Forms.Button(); + this.DeleteButton = new System.Windows.Forms.Button(); + this.EditDetailsPanel.SuspendLayout(); + this.SuspendLayout(); + // + // ToolTip + // + this.ToolTip.AutoPopDelay = 10000; + this.ToolTip.InitialDelay = 250; + this.ToolTip.ReshowDelay = 250; + this.ToolTip.ShowAlways = true; + // + // CreateButton + // + this.CreateButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Left)); + this.CreateButton.Location = new System.Drawing.Point(10, 10); + this.CreateButton.Name = "CreateButton"; + this.CreateButton.Size = new System.Drawing.Size(75, 23); + this.CreateButton.TabIndex = 0; + this.CreateButton.UseVisualStyleBackColor = true; + this.CreateButton.Click += new System.EventHandler(this.CreateButton_Click); + resources.ApplyResources(this.CreateButton, "CreateButton"); + // + // LabelSelectionTree + // + this.LabelSelectionTree.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left)))); + this.LabelSelectionTree.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.LabelSelectionTree.FullRowSelect = true; + this.LabelSelectionTree.HideSelection = false; + this.LabelSelectionTree.Indent = 16; + this.LabelSelectionTree.ItemHeight = 24; + this.LabelSelectionTree.Location = new System.Drawing.Point(10, 43); + this.LabelSelectionTree.Name = "LabelSelectionTree"; + this.LabelSelectionTree.Size = new System.Drawing.Size(125, 320); + this.LabelSelectionTree.ShowPlusMinus = false; + this.LabelSelectionTree.ShowRootLines = false; + this.LabelSelectionTree.ShowLines = false; + this.LabelSelectionTree.TabIndex = 0; + this.LabelSelectionTree.BeforeSelect += new System.Windows.Forms.TreeViewCancelEventHandler(LabelSelectionTree_BeforeSelect); + this.LabelSelectionTree.BeforeCollapse += new System.Windows.Forms.TreeViewCancelEventHandler(LabelSelectionTree_BeforeCollapse); + // + // SelectOrCreateLabel + // + this.SelectOrCreateLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.SelectOrCreateLabel.Location = new System.Drawing.Point(160, 50); + this.SelectOrCreateLabel.Name = "SelectOrCreateLabel"; + this.SelectOrCreateLabel.Size = new System.Drawing.Size(300, 23); + resources.ApplyResources(this.SelectOrCreateLabel, "SelectOrCreateLabel"); + // + // EditDetailsPanel + // + this.EditDetailsPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.EditDetailsPanel.Controls.Add(this.NameLabel); + this.EditDetailsPanel.Controls.Add(this.NameTextBox); + this.EditDetailsPanel.Controls.Add(this.ColorLabel); + this.EditDetailsPanel.Controls.Add(this.ColorButton); + this.EditDetailsPanel.Controls.Add(this.InstanceNameLabel); + this.EditDetailsPanel.Controls.Add(this.InstanceNameComboBox); + this.EditDetailsPanel.Controls.Add(this.HideFromOtherFiltersCheckBox); + this.EditDetailsPanel.Controls.Add(this.NotifyOnChangesCheckBox); + this.EditDetailsPanel.Controls.Add(this.RemoveOnChangesCheckBox); + this.EditDetailsPanel.Controls.Add(this.AlertOnInstallCheckBox); + this.EditDetailsPanel.Controls.Add(this.RemoveOnInstallCheckBox); + this.EditDetailsPanel.Controls.Add(this.SaveButton); + this.EditDetailsPanel.Controls.Add(this.CancelButton); + this.EditDetailsPanel.Controls.Add(this.DeleteButton); + this.EditDetailsPanel.Location = new System.Drawing.Point(135, 43); + this.EditDetailsPanel.Name = "EditDetailsPanel"; + this.EditDetailsPanel.Size = new System.Drawing.Size(350, 320); + this.EditDetailsPanel.TabIndex = 1; + this.EditDetailsPanel.Visible = false; + // + // NameLabel + // + this.NameLabel.AutoSize = true; + this.NameLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.NameLabel.Location = new System.Drawing.Point(10, 13); + this.NameLabel.Name = "NameLabel"; + this.NameLabel.Size = new System.Drawing.Size(75, 23); + resources.ApplyResources(this.NameLabel, "NameLabel"); + // + // NameTextBox + // + this.NameTextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.NameTextBox.Location = new System.Drawing.Point(90, 10); + this.NameTextBox.Name = "NameTextBox"; + this.NameTextBox.Size = new System.Drawing.Size(125, 23); + resources.ApplyResources(this.NameTextBox, "NameTextBox"); + // + // ColorLabel + // + this.ColorLabel.AutoSize = true; + this.ColorLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.ColorLabel.Location = new System.Drawing.Point(10, 43); + this.ColorLabel.Name = "ColorLabel"; + this.ColorLabel.Size = new System.Drawing.Size(75, 23); + resources.ApplyResources(this.ColorLabel, "ColorLabel"); + // + // ColorButton + // + this.ColorButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Left)); + this.ColorButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.ColorButton.Location = new System.Drawing.Point(90, 40); + this.ColorButton.Name = "ColorButton"; + this.ColorButton.Size = new System.Drawing.Size(50, 20); + this.ColorButton.UseVisualStyleBackColor = false; + this.ColorButton.Click += new System.EventHandler(this.ColorButton_Click); + resources.ApplyResources(this.ColorButton, "ColorButton"); + // + // InstanceNameLabel + // + this.InstanceNameLabel.AutoSize = true; + this.InstanceNameLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.InstanceNameLabel.Location = new System.Drawing.Point(10, 73); + this.InstanceNameLabel.Name = "InstanceNameLabel"; + this.InstanceNameLabel.Size = new System.Drawing.Size(75, 23); + resources.ApplyResources(this.InstanceNameLabel, "InstanceNameLabel"); + // + // InstanceNameComboBox + // + this.InstanceNameComboBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.InstanceNameComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.InstanceNameComboBox.Location = new System.Drawing.Point(90, 70); + this.InstanceNameComboBox.Name = "InstanceNameComboBox"; + this.InstanceNameComboBox.Size = new System.Drawing.Size(125, 23); + resources.ApplyResources(this.InstanceNameComboBox, "InstanceNameComboBox"); + // + // HideFromOtherFiltersCheckBox + // + this.HideFromOtherFiltersCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.HideFromOtherFiltersCheckBox.Location = new System.Drawing.Point(90, 100); + this.HideFromOtherFiltersCheckBox.Name = "HideFromOtherFiltersCheckBox"; + this.HideFromOtherFiltersCheckBox.Size = new System.Drawing.Size(200, 23); + resources.ApplyResources(this.HideFromOtherFiltersCheckBox, "HideFromOtherFiltersCheckBox"); + // + // NotifyOnChangesCheckBox + // + this.NotifyOnChangesCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.NotifyOnChangesCheckBox.Location = new System.Drawing.Point(90, 130); + this.NotifyOnChangesCheckBox.Name = "NotifyOnChangesCheckBox"; + this.NotifyOnChangesCheckBox.Size = new System.Drawing.Size(200, 23); + resources.ApplyResources(this.NotifyOnChangesCheckBox, "NotifyOnChangesCheckBox"); + // + // RemoveOnChangesCheckBox + // + this.RemoveOnChangesCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.RemoveOnChangesCheckBox.Location = new System.Drawing.Point(90, 160); + this.RemoveOnChangesCheckBox.Name = "RemoveOnChangesCheckBox"; + this.RemoveOnChangesCheckBox.Size = new System.Drawing.Size(200, 23); + resources.ApplyResources(this.RemoveOnChangesCheckBox, "RemoveOnChangesCheckBox"); + // + // AlertOnInstallCheckBox + // + this.AlertOnInstallCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.AlertOnInstallCheckBox.Location = new System.Drawing.Point(90, 190); + this.AlertOnInstallCheckBox.Name = "AlertOnInstallCheckBox"; + this.AlertOnInstallCheckBox.Size = new System.Drawing.Size(200, 23); + resources.ApplyResources(this.AlertOnInstallCheckBox, "AlertOnInstallCheckBox"); + // + // RemoveOnInstallCheckBox + // + this.RemoveOnInstallCheckBox.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.RemoveOnInstallCheckBox.Location = new System.Drawing.Point(90, 220); + this.RemoveOnInstallCheckBox.Name = "RemoveOnInstallCheckBox"; + this.RemoveOnInstallCheckBox.Size = new System.Drawing.Size(200, 23); + resources.ApplyResources(this.RemoveOnInstallCheckBox, "RemoveOnInstallCheckBox"); + // + // SaveButton + // + this.SaveButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Left)); + this.SaveButton.Location = new System.Drawing.Point(10, 290); + this.SaveButton.Name = "SaveButton"; + this.SaveButton.Size = new System.Drawing.Size(75, 23); + this.SaveButton.TabIndex = 0; + this.SaveButton.UseVisualStyleBackColor = true; + this.SaveButton.Click += new System.EventHandler(this.SaveButton_Click); + resources.ApplyResources(this.SaveButton, "SaveButton"); + // + // CancelButton + // + this.CancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Left)); + this.CancelButton.Location = new System.Drawing.Point(90, 290); + this.CancelButton.Name = "CancelButton"; + this.CancelButton.Size = new System.Drawing.Size(75, 23); + this.CancelButton.TabIndex = 0; + this.CancelButton.UseVisualStyleBackColor = true; + this.CancelButton.Click += new System.EventHandler(this.CancelButton_Click); + resources.ApplyResources(this.CancelButton, "CancelButton"); + // + // DeleteButton + // + this.DeleteButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Left)); + this.DeleteButton.Location = new System.Drawing.Point(170, 290); + this.DeleteButton.Name = "DeleteButton"; + this.DeleteButton.Size = new System.Drawing.Size(75, 23); + this.DeleteButton.TabIndex = 0; + this.DeleteButton.UseVisualStyleBackColor = true; + this.DeleteButton.Click += new System.EventHandler(this.DeleteButton_Click); + resources.ApplyResources(this.DeleteButton, "DeleteButton"); + // + // CloseButton + // + this.CloseButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom + | System.Windows.Forms.AnchorStyles.Left)); + this.CloseButton.Location = new System.Drawing.Point(10, 367); + this.CloseButton.Name = "CloseButton"; + this.CloseButton.Size = new System.Drawing.Size(75, 23); + this.CloseButton.TabIndex = 2; + this.CloseButton.UseVisualStyleBackColor = true; + this.CloseButton.Click += new System.EventHandler(this.CloseButton_Click); + resources.ApplyResources(this.CloseButton, "CloseButton"); + // + // EditLabelsDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(500, 400); + this.Controls.Add(this.CreateButton); + this.Controls.Add(this.LabelSelectionTree); + this.Controls.Add(this.SelectOrCreateLabel); + this.Controls.Add(this.EditDetailsPanel); + this.Controls.Add(this.CloseButton); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.Icon = Properties.Resources.AppIcon; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "EditLabelsDialog"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + resources.ApplyResources(this, "$this"); + this.EditDetailsPanel.ResumeLayout(false); + this.ResumeLayout(false); + } + + #endregion + + private System.Windows.Forms.ToolTip ToolTip; + private System.Windows.Forms.TreeView LabelSelectionTree; + private System.Windows.Forms.Label SelectOrCreateLabel; + private System.Windows.Forms.Panel EditDetailsPanel; + private System.Windows.Forms.Label NameLabel; + private System.Windows.Forms.TextBox NameTextBox; + private System.Windows.Forms.Label InstanceNameLabel; + private System.Windows.Forms.ComboBox InstanceNameComboBox; + private System.Windows.Forms.CheckBox HideFromOtherFiltersCheckBox; + private System.Windows.Forms.CheckBox NotifyOnChangesCheckBox; + private System.Windows.Forms.CheckBox RemoveOnChangesCheckBox; + private System.Windows.Forms.CheckBox AlertOnInstallCheckBox; + private System.Windows.Forms.CheckBox RemoveOnInstallCheckBox; + private System.Windows.Forms.Label ColorLabel; + private System.Windows.Forms.Button ColorButton; + private System.Windows.Forms.Button CreateButton; + private System.Windows.Forms.Button CloseButton; + private System.Windows.Forms.Button SaveButton; + private System.Windows.Forms.Button CancelButton; + private System.Windows.Forms.Button DeleteButton; + } +} diff --git a/GUI/EditLabelsDialog.cs b/GUI/EditLabelsDialog.cs new file mode 100644 index 0000000000..cfcf47a876 --- /dev/null +++ b/GUI/EditLabelsDialog.cs @@ -0,0 +1,281 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Drawing; +using System.Windows.Forms; +using log4net; + +namespace CKAN +{ + public partial class EditLabelsDialog : Form + { + public EditLabelsDialog(IUser user, KSPManager manager, ModuleLabelList labels) + { + InitializeComponent(); + this.user = user; + this.labels = labels; + InstanceNameComboBox.DataSource = new string[] { "" } + .Concat(manager.Instances.Keys).ToArray(); + LoadTree(); + + this.ToolTip.SetToolTip(NameTextBox, Properties.Resources.EditLabelsToolTipName); + this.ToolTip.SetToolTip(ColorButton, Properties.Resources.EditLabelsToolTipColor); + this.ToolTip.SetToolTip(InstanceNameComboBox, Properties.Resources.EditLabelsToolTipInstance); + this.ToolTip.SetToolTip(HideFromOtherFiltersCheckBox, Properties.Resources.EditLabelsToolTipHide); + this.ToolTip.SetToolTip(NotifyOnChangesCheckBox, Properties.Resources.EditLabelsToolTipNotifyOnChanges); + this.ToolTip.SetToolTip(RemoveOnChangesCheckBox, Properties.Resources.EditLabelsToolTipRemoveOnChanges); + this.ToolTip.SetToolTip(AlertOnInstallCheckBox, Properties.Resources.EditLabelsToolTipAlertOnInstall); + this.ToolTip.SetToolTip(RemoveOnInstallCheckBox, Properties.Resources.EditLabelsToolTipRemoveOnInstall); + } + + private void LoadTree() + { + LabelSelectionTree.BeginUpdate(); + LabelSelectionTree.Nodes.Clear(); + var groups = this.labels.Labels + .GroupBy(l => l.InstanceName) + .OrderBy(g => g.Key); + foreach (var group in groups) + { + string groupName = string.IsNullOrEmpty(group.Key) + ? Properties.Resources.ModuleLabelListGlobal + : group.Key; + var gnd = LabelSelectionTree.Nodes.Add(groupName); + gnd.NodeFont = new Font(LabelSelectionTree.Font, FontStyle.Bold); + foreach (ModuleLabel mlbl in group.OrderBy(l => l.Name)) + { + var lblnd = gnd.Nodes.Add(mlbl.Name); + lblnd.Tag = mlbl; + } + } + LabelSelectionTree.ExpandAll(); + LabelSelectionTree.EndUpdate(); + } + + private void LabelSelectionTree_BeforeSelect(Object sender, TreeViewCancelEventArgs e) + { + if (e.Node == null) + { + e.Cancel = false; + } + else if (e.Node.Tag == null) + { + e.Cancel = true; + } + else if (!TryCloseEdit()) + { + e.Cancel = true; + } + else + { + StartEdit(e.Node.Tag as ModuleLabel); + e.Cancel = false; + } + } + + private void LabelSelectionTree_BeforeCollapse(object sender, TreeViewCancelEventArgs e) + { + e.Cancel = true; + } + + private void CreateButton_Click(object sender, EventArgs e) + { + LabelSelectionTree.SelectedNode = null; + StartEdit(new ModuleLabel()); + } + + private void ColorButton_Click(object sender, EventArgs e) + { + var dlg = new ColorDialog() + { + AnyColor = true, + AllowFullOpen = true, + ShowHelp = true, + SolidColorOnly = true, + Color = ColorButton.BackColor, + }; + if (dlg.ShowDialog(this) == DialogResult.OK) + { + ColorButton.BackColor = dlg.Color; + } + } + + private void SaveButton_Click(object sender, EventArgs e) + { + string errMsg; + if (TrySave(out errMsg)) + { + LabelSelectionTree.SelectedNode = null; + } + else + { + user.RaiseError(errMsg); + } + } + + private void CancelButton_Click(object sender, EventArgs e) + { + EditDetailsPanel.Visible = false; + currentlyEditing = null; + LabelSelectionTree.SelectedNode = null; + } + + private void DeleteButton_Click(object sender, EventArgs e) + { + if (currentlyEditing != null && Main.Instance.YesNoDialog( + string.Format( + Properties.Resources.EditLabelsDialogConfirmDelete, + currentlyEditing.Name + ), + Properties.Resources.EditLabelsDialogDelete, + Properties.Resources.EditLabelsDialogCancel + )) + { + labels.Labels = labels.Labels + .Except(new ModuleLabel[] { currentlyEditing }) + .ToArray(); + EditDetailsPanel.Visible = false; + currentlyEditing = null; + LoadTree(); + } + } + + private void CloseButton_Click(object sender, EventArgs e) + { + if (TryCloseEdit()) + { + Close(); + } + } + + private void StartEdit(ModuleLabel lbl) + { + currentlyEditing = lbl; + + NameTextBox.Text = lbl.Name; + ColorButton.BackColor = lbl.Color; + InstanceNameComboBox.SelectedItem = lbl.InstanceName; + HideFromOtherFiltersCheckBox.Checked = lbl.Hide; + NotifyOnChangesCheckBox.Checked = lbl.NotifyOnChange; + RemoveOnChangesCheckBox.Checked = lbl.RemoveOnChange; + AlertOnInstallCheckBox.Checked = lbl.AlertOnInstall; + RemoveOnInstallCheckBox.Checked = lbl.RemoveOnInstall; + + DeleteButton.Enabled = labels.Labels.Contains(lbl); + + EditDetailsPanel.Visible = true; + EditDetailsPanel.BringToFront(); + NameTextBox.Focus(); + } + + private bool TryCloseEdit() + { + if (HasChanges()) + { + if (Main.Instance.YesNoDialog( + Properties.Resources.EditLabelsDialogSavePrompt, + Properties.Resources.EditLabelsDialogSave, + Properties.Resources.EditLabelsDialogDiscard + )) + { + string errMsg; + if (!TrySave(out errMsg)) + { + user.RaiseError(errMsg); + return false; + } + } + } + return true; + } + + private bool TrySave(out string errMsg) + { + if (EditingValid(out errMsg)) + { + if (!labels.Labels.Contains(currentlyEditing)) + { + labels.Labels = labels.Labels + .Concat(new ModuleLabel[] { currentlyEditing }) + .ToArray(); + } + currentlyEditing.Name = NameTextBox.Text; + currentlyEditing.Color = ColorButton.BackColor; + currentlyEditing.InstanceName = + string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem?.ToString()) + ? null + : InstanceNameComboBox.SelectedItem.ToString(); + currentlyEditing.Hide = HideFromOtherFiltersCheckBox.Checked; + currentlyEditing.NotifyOnChange = NotifyOnChangesCheckBox.Checked; + currentlyEditing.RemoveOnChange = RemoveOnChangesCheckBox.Checked; + currentlyEditing.AlertOnInstall = AlertOnInstallCheckBox.Checked; + currentlyEditing.RemoveOnInstall = RemoveOnInstallCheckBox.Checked; + + EditDetailsPanel.Visible = false; + currentlyEditing = null; + LoadTree(); + return true; + } + return false; + } + + private bool EditingValid(out string errMsg) + { + if (currentlyEditing == null) + { + errMsg = Properties.Resources.EditLabelsDialogNoRecord; + return false; + } + if (string.IsNullOrWhiteSpace(NameTextBox.Text)) + { + errMsg = Properties.Resources.EditLabelsDialogNameRequired; + return false; + } + var newInst = string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem?.ToString()) + ? null + : InstanceNameComboBox.SelectedItem.ToString(); + var found = labels.Labels.FirstOrDefault(l => + l != currentlyEditing + && l.Name == NameTextBox.Text + && (l.InstanceName == newInst + || newInst == null + || l.InstanceName == null) + ); + if (found != null) + { + errMsg = string.Format( + Properties.Resources.EditLabelsDialogAlreadyExists, + NameTextBox.Text, + found.InstanceName ?? Properties.Resources.ModuleLabelListGlobal + ); + return false; + } + errMsg = ""; + return true; + } + + private bool HasChanges() + { + var newInst = string.IsNullOrWhiteSpace(InstanceNameComboBox.SelectedItem?.ToString()) + ? null + : InstanceNameComboBox.SelectedItem.ToString(); + return EditDetailsPanel.Visible && currentlyEditing != null + && ( currentlyEditing.Name != NameTextBox.Text + || currentlyEditing.Color != ColorButton.BackColor + || currentlyEditing.InstanceName != newInst + || currentlyEditing.Hide != HideFromOtherFiltersCheckBox.Checked + || currentlyEditing.NotifyOnChange != NotifyOnChangesCheckBox.Checked + || currentlyEditing.RemoveOnChange != RemoveOnChangesCheckBox.Checked + || currentlyEditing.AlertOnInstall != AlertOnInstallCheckBox.Checked + || currentlyEditing.RemoveOnInstall != RemoveOnInstallCheckBox.Checked + ); + } + + private ModuleLabel currentlyEditing; + + private readonly IUser user; + private readonly ModuleLabelList labels; + + private static readonly ILog log = LogManager.GetLogger(typeof(EditLabelsDialog)); + } +} diff --git a/GUI/EditLabelsDialog.resx b/GUI/EditLabelsDialog.resx new file mode 100644 index 0000000000..63d193f16f --- /dev/null +++ b/GUI/EditLabelsDialog.resx @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Create + Select a label to edit or create a new one + Name: + Background: + Select... + Instance +(blank for all): + Hide from other filters + Notify on updates + Remove on updates + Alert on install + Remove on install + Close + Save + Cancel + Delete + Export... + Edit Labels + diff --git a/GUI/GUIConfiguration.cs b/GUI/GUIConfiguration.cs index 3e759807ef..7691484225 100644 --- a/GUI/GUIConfiguration.cs +++ b/GUI/GUIConfiguration.cs @@ -30,6 +30,16 @@ public class GUIConfiguration public int ActiveFilter = 0; + /// + /// Name of the tag filter the user chose, if any + /// + public string TagFilter = null; + + /// + /// Name of the label filter the user chose, if any + /// + public string CustomLabelFilter = null; + // Sort by the mod name (index = 2) column by default public int SortByColumnIndex = 2; public bool SortDescending = false; diff --git a/GUI/Labels/ModuleLabel.cs b/GUI/Labels/ModuleLabel.cs new file mode 100644 index 0000000000..90fc6d03d5 --- /dev/null +++ b/GUI/Labels/ModuleLabel.cs @@ -0,0 +1,75 @@ +using System; +using System.Linq; +using System.Drawing; +using System.ComponentModel; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace CKAN +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModuleLabel + { + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name; + + [JsonProperty("color", NullValueHandling = NullValueHandling.Ignore)] + public Color Color; + + [JsonProperty("instance_name", NullValueHandling = NullValueHandling.Ignore)] + public string InstanceName; + + [JsonProperty("hide", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(false)] + public bool Hide; + + [JsonProperty("notify_on_change", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(false)] + public bool NotifyOnChange; + + [JsonProperty("remove_on_change", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(false)] + public bool RemoveOnChange; + + [JsonProperty("alert_on_install", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(false)] + public bool AlertOnInstall; + + [JsonProperty("remove_on_install", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + [DefaultValue(false)] + public bool RemoveOnInstall; + + [JsonProperty("module_identifiers", NullValueHandling = NullValueHandling.Ignore)] + public HashSet ModuleIdentifiers = new HashSet(); + + /// + /// Check whether this label is active for a given game instance + /// + /// Name of the instance + /// + /// True if active, false otherwise + /// + public bool AppliesTo(string instanceName) + { + return InstanceName == null || InstanceName == instanceName; + } + + /// + /// Add a module to this label's group + /// + /// The identifier of the module to add + public void Add(string identifier) + { + ModuleIdentifiers.Add(identifier); + } + + /// + /// Remove a module from this label's group + /// + /// The identifier of the module to remove + public void Remove(string identifier) + { + ModuleIdentifiers.Remove(identifier); + } + } +} diff --git a/GUI/Labels/ModuleLabelList.cs b/GUI/Labels/ModuleLabelList.cs new file mode 100644 index 0000000000..b71f23d025 --- /dev/null +++ b/GUI/Labels/ModuleLabelList.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.IO; +using System.Drawing; +using Newtonsoft.Json; + +namespace CKAN +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModuleLabelList + { + [JsonProperty("labels", NullValueHandling = NullValueHandling.Ignore)] + public ModuleLabel[] Labels = new ModuleLabel[] {}; + + public IEnumerable LabelsFor(string instanceName) + { + return Labels.Where(l => l.AppliesTo(instanceName)); + } + + public static readonly string DefaultPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "CKAN", + "labels.json" + ); + + public static ModuleLabelList GetDefaultLabels() + { + return new ModuleLabelList() + { + Labels = new ModuleLabel[] + { + new ModuleLabel() + { + Name = Properties.Resources.ModuleLabelListFavourites, + Color = Color.PaleGreen, + }, + new ModuleLabel() + { + Name = Properties.Resources.ModuleLabelListHidden, + Hide = true, + Color = Color.PaleVioletRed, + }, + } + }; + } + + public static ModuleLabelList Load(string path) + { + try + { + return JsonConvert.DeserializeObject(File.ReadAllText(path)); + } + catch (FileNotFoundException ex) + { + return null; + } + } + + public bool Save(string path) + { + try + { + File.WriteAllText(path, JsonConvert.SerializeObject(this, Formatting.Indented)); + return true; + } + catch + { + return false; + } + } + } +} diff --git a/GUI/Localization/de-DE/EditLabelsDialog.de-DE.resx b/GUI/Localization/de-DE/EditLabelsDialog.de-DE.resx new file mode 100644 index 0000000000..ca4c5adf88 --- /dev/null +++ b/GUI/Localization/de-DE/EditLabelsDialog.de-DE.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Erstellen + Wähle ein Label zum Bearbeiten oder erstelle ein neues + Name: + Hintergrund: + Auswahl... + Instanz +(leer für alle): + In anderen Filtern verstecken + Bei verfügbaren Upgrades benachrichtigen + Label nach Updates entfernen + Vor Installation warnen + Label nach der Installation entfernen + Schließen + Speichern + Abbrechen + Löschen + Labels bearbeiten + diff --git a/GUI/Localization/de-DE/Main.de-DE.resx b/GUI/Localization/de-DE/Main.de-DE.resx index 994ea1d2f0..3e8eea7565 100644 --- a/GUI/Localization/de-DE/Main.de-DE.resx +++ b/GUI/Localization/de-DE/Main.de-DE.resx @@ -195,6 +195,7 @@ Nicht installiert Inkompatibel Alle + Kategorien Zuvor ausgewählte Mod... Danach ausgewählte mod... Auto-installiert @@ -251,4 +252,5 @@ KSP-Verzeichnis öffnen CKAN Einstellungen Beenden + Labels bearbeiten... diff --git a/GUI/Main.Designer.cs b/GUI/Main.Designer.cs index a7193913eb..ff83eceddb 100644 --- a/GUI/Main.Designer.cs +++ b/GUI/Main.Designer.cs @@ -68,6 +68,8 @@ private void InitializeComponent() this.FilterNotInstalledButton = new System.Windows.Forms.ToolStripMenuItem(); this.FilterIncompatibleButton = new System.Windows.Forms.ToolStripMenuItem(); this.FilterAllButton = new System.Windows.Forms.ToolStripMenuItem(); + this.FilterLabelsToolButton = new System.Windows.Forms.ToolStripMenuItem(); + this.FilterTagsToolButton = new System.Windows.Forms.ToolStripMenuItem(); this.NavBackwardToolButton = new System.Windows.Forms.ToolStripMenuItem(); this.NavForwardToolButton = new System.Windows.Forms.ToolStripMenuItem(); this.splitContainer1 = new System.Windows.Forms.SplitContainer(); @@ -87,7 +89,14 @@ private void InitializeComponent() this.DownloadCount = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.Description = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.ModListContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); + this.LabelsContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); this.ModListHeaderContextMenuStrip = new System.Windows.Forms.ContextMenuStrip(this.components); + this.modListToolStripSeparator = new System.Windows.Forms.ToolStripSeparator(); + this.tagFilterToolStripSeparator = new System.Windows.Forms.ToolStripSeparator(); + this.untaggedFilterToolStripSeparator = new System.Windows.Forms.ToolStripSeparator(); + this.labelsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.labelToolStripSeparator = new System.Windows.Forms.ToolStripSeparator(); + this.editLabelsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.reinstallToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.downloadContentsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.purgeContentsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -423,7 +432,11 @@ private void InitializeComponent() this.FilterNewButton, this.FilterNotInstalledButton, this.FilterIncompatibleButton, - this.FilterAllButton}); + this.FilterAllButton, + this.tagFilterToolStripSeparator, + this.FilterTagsToolButton, + this.FilterLabelsToolButton}); + this.FilterToolButton.DropDown.Opening += new System.ComponentModel.CancelEventHandler(FilterToolButton_DropDown_Opening); this.FilterToolButton.Image = global::CKAN.Properties.Resources.filter; this.FilterToolButton.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None; this.FilterToolButton.Name = "FilterToolButton"; @@ -499,6 +512,20 @@ private void InitializeComponent() this.FilterAllButton.Size = new System.Drawing.Size(307, 30); this.FilterAllButton.Click += new System.EventHandler(this.FilterAllButton_Click); resources.ApplyResources(this.FilterAllButton, "FilterAllButton"); + // + // FilterTagsToolButton + // + this.FilterTagsToolButton.Name = "FilterTagsToolButton"; + this.FilterTagsToolButton.Size = new System.Drawing.Size(179, 22); + resources.ApplyResources(this.FilterTagsToolButton, "FilterTagsToolButton"); + this.FilterTagsToolButton.DropDown.Opening += new System.ComponentModel.CancelEventHandler(FilterTagsToolButton_DropDown_Opening); + // + // FilterLabelsToolButton + // + this.FilterLabelsToolButton.Name = "FilterLabelsToolButton"; + this.FilterLabelsToolButton.Size = new System.Drawing.Size(179, 22); + resources.ApplyResources(this.FilterLabelsToolButton, "FilterLabelsToolButton"); + this.FilterLabelsToolButton.DropDown.Opening += new System.ComponentModel.CancelEventHandler(FilterLabelsToolButton_DropDown_Opening); // // NavBackwardToolButton // @@ -698,12 +725,21 @@ private void InitializeComponent() // ModListContextMenuStrip // this.ModListContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.labelsToolStripMenuItem, + this.modListToolStripSeparator, this.reinstallToolStripMenuItem, this.downloadContentsToolStripMenuItem, this.purgeContentsToolStripMenuItem}); this.ModListContextMenuStrip.Name = "ModListContextMenuStrip"; this.ModListContextMenuStrip.Size = new System.Drawing.Size(180, 70); // + // labelsToolStripMenuItem + // + this.labelsToolStripMenuItem.Name = "labelsToolStripMenuItem"; + this.labelsToolStripMenuItem.Size = new System.Drawing.Size(179, 22); + this.labelsToolStripMenuItem.DropDown = this.LabelsContextMenuStrip; + resources.ApplyResources(this.labelsToolStripMenuItem, "labelsToolStripMenuItem"); + // // reinstallToolStripMenuItem // this.reinstallToolStripMenuItem.Name = "reinstallToolStripMenuItem"; @@ -725,6 +761,21 @@ private void InitializeComponent() this.purgeContentsToolStripMenuItem.Click += new System.EventHandler(this.purgeContentsToolStripMenuItem_Click); resources.ApplyResources(this.purgeContentsToolStripMenuItem, "purgeContentsToolStripMenuItem"); // + // LabelsContextMenuStrip + // + this.LabelsContextMenuStrip.Name = "LabelsContextMenuStrip"; + this.LabelsContextMenuStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.editLabelsToolStripMenuItem}); + this.LabelsContextMenuStrip.Size = new System.Drawing.Size(180, 70); + this.LabelsContextMenuStrip.Opening += new System.ComponentModel.CancelEventHandler(LabelsContextMenuStrip_Opening); + // + // editLabelsToolStripMenuItem + // + this.editLabelsToolStripMenuItem.Name = "editLabelsToolStripMenuItem"; + this.editLabelsToolStripMenuItem.Size = new System.Drawing.Size(179, 22); + this.editLabelsToolStripMenuItem.Click += new System.EventHandler(this.editLabelsToolStripMenuItem_Click); + resources.ApplyResources(this.editLabelsToolStripMenuItem, "editLabelsToolStripMenuItem"); + // // ModListHeaderContextMenuStrip // this.ModListHeaderContextMenuStrip.Name = "ModListHeaderContextMenuStrip"; @@ -1425,6 +1476,8 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem FilterNotInstalledButton; private System.Windows.Forms.ToolStripMenuItem FilterIncompatibleButton; private System.Windows.Forms.ToolStripMenuItem FilterAllButton; + private System.Windows.Forms.ToolStripMenuItem FilterLabelsToolButton; + private System.Windows.Forms.ToolStripMenuItem FilterTagsToolButton; private System.Windows.Forms.ToolStripMenuItem NavBackwardToolButton; private System.Windows.Forms.ToolStripMenuItem NavForwardToolButton; private System.Windows.Forms.SplitContainer splitContainer1; @@ -1444,7 +1497,14 @@ private void InitializeComponent() private System.Windows.Forms.DataGridViewTextBoxColumn DownloadCount; private System.Windows.Forms.DataGridViewTextBoxColumn Description; private System.Windows.Forms.ContextMenuStrip ModListContextMenuStrip; + private System.Windows.Forms.ToolStripSeparator modListToolStripSeparator; + private System.Windows.Forms.ToolStripSeparator tagFilterToolStripSeparator; + private System.Windows.Forms.ToolStripSeparator untaggedFilterToolStripSeparator; + private System.Windows.Forms.ContextMenuStrip LabelsContextMenuStrip; private System.Windows.Forms.ContextMenuStrip ModListHeaderContextMenuStrip; + private System.Windows.Forms.ToolStripMenuItem labelsToolStripMenuItem; + private System.Windows.Forms.ToolStripSeparator labelToolStripSeparator; + private System.Windows.Forms.ToolStripMenuItem editLabelsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem reinstallToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem downloadContentsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem purgeContentsToolStripMenuItem; diff --git a/GUI/Main.cs b/GUI/Main.cs index cc8fcb2e1b..f2e51a1fac 100644 --- a/GUI/Main.cs +++ b/GUI/Main.cs @@ -11,6 +11,7 @@ using System.Linq; using CKAN.Versioning; using CKAN.Exporters; +using CKAN.Extensions; using CKAN.Properties; using CKAN.Types; using log4net; @@ -113,7 +114,7 @@ private void ConflictsUpdated(Dictionary prevConflicts) { cell.ToolTipText = null; } - row.DefaultCellStyle.BackColor = Color.Empty; + mainModList.ReapplyLabels(guiMod, false, CurrentInstance.Name); if (row.Visible) { ModList.InvalidateRow(row.Index); @@ -133,7 +134,7 @@ private void ConflictsUpdated(Dictionary prevConflicts) { cell.ToolTipText = conflict_text; } - row.DefaultCellStyle.BackColor = Color.LightCoral; + row.DefaultCellStyle.BackColor = mainModList.GetRowBackground(guiMod, true, CurrentInstance.Name); if (row.Visible) { ModList.InvalidateRow(row.Index); @@ -194,6 +195,9 @@ public Main(string[] cmdlineArgs, KSPManager mgr, GUIUser user, bool showConsole InitializeComponent(); + // React when the user clicks a tag or filter link in mod info + ModInfoTabControl.OnChangeFilter += Filter; + // Replace mono's broken, ugly toolstrip renderer if (Platform.IsMono) { @@ -203,9 +207,12 @@ public Main(string[] cmdlineArgs, KSPManager mgr, GUIUser user, bool showConsole settingsToolStripMenuItem.DropDown.Renderer = new FlatToolStripRenderer(); helpToolStripMenuItem.DropDown.Renderer = new FlatToolStripRenderer(); FilterToolButton.DropDown.Renderer = new FlatToolStripRenderer(); + FilterTagsToolButton.DropDown.Renderer = new FlatToolStripRenderer(); + FilterLabelsToolButton.DropDown.Renderer = new FlatToolStripRenderer(); minimizedContextMenuStrip.Renderer = new FlatToolStripRenderer(); ModListContextMenuStrip.Renderer = new FlatToolStripRenderer(); ModListHeaderContextMenuStrip.Renderer = new FlatToolStripRenderer(); + LabelsContextMenuStrip.Renderer = new FlatToolStripRenderer(); } // Initialize all user interaction dialogs. @@ -536,6 +543,7 @@ protected override void OnFormClosing(FormClosingEventArgs e) // Save the active filter configuration.ActiveFilter = (int)mainModList.ModFilter; + configuration.CustomLabelFilter = mainModList.CustomLabelFilter?.Name; // Save settings. configuration.Save(); @@ -566,7 +574,7 @@ private void CurrentInstanceUpdated(bool allowRepoUpdate) }); configuration = GUIConfiguration.LoadOrCreateConfiguration( - Path.Combine(CurrentInstance.CkanDir (), "GUIConfig.xml") + Path.Combine(CurrentInstance.CkanDir(), "GUIConfig.xml") ); if (CurrentInstance.CompatibleVersionsAreFromDifferentKsp) @@ -575,28 +583,41 @@ private void CurrentInstanceUpdated(bool allowRepoUpdate) .ShowDialog(); } + (RegistryManager.Instance(CurrentInstance).registry as Registry) + ?.BuildTagIndex(mainModList.ModuleTags); + bool repoUpdateNeeded = configuration.RefreshOnStartup || !RegistryManager.Instance(CurrentInstance).registry.HasAnyAvailable(); if (allowRepoUpdate && repoUpdateNeeded) { - ModList.Rows.Clear(); - UpdateRepo(); - // Update the filters after UpdateRepo() completed. // Since this happens with a backgroundworker, Filter() is added as callback for RunWorkerCompleted. // Remove it again after it ran, else it stays there and is added again and again. - void filterUpdate (object sender, RunWorkerCompletedEventArgs e) + void filterUpdate(object sender, RunWorkerCompletedEventArgs e) { - Filter((GUIModFilter)configuration.ActiveFilter); + Filter( + (GUIModFilter)configuration.ActiveFilter, + mainModList.ModuleTags.Tags.GetOrDefault(configuration.TagFilter), + mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .FirstOrDefault(l => l.Name == configuration.CustomLabelFilter) + ); m_UpdateRepoWorker.RunWorkerCompleted -= filterUpdate; } m_UpdateRepoWorker.RunWorkerCompleted += filterUpdate; + + ModList.Rows.Clear(); + UpdateRepo(); } else { UpdateModsList(); - Filter((GUIModFilter)configuration.ActiveFilter); + Filter( + (GUIModFilter)configuration.ActiveFilter, + mainModList.ModuleTags.Tags.GetOrDefault(configuration.TagFilter), + mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .FirstOrDefault(l => l.Name == configuration.CustomLabelFilter) + ); } ChangeSet = null; @@ -852,13 +873,17 @@ private void FilterAllButton_Click(object sender, EventArgs e) /// Called when the modlist filter (all, compatible, incompatible...) is changed. /// /// Filter. - private void Filter(GUIModFilter filter) + private void Filter(GUIModFilter filter, ModuleTag tag = null, ModuleLabel label = null) { // Triggers mainModList.ModFiltersUpdated() + mainModList.TagFilter = tag; + mainModList.CustomLabelFilter = label; mainModList.ModFilter = filter; // Save new filter to the configuration. configuration.ActiveFilter = (int)mainModList.ModFilter; + configuration.CustomLabelFilter = label?.Name; + configuration.TagFilter = tag?.Name; configuration.Save(); // Ask the configuration which columns to show. @@ -888,6 +913,12 @@ private void Filter(GUIModFilter filter) ModList.Columns["InstallDate"].Visible = false; ModList.Columns["AutoInstalled"].Visible = false; FilterToolButton.Text = Properties.Resources.MainFilterNotInstalled; break; + case GUIModFilter.CustomLabel: FilterToolButton.Text = string.Format(Properties.Resources.MainFilterLabel, label?.Name ?? "CUSTOM"); break; + case GUIModFilter.Tag: + FilterToolButton.Text = tag == null + ? Properties.Resources.MainFilterUntagged + : string.Format(Properties.Resources.MainFilterTag, tag.Name); + break; default: FilterToolButton.Text = Properties.Resources.MainFilterCompatible; break; } } diff --git a/GUI/Main.resx b/GUI/Main.resx index 09f3f50b19..5602e52293 100644 --- a/GUI/Main.resx +++ b/GUI/Main.resx @@ -197,6 +197,8 @@ Not installed Incompatible All + Tags + Labels Previous selected mod... Next selected mod... Inst @@ -260,4 +262,6 @@ Open KSP Directory CKAN Settings Quit + Labels + Edit labels... diff --git a/GUI/MainAllModVersions.Designer.cs b/GUI/MainAllModVersions.Designer.cs index 575dde85fe..4f9e85cda7 100644 --- a/GUI/MainAllModVersions.Designer.cs +++ b/GUI/MainAllModVersions.Designer.cs @@ -62,7 +62,7 @@ private void InitializeComponent() // // CompatibleKSPVersion // - this.CompatibleKSPVersion.Width = 248; + this.CompatibleKSPVersion.Width = 160; resources.ApplyResources(this.CompatibleKSPVersion, "CompatibleKSPVersion"); // // VersionsListView diff --git a/GUI/MainChangeset.cs b/GUI/MainChangeset.cs index 8900b25b5e..193ef59e37 100644 --- a/GUI/MainChangeset.cs +++ b/GUI/MainChangeset.cs @@ -1,7 +1,8 @@ -using System; +using System; +using System.Linq; using System.Collections.Generic; +using System.Drawing; using System.ComponentModel; -using System.Linq; using System.Windows.Forms; namespace CKAN @@ -47,14 +48,22 @@ public void UpdateChangesDialog(List changeset, BackgroundWorker inst } CkanModule m = change.Mod; + ModuleLabel warnLbl = FindLabelAlertsBeforeInstall(m); ChangesListView.Items.Add(new ListViewItem(new string[] { change.NameAndStatus, change.ChangeType.ToString(), - change.Description + warnLbl != null + ? string.Format( + Properties.Resources.MainChangesetWarningInstallingModuleWithLabel, + warnLbl.Name, + change.Description + ) + : change.Description }) { - Tag = m + Tag = m, + ForeColor = warnLbl != null ? Color.Red : SystemColors.WindowText }); } } diff --git a/GUI/MainInstall.cs b/GUI/MainInstall.cs index c5ae24b931..e664d8d56a 100644 --- a/GUI/MainInstall.cs +++ b/GUI/MainInstall.cs @@ -317,6 +317,7 @@ private void InstallMods(object sender, DoWorkEventArgs e) private void OnModInstalled(CkanModule mod) { AddStatusMessage(string.Format(Properties.Resources.MainInstallModSuccess, mod.name)); + LabelsAfterInstall(mod); } private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e) diff --git a/GUI/MainLabels.cs b/GUI/MainLabels.cs new file mode 100644 index 0000000000..b797e2c9be --- /dev/null +++ b/GUI/MainLabels.cs @@ -0,0 +1,183 @@ +using System; +using System.Linq; +using System.ComponentModel; +using System.Windows.Forms; +using System.Collections.Generic; + +namespace CKAN +{ + public partial class Main + { + #region Filter dropdown + + private void FilterToolButton_DropDown_Opening(object sender, System.ComponentModel.CancelEventArgs e) + { + // The menu items' dropdowns can't be accessed if they're empty + FilterTagsToolButton_DropDown_Opening(null, null); + FilterLabelsToolButton_DropDown_Opening(null, null); + } + + private void FilterTagsToolButton_DropDown_Opening(object sender, System.ComponentModel.CancelEventArgs e) + { + FilterTagsToolButton.DropDownItems.Clear(); + foreach (var kvp in mainModList.ModuleTags.Tags.OrderBy(kvp => kvp.Key)) + { + FilterTagsToolButton.DropDownItems.Add(new ToolStripMenuItem( + $"{kvp.Key} ({kvp.Value.ModuleIdentifiers.Count})", + null, tagFilterButton_Click + ) + { + Tag = kvp.Value + }); + } + FilterTagsToolButton.DropDownItems.Add(untaggedFilterToolStripSeparator); + FilterTagsToolButton.DropDownItems.Add(new ToolStripMenuItem( + string.Format(Properties.Resources.MainLabelsUntagged, mainModList.ModuleTags.Untagged.Count), + null, tagFilterButton_Click + ) + { + Tag = null + }); + } + + private void FilterLabelsToolButton_DropDown_Opening(object sender, System.ComponentModel.CancelEventArgs e) + { + FilterLabelsToolButton.DropDownItems.Clear(); + foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)) + { + FilterLabelsToolButton.DropDownItems.Add(new ToolStripMenuItem( + $"{mlbl.Name} ({mlbl.ModuleIdentifiers.Count})", + null, customFilterButton_Click + ) + { + Tag = mlbl + }); + } + } + + private void tagFilterButton_Click(object sender, EventArgs e) + { + var clicked = sender as ToolStripMenuItem; + Filter(GUIModFilter.Tag, clicked.Tag as ModuleTag, null); + } + + private void customFilterButton_Click(object sender, EventArgs e) + { + var clicked = sender as ToolStripMenuItem; + Filter(GUIModFilter.CustomLabel, null, clicked.Tag as ModuleLabel); + } + + #endregion + + #region Right click menu + + private void LabelsContextMenuStrip_Opening(object sender, System.ComponentModel.CancelEventArgs e) + { + LabelsContextMenuStrip.Items.Clear(); + + var module = GetSelectedModule(); + foreach (ModuleLabel mlbl in mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name)) + { + LabelsContextMenuStrip.Items.Add( + new ToolStripMenuItem(mlbl.Name, null, labelMenuItem_Click) + { + Checked = mlbl.ModuleIdentifiers.Contains(module.Identifier), + CheckOnClick = true, + Tag = mlbl, + } + ); + } + LabelsContextMenuStrip.Items.Add(labelToolStripSeparator); + LabelsContextMenuStrip.Items.Add(editLabelsToolStripMenuItem); + e.Cancel = false; + } + + private void labelMenuItem_Click(object sender, EventArgs e) + { + var item = sender as ToolStripMenuItem; + var mlbl = item.Tag as ModuleLabel; + var module = GetSelectedModule(); + if (item.Checked) + { + mlbl.Add(module.Identifier); + } + else + { + mlbl.Remove(module.Identifier); + } + mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, CurrentInstance.Name); + mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath); + } + + private void editLabelsToolStripMenuItem_Click(object sender, EventArgs e) + { + EditLabelsDialog eld = new EditLabelsDialog(currentUser, Manager, mainModList.ModuleLabels); + eld.ShowDialog(this); + eld.Dispose(); + mainModList.ModuleLabels.Save(ModuleLabelList.DefaultPath); + foreach (GUIMod module in mainModList.Modules) + { + mainModList.ReapplyLabels(module, Conflicts?.ContainsKey(module) ?? false, CurrentInstance.Name); + } + } + + #endregion + + #region Notifications + + private void LabelsAfterUpdate(IEnumerable mods) + { + Util.Invoke(Main.Instance, () => + { + var notifLabs = mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Where(l => l.NotifyOnChange); + var toNotif = mods + .Where(m => + notifLabs.Any(l => + l.ModuleIdentifiers.Contains(m.Identifier))) + .Select(m => m.Name) + .ToArray(); + if (toNotif.Any()) + { + MessageBox.Show( + string.Format( + Properties.Resources.MainLabelsUpdateMessage, + string.Join("\r\n", toNotif) + ), + Properties.Resources.MainLabelsUpdateTitle, + MessageBoxButtons.OK + ); + } + + foreach (GUIMod mod in mods) + { + foreach (ModuleLabel l in mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Where(l => l.RemoveOnChange + && l.ModuleIdentifiers.Contains(mod.Identifier))) + { + l.Remove(mod.Identifier); + } + + } + }); + } + + private ModuleLabel FindLabelAlertsBeforeInstall(CkanModule mod) + { + return mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Where(l => l.AlertOnInstall) + .FirstOrDefault(l => l.ModuleIdentifiers.Contains(mod.identifier)); + } + + private void LabelsAfterInstall(CkanModule mod) + { + foreach (ModuleLabel l in mainModList.ModuleLabels.LabelsFor(CurrentInstance.Name) + .Where(l => l.RemoveOnInstall && l.ModuleIdentifiers.Contains(mod.identifier))) + { + l.Remove(mod.identifier); + } + } + + #endregion + } +} diff --git a/GUI/MainModInfo.Designer.cs b/GUI/MainModInfo.Designer.cs index 27d25822aa..ea70a989d0 100644 --- a/GUI/MainModInfo.Designer.cs +++ b/GUI/MainModInfo.Designer.cs @@ -35,6 +35,7 @@ private void InitializeComponent() this.splitContainer2 = new System.Windows.Forms.SplitContainer(); this.MetaDataUpperLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); this.MetadataModuleNameTextBox = new TransparentTextBox(); + this.MetadataTagsLabelsPanel = new System.Windows.Forms.FlowLayoutPanel(); this.MetadataModuleAbstractLabel = new System.Windows.Forms.Label(); this.MetadataModuleDescriptionTextBox = new TransparentTextBox(); this.MetaDataLowerLayoutPanel = new System.Windows.Forms.TableLayoutPanel(); @@ -142,13 +143,15 @@ private void InitializeComponent() this.MetaDataUpperLayoutPanel.ColumnCount = 1; this.MetaDataUpperLayoutPanel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); this.MetaDataUpperLayoutPanel.Controls.Add(this.MetadataModuleNameTextBox, 0, 0); - this.MetaDataUpperLayoutPanel.Controls.Add(this.MetadataModuleAbstractLabel, 0, 1); - this.MetaDataUpperLayoutPanel.Controls.Add(this.MetadataModuleDescriptionTextBox, 0, 2); + this.MetaDataUpperLayoutPanel.Controls.Add(this.MetadataTagsLabelsPanel, 0, 1); + this.MetaDataUpperLayoutPanel.Controls.Add(this.MetadataModuleAbstractLabel, 0, 2); + this.MetaDataUpperLayoutPanel.Controls.Add(this.MetadataModuleDescriptionTextBox, 0, 3); this.MetaDataUpperLayoutPanel.Dock = System.Windows.Forms.DockStyle.Fill; this.MetaDataUpperLayoutPanel.Location = new System.Drawing.Point(0, 0); this.MetaDataUpperLayoutPanel.Name = "MetaDataUpperLayoutPanel"; - this.MetaDataUpperLayoutPanel.RowCount = 3; + this.MetaDataUpperLayoutPanel.RowCount = 4; this.MetaDataUpperLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize, 20F)); + this.MetaDataUpperLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 32F)); this.MetaDataUpperLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.AutoSize, 30F)); this.MetaDataUpperLayoutPanel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 80F)); this.MetaDataUpperLayoutPanel.Size = new System.Drawing.Size(346, 283); @@ -168,6 +171,14 @@ private void InitializeComponent() this.MetadataModuleNameTextBox.ForeColor = System.Drawing.SystemColors.ControlText; this.MetadataModuleNameTextBox.BorderStyle = System.Windows.Forms.BorderStyle.None; resources.ApplyResources(this.MetadataModuleNameTextBox, "MetadataModuleNameTextBox"); + // + // MetadataTagsLabelsPanel + // + this.MetadataTagsLabelsPanel.Dock = System.Windows.Forms.DockStyle.Fill; + this.MetadataTagsLabelsPanel.Padding = new System.Windows.Forms.Padding(0); + this.MetadataTagsLabelsPanel.Location = new System.Drawing.Point(0, 0); + this.MetadataTagsLabelsPanel.Name = "MetadataTagsLabelsPanel"; + this.MetadataTagsLabelsPanel.Size = new System.Drawing.Size(340, 12); // // MetadataModuleAbstractLabel // @@ -683,6 +694,7 @@ private void InitializeComponent() private System.Windows.Forms.SplitContainer splitContainer2; private System.Windows.Forms.TableLayoutPanel MetaDataUpperLayoutPanel; private TransparentTextBox MetadataModuleNameTextBox; + private System.Windows.Forms.FlowLayoutPanel MetadataTagsLabelsPanel; private System.Windows.Forms.Label MetadataModuleAbstractLabel; private TransparentTextBox MetadataModuleDescriptionTextBox; private System.Windows.Forms.TableLayoutPanel MetaDataLowerLayoutPanel; diff --git a/GUI/MainModInfo.cs b/GUI/MainModInfo.cs index 973d53d9f4..453d14d2d4 100644 --- a/GUI/MainModInfo.cs +++ b/GUI/MainModInfo.cs @@ -111,7 +111,7 @@ private void ContentsDownloadButton_Click(object sender, EventArgs e) { StartDownload(SelectedModule); } - + private void ContentsOpenButton_Click(object sender, EventArgs e) { Process.Start(manager.Cache.GetCachedFilename(SelectedModule.ToModule())); @@ -127,9 +127,7 @@ private void UpdateModInfo(GUIMod gui_module) CkanModule module = gui_module.ToModule(); Util.Invoke(MetadataModuleNameTextBox, () => MetadataModuleNameTextBox.Text = gui_module.Name); - Util.Invoke(MetadataModuleVersionTextBox, () => MetadataModuleVersionTextBox.Text = gui_module.LatestVersion.ToString()); - Util.Invoke(MetadataModuleLicenseTextBox, () => MetadataModuleLicenseTextBox.Text = string.Join(", ", module.license)); - Util.Invoke(MetadataModuleAuthorTextBox, () => MetadataModuleAuthorTextBox.Text = gui_module.Authors); + UpdateTagsAndLabels(gui_module.ToModule()); Util.Invoke(MetadataModuleAbstractLabel, () => MetadataModuleAbstractLabel.Text = gui_module.Abstract); Util.Invoke(MetadataModuleDescriptionTextBox, () => { @@ -140,6 +138,10 @@ private void UpdateModInfo(GUIMod gui_module) ? ScrollBars.None : ScrollBars.Vertical; }); + + Util.Invoke(MetadataModuleVersionTextBox, () => MetadataModuleVersionTextBox.Text = gui_module.LatestVersion.ToString()); + Util.Invoke(MetadataModuleLicenseTextBox, () => MetadataModuleLicenseTextBox.Text = string.Join(", ", module.license)); + Util.Invoke(MetadataModuleAuthorTextBox, () => MetadataModuleAuthorTextBox.Text = gui_module.Authors); Util.Invoke(MetadataIdentifierTextBox, () => MetadataIdentifierTextBox.Text = gui_module.Identifier); // If we have a homepage provided, use that; otherwise use the spacedock page, curse page or the github repo so that users have somewhere to get more info than just the abstract. @@ -150,6 +152,91 @@ private void UpdateModInfo(GUIMod gui_module) Util.Invoke(ReplacementTextBox, () => ReplacementTextBox.Text = gui_module.ToModule()?.replaced_by?.ToString() ?? Properties.Resources.MainModInfoNSlashA); } + private ModuleLabelList ModuleLabels + { + get + { + return Main.Instance.mainModList.ModuleLabels; + } + } + + private ModuleTagList ModuleTags + { + get + { + return Main.Instance.mainModList.ModuleTags; + } + } + + private void UpdateTagsAndLabels(CkanModule mod) + { + Util.Invoke(MetadataTagsLabelsPanel, () => + { + MetadataTagsLabelsPanel.Controls.Clear(); + var tags = ModuleTags?.Tags + .Where(t => t.Value.ModuleIdentifiers.Contains(mod.identifier)) + .OrderBy(t => t.Key) + .Select(t => t.Value); + if (tags != null) + { + foreach (ModuleTag tag in tags) + { + MetadataTagsLabelsPanel.Controls.Add(TagLabelLink( + tag.Name, tag, new LinkLabelLinkClickedEventHandler(this.TagLinkLabel_LinkClicked) + )); + } + } + var labels = ModuleLabels?.LabelsFor(manager.CurrentInstance.Name) + .Where(l => l.ModuleIdentifiers.Contains(mod.identifier)) + .OrderBy(l => l.Name); + if (labels != null) + { + foreach (ModuleLabel mlbl in labels) + { + MetadataTagsLabelsPanel.Controls.Add(TagLabelLink( + mlbl.Name, mlbl, new LinkLabelLinkClickedEventHandler(this.LabelLinkLabel_LinkClicked) + )); + } + } + }); + } + + private LinkLabel TagLabelLink(string name, object tag, LinkLabelLinkClickedEventHandler onClick) + { + var link = new LinkLabel() + { + AutoSize = true, + LinkColor = SystemColors.GrayText, + LinkBehavior = LinkBehavior.HoverUnderline, + Margin = new Padding(2), + Text = name, + Tag = tag, + }; + link.LinkClicked += onClick; + return link; + } + + public delegate void ChangeFilter(GUIModFilter filter, ModuleTag tag, ModuleLabel label); + public event ChangeFilter OnChangeFilter; + + private void TagLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + var link = sender as LinkLabel; + if (OnChangeFilter != null) + { + OnChangeFilter(GUIModFilter.Tag, link.Tag as ModuleTag, null); + } + } + + private void LabelLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) + { + var link = sender as LinkLabel; + if (OnChangeFilter != null) + { + OnChangeFilter(GUIModFilter.CustomLabel, null, link.Tag as ModuleLabel); + } + } + private void BeforeExpand(object sender, TreeViewCancelEventArgs args) { // Hourglass cursor @@ -453,7 +540,7 @@ public void StartDownload(GUIMod module) Main.Instance.ShowWaitDialog(false); if (cacheWorker.IsBusy) { - Task.Factory.StartNew(() => + Task.Factory.StartNew(() => { // Just pass to the existing worker downloader.DownloadModules(new List { module.ToCkanModule() }); diff --git a/GUI/MainModList.cs b/GUI/MainModList.cs index 62c5723bfb..b37c6de24e 100644 --- a/GUI/MainModList.cs +++ b/GUI/MainModList.cs @@ -25,6 +25,8 @@ public enum GUIModFilter Cached = 7, Replaceable = 8, Uncached = 9, + CustomLabel = 10, + Tag = 11, } public partial class Main @@ -140,7 +142,7 @@ private void _UpdateFilters() foreach (var row in rows) { var mod = ((GUIMod) row.Tag); - row.Visible = mainModList.IsVisible(mod); + row.Visible = mainModList.IsVisible(mod, CurrentInstance.Name); } var sorted = this._SortRowsByColumn(rows.Where(row => row.Visible)); @@ -201,6 +203,7 @@ private void _UpdateModsList(IEnumerable mc, Dictionary ); AddLogMessage(Properties.Resources.MainModListPreservingNew); + var toNotify = new HashSet(); if (old_modules != null) { foreach (GUIMod gm in gui_mods) @@ -211,6 +214,7 @@ private void _UpdateModsList(IEnumerable mc, Dictionary if (!gm.IsIncompatible && oldIncompat) { gm.IsNew = true; + toNotify.Add(gm); } } else @@ -230,13 +234,14 @@ private void _UpdateModsList(IEnumerable mc, Dictionary gui_mod.IsNew = true; } } + LabelsAfterUpdate(toNotify); AddLogMessage(Properties.Resources.MainModListPopulatingList); // Update our mod listing - mainModList.ConstructModList(gui_mods.ToList(), mc, configuration.HideEpochs, configuration.HideV); + mainModList.ConstructModList(gui_mods.ToList(), CurrentInstance.Name, mc, configuration.HideEpochs, configuration.HideV); mainModList.Modules = new ReadOnlyCollection( mainModList.full_list_of_mod_rows.Values.Select(row => row.Tag as GUIMod).ToList()); - + UpdateChangeSetAndConflicts(registry); AddLogMessage(Properties.Resources.MainModListUpdatingFilters); @@ -270,11 +275,13 @@ private void _UpdateModsList(IEnumerable mc, Dictionary UpdateAllToolButton.Enabled = has_any_updates; }); - + + (registry as Registry)?.BuildTagIndex(mainModList.ModuleTags); + UpdateFilters(this); // Hide update and replacement columns if not needed. - // Write it to the configuration, else they are hidden agian after a filter change. + // Write it to the configuration, else they are hidden again after a filter change. // After the update / replacement, they are hidden again. Util.Invoke(ModList, () => { @@ -364,6 +371,7 @@ private void ModList_HeaderMouseClick(object sender, DataGridViewCellMouseEventA // Start from scrap: clear the entire item list, then add all options again. ModListHeaderContextMenuStrip.Items.Clear(); + // Add columns ModListHeaderContextMenuStrip.Items.AddRange( ModList.Columns.Cast() .Where(col => col.Name != "Installed" && col.Name != "UpdateCol" && col.Name != "ReplaceCol") @@ -376,6 +384,22 @@ private void ModList_HeaderMouseClick(object sender, DataGridViewCellMouseEventA }) .ToArray() ); + + // Separator + ModListHeaderContextMenuStrip.Items.Add(new ToolStripSeparator()); + + // Add tags + ModListHeaderContextMenuStrip.Items.AddRange( + mainModList.ModuleTags.Tags.OrderBy(kvp => kvp.Key) + .Select(kvp => new ToolStripMenuItem() + { + Name = kvp.Key, + Text = kvp.Key, + Checked = kvp.Value.Visible, + Tag = kvp.Value, + }) + .ToArray() + ); // Show the context menu on cursor position. ModListHeaderContextMenuStrip.Show(Cursor.Position); @@ -390,6 +414,7 @@ private void ModListHeaderContextMenuStrip_ItemClicked(object sender, System.Win // ClickedItem is of type ToolStripItem, we need ToolStripButton. ToolStripMenuItem clickedItem = e.ClickedItem as ToolStripMenuItem; DataGridViewColumn col = clickedItem?.Tag as DataGridViewColumn; + ModuleTag tag = clickedItem?.Tag as ModuleTag; if (col != null) { @@ -400,6 +425,20 @@ private void ModListHeaderContextMenuStrip_ItemClicked(object sender, System.Win InstallAllCheckbox.Visible = col.Visible; } } + else if (tag != null) + { + tag.Visible = !clickedItem.Checked; + if (tag.Visible) + { + mainModList.ModuleTags.HiddenTags.Remove(tag.Name); + } + else + { + mainModList.ModuleTags.HiddenTags.Add(tag.Name); + } + mainModList.ModuleTags.Save(ModuleTagList.DefaultPath); + UpdateFilters(this); + } } /// @@ -576,7 +615,7 @@ await UpdateChangeSetAndConflicts( } } } - + private void ModList_GotFocus(object sender, EventArgs e) { Util.Invoke(this, () => @@ -695,6 +734,42 @@ public GUIModFilter ModFilter } } + public readonly ModuleLabelList ModuleLabels = ModuleLabelList.Load(ModuleLabelList.DefaultPath) + ?? ModuleLabelList.GetDefaultLabels(); + + public readonly ModuleTagList ModuleTags = ModuleTagList.Load(ModuleTagList.DefaultPath) + ?? new ModuleTagList(); + + private ModuleTag _tagFilter; + public ModuleTag TagFilter + { + get { return _tagFilter; } + set + { + var old = _tagFilter; + _tagFilter = value; + if (!old?.Equals(value) ?? !value?.Equals(old) ?? false) + { + ModFiltersUpdated(this); + } + } + } + + private ModuleLabel _customLabelFilter; + public ModuleLabel CustomLabelFilter + { + get { return _customLabelFilter; } + set + { + var old = _customLabelFilter; + _customLabelFilter = value; + if (!old?.Equals(value) ?? !value?.Equals(old) ?? false) + { + ModFiltersUpdated(this); + } + } + } + public string ModNameFilter { get { return _modNameFilter; } @@ -820,14 +895,36 @@ public IEnumerable ComputeChangeSetFromModList( return changeSet; } - public bool IsVisible(GUIMod mod) + public bool IsVisible(GUIMod mod, string instanceName) { - var nameMatchesFilter = IsNameInNameFilter(mod); - var authorMatchesFilter = IsAuthorInAuthorFilter(mod); - var abstractMatchesFilter = IsAbstractInDescriptionFilter(mod); - var modMatchesType = IsModInFilter(ModFilter, mod); - var isVisible = nameMatchesFilter && modMatchesType && authorMatchesFilter && abstractMatchesFilter; - return isVisible; + return IsNameInNameFilter(mod) + && IsAuthorInAuthorFilter(mod) + && IsAbstractInDescriptionFilter(mod) + && IsModInFilter(ModFilter, TagFilter, CustomLabelFilter, mod) + && !HiddenByTagsOrLabels(ModFilter, TagFilter, CustomLabelFilter, mod, instanceName); + } + + private bool HiddenByTagsOrLabels(GUIModFilter filter, ModuleTag tag, ModuleLabel label, GUIMod m, string instanceName) + { + if (filter != GUIModFilter.CustomLabel) + { + // "Hide" labels apply to all non-custom filters + if (ModuleLabels?.LabelsFor(instanceName) + .Where(l => l != label && l.Hide) + .Any(l => l.ModuleIdentifiers.Contains(m.Identifier)) + ?? false) + { + return true; + } + if (ModuleTags?.Tags?.Values + .Where(t => t != tag && t.Visible == false) + .Any(t => t.ModuleIdentifiers.Contains(m.Identifier)) + ?? false) + { + return true; + } + } + return false; } public int CountModsByFilter(GUIModFilter filter) @@ -837,7 +934,8 @@ public int CountModsByFilter(GUIModFilter filter) // Don't check each one return Modules.Count; } - return Modules.Count(m => IsModInFilter(filter, m)); + // Tags and Labels are not counted here + return Modules.Count(m => IsModInFilter(filter, null, null, m)); } /// @@ -850,21 +948,29 @@ public int CountModsByFilter(GUIModFilter filter) /// If true, strip 'v' prefix from versions /// The mod list public IEnumerable ConstructModList( - IEnumerable modules, IEnumerable mc = null, + IEnumerable modules, string instanceName, IEnumerable mc = null, bool hideEpochs = false, bool hideV = false) { List changes = mc?.ToList(); full_list_of_mod_rows = modules.ToDictionary( gm => gm.Identifier, - gm => MakeRow(gm, changes, hideEpochs, hideV) + gm => MakeRow(gm, changes, instanceName, hideEpochs, hideV) ); return full_list_of_mod_rows.Values; } - private DataGridViewRow MakeRow(GUIMod mod, List changes, bool hideEpochs = false, bool hideV = false) + private DataGridViewRow MakeRow(GUIMod mod, List changes, string instanceName, bool hideEpochs = false, bool hideV = false) { DataGridViewRow item = new DataGridViewRow() {Tag = mod}; + Color? myColor = ModuleLabels.LabelsFor(instanceName) + .FirstOrDefault(l => l.ModuleIdentifiers.Contains(mod.Identifier)) + ?.Color; + if (myColor.HasValue) + { + item.DefaultCellStyle.BackColor = myColor.Value; + } + ModChange myChange = changes?.FindLast((ModChange ch) => ch.Mod.Equals(mod)); var selecting = mod.IsInstallable() @@ -902,7 +1008,7 @@ private DataGridViewRow MakeRow(GUIMod mod, List changes, bool hideEp Value = "-" }; - var replacing = IsModInFilter(GUIModFilter.Replaceable, mod) + var replacing = IsModInFilter(GUIModFilter.Replaceable, null, null, mod) ? (DataGridViewCell) new DataGridViewCheckBoxCell() { Value = myChange == null ? false @@ -952,6 +1058,41 @@ private DataGridViewRow MakeRow(GUIMod mod, List changes, bool hideEp return item; } + + public Color GetRowBackground(GUIMod mod, bool conflicted, string instanceName) + { + if (conflicted) + { + return Color.LightCoral; + } + DataGridViewRow row; + if (full_list_of_mod_rows.TryGetValue(mod.Identifier, out row)) + { + Color? myColor = ModuleLabels.LabelsFor(instanceName) + .FirstOrDefault(l => l.ModuleIdentifiers.Contains(mod.Identifier)) + ?.Color; + if (myColor.HasValue) + { + return myColor.Value; + } + } + return Color.Empty; + } + + /// + /// Update the color and visible state of the given row + /// after it has been added to or removed from a label group + /// + /// The mod that needs an update + public void ReapplyLabels(GUIMod mod, bool conflicted, string instanceName) + { + DataGridViewRow row; + if (full_list_of_mod_rows.TryGetValue(mod.Identifier, out row)) + { + row.DefaultCellStyle.BackColor = GetRowBackground(mod, conflicted, instanceName); + row.Visible = IsVisible(mod, instanceName); + } + } /// /// Returns a version string shorn of any leading epoch as delimited by a single colon @@ -993,7 +1134,7 @@ private bool IsAbstractInDescriptionFilter(GUIMod mod) || mod.SearchableDescription.IndexOf(sanitisedModDescriptionFilter, StringComparison.InvariantCultureIgnoreCase) != -1; } - private static bool IsModInFilter(GUIModFilter filter, GUIMod m) + private bool IsModInFilter(GUIModFilter filter, ModuleTag tag, ModuleLabel label, GUIMod m) { switch (filter) { @@ -1007,6 +1148,9 @@ private static bool IsModInFilter(GUIModFilter filter, GUIMod m) case GUIModFilter.Incompatible: return m.IsIncompatible; case GUIModFilter.Replaceable: return m.IsInstalled && m.HasReplacement; case GUIModFilter.All: return true; + case GUIModFilter.Tag: return tag?.ModuleIdentifiers.Contains(m.Identifier) + ?? ModuleTags.Untagged.Contains(m.Identifier); + case GUIModFilter.CustomLabel: return label?.ModuleIdentifiers?.Contains(m.Identifier) ?? false; default: throw new Kraken(string.Format(Properties.Resources.MainModListUnknownFilter, filter)); } } diff --git a/GUI/Properties/Resources.Designer.cs b/GUI/Properties/Resources.Designer.cs index 86572f7698..d13c72eed8 100644 --- a/GUI/Properties/Resources.Designer.cs +++ b/GUI/Properties/Resources.Designer.cs @@ -388,6 +388,15 @@ internal static string MainFilterNotInstalled { internal static string MainFilterCompatible { get { return (string)(ResourceManager.GetObject("MainFilterCompatible", resourceCulture)); } } + internal static string MainFilterLabel { + get { return (string)(ResourceManager.GetObject("MainFilterLabel", resourceCulture)); } + } + internal static string MainFilterTag { + get { return (string)(ResourceManager.GetObject("MainFilterTag", resourceCulture)); } + } + internal static string MainFilterUntagged { + get { return (string)(ResourceManager.GetObject("MainFilterUntagged", resourceCulture)); } + } internal static string MainLaunchWithIncompatible { get { return (string)(ResourceManager.GetObject("MainLaunchWithIncompatible", resourceCulture)); } } @@ -745,5 +754,77 @@ internal static string UtilCopyLink { internal static string StatusInstanceLabelText { get { return (string)(ResourceManager.GetObject("StatusInstanceLabelText", resourceCulture)); } } + internal static string ModuleLabelListFavourites { + get { return (string)(ResourceManager.GetObject("ModuleLabelListFavourites", resourceCulture)); } + } + internal static string ModuleLabelListHidden { + get { return (string)(ResourceManager.GetObject("ModuleLabelListHidden", resourceCulture)); } + } + internal static string ModuleLabelListGlobal { + get { return (string)(ResourceManager.GetObject("ModuleLabelListGlobal", resourceCulture)); } + } + internal static string EditLabelsDialogConfirmDelete { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogConfirmDelete", resourceCulture)); } + } + internal static string EditLabelsDialogSavePrompt { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogSavePrompt", resourceCulture)); } + } + internal static string EditLabelsDialogNoRecord { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogNoRecord", resourceCulture)); } + } + internal static string EditLabelsDialogNameRequired { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogNameRequired", resourceCulture)); } + } + internal static string EditLabelsDialogAlreadyExists { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogAlreadyExists", resourceCulture)); } + } + internal static string EditLabelsDialogDelete { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogDelete", resourceCulture)); } + } + internal static string EditLabelsDialogCancel { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogCancel", resourceCulture)); } + } + internal static string EditLabelsDialogSave { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogSave", resourceCulture)); } + } + internal static string EditLabelsDialogDiscard { + get { return (string)(ResourceManager.GetObject("EditLabelsDialogDiscard", resourceCulture)); } + } + internal static string MainChangesetWarningInstallingModuleWithLabel { + get { return (string)(ResourceManager.GetObject("MainChangesetWarningInstallingModuleWithLabel", resourceCulture)); } + } + internal static string EditLabelsToolTipName { + get { return (string)(ResourceManager.GetObject("EditLabelsToolTipName", resourceCulture)); } + } + internal static string EditLabelsToolTipColor { + get { return (string)(ResourceManager.GetObject("EditLabelsToolTipColor", resourceCulture)); } + } + internal static string EditLabelsToolTipInstance { + get { return (string)(ResourceManager.GetObject("EditLabelsToolTipInstance", resourceCulture)); } + } + internal static string EditLabelsToolTipHide { + get { return (string)(ResourceManager.GetObject("EditLabelsToolTipHide", resourceCulture)); } + } + internal static string EditLabelsToolTipNotifyOnChanges { + get { return (string)(ResourceManager.GetObject("EditLabelsToolTipNotifyOnChanges", resourceCulture)); } + } + internal static string EditLabelsToolTipRemoveOnChanges { + get { return (string)(ResourceManager.GetObject("EditLabelsToolTipRemoveOnChanges", resourceCulture)); } + } + internal static string EditLabelsToolTipAlertOnInstall { + get { return (string)(ResourceManager.GetObject("EditLabelsToolTipAlertOnInstall", resourceCulture)); } + } + internal static string EditLabelsToolTipRemoveOnInstall { + get { return (string)(ResourceManager.GetObject("EditLabelsToolTipRemoveOnInstall", resourceCulture)); } + } + internal static string MainLabelsUpdateMessage { + get { return (string)(ResourceManager.GetObject("MainLabelsUpdateMessage", resourceCulture)); } + } + internal static string MainLabelsUpdateTitle { + get { return (string)(ResourceManager.GetObject("MainLabelsUpdateTitle", resourceCulture)); } + } + internal static string MainLabelsUntagged { + get { return (string)(ResourceManager.GetObject("MainLabelsUntagged", resourceCulture)); } + } } } diff --git a/GUI/Properties/Resources.de-DE.resx b/GUI/Properties/Resources.de-DE.resx index f2dddfb932..bf9812ec13 100644 --- a/GUI/Properties/Resources.de-DE.resx +++ b/GUI/Properties/Resources.de-DE.resx @@ -162,6 +162,9 @@ Filter (Neu) Filter (Nicht installiert) Filter (Kompatibel) + Label ({0}) + Kategorie ({0}) + Kategorie (Keine) Einige installierte Module sind inkompatibel! Es ist möglicherweise nicht sicher, KSP zu starten. Trotzdem starten? @@ -314,4 +317,30 @@ Wenn du auf Nein klickst, siehst du diese Nachricht nicht mehr. &Linkadresse kopieren Spielinstanz: {0} (KSP {1}) + Favoriten + Versteckt + Global + Möchtest du {0} wirklich löschen? Das kann nicht mehr rückgängig gemacht werden! + Änderungen speichern? + Es wird gerade kein Label bearbeitet! + Name ist erforderlich! + {0} existiert bereits in {1}! + Löschen + Abbrechen + Speichern + Änderungen verwerfen + WARNUNG: INSTALLATION EINES MODULS MIT LABEL {0} ({1}) + Das Label wird im Label-Menü unter diesem Namen angezeigt und muss in einer Instanz eindeutig sein + Module mit diesem Label werden haben diese Hintergrundfarbe in der Modliste + Instanz, für welche dieses Label verfügbar ist. Leerlassen, um es in allen Instanzen verfügbar zu haben + Wenn diese Option aktiviert ist, werden Module mit diesem Label ausgeblendet + Wenn diese Option aktiviert ist, wird eine Benachrichtigung angezeigt, sobald das Modul für deine KSP-Version kompatibel wird + Wenn diese Option aktiviert ist, wird das Label von Modulen entfernt, sobald sie kompatibel werden + Wenn diese Option aktiviert ist, wird eine Warnung angezeigt, wenn das Modul installiert werden soll + Wenn diese Option aktiviert ist, wird das Label von dem Modul entfernt, wenn es installiert wurde + Mods die du beobachtest wurden aktualisiert: + +{0} + Aktualisierungen für beobachtete Mods + Ohne Kategorie ({0}) diff --git a/GUI/Properties/Resources.en-US.resx b/GUI/Properties/Resources.en-US.resx index 86d9ad30bb..68f858fcf9 100644 --- a/GUI/Properties/Resources.en-US.resx +++ b/GUI/Properties/Resources.en-US.resx @@ -119,4 +119,5 @@ CKAN favorites list (*.ckan) + Favorites diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index a937de51d5..bb4d6dc2cf 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -184,6 +184,9 @@ Filter (New) Filter (Not installed) Filter (Compatible) + Label ({0}) + Tag ({0}) + Tag (Untagged) Some installed modules are incompatible! It might not be safe to launch KSP. Really launch? {0} @@ -333,4 +336,30 @@ Can't update automatically, because ckan.exe is read-only or we are not allowed Do you want to allow CKAN to do this? If you click no you won't see this message again. &Copy link address Game instance: {0} (KSP {1}) + Favourites + Hidden + Global + Are you sure you want to delete {0}? This can't be undone! + Save changes? + No record being edited! + Name is required! + {0} already exists in {1}! + Delete + Cancel + Save + Discard + WARNING: INSTALLING MODULE WITH LABEL {0} ({1}) + The label will appear in the Labels menu under this name, must be unique per install + Rows for modules with this label will be drawn with this background color + The instance for which this label is available, leave blank for all + If checked, modules with this label will be hidden from standard filters + If checked, a notification will be displayed if this module changes from incompatible to compatible + If checked, a module that changes from incompatible to compatible will be removed from this label + If checked, the change set screen will alert you if this mod is about to be installed + If checked, modules will be removed from this label if they are installed + Some of your watched mods have updated: + +{0} + Update Notifications + Untagged ({0}) diff --git a/Tests/GUI/GH1866.cs b/Tests/GUI/GH1866.cs index 8b42cec6e3..fe0f8809cf 100644 --- a/Tests/GUI/GH1866.cs +++ b/Tests/GUI/GH1866.cs @@ -118,7 +118,7 @@ public void TestSimple() .ToList(); // varargs method signature means we must call .ToArray() - _listGui.Rows.AddRange(_modList.ConstructModList(modules).ToArray()); + _listGui.Rows.AddRange(_modList.ConstructModList(modules, null).ToArray()); // the header row adds one to the count Assert.AreEqual(modules.Count + 1, _listGui.Rows.Count); diff --git a/Tests/GUI/MainModList.cs b/Tests/GUI/MainModList.cs index 1882b56e18..b283b8ec74 100644 --- a/Tests/GUI/MainModList.cs +++ b/Tests/GUI/MainModList.cs @@ -69,7 +69,10 @@ public void IsVisible_WithAllAndNoNameFilter_ReturnsTrueForCompatible() var registry = Registry.Empty(); registry.AddAvailable(ckan_mod); var item = new MainModList(delegate { }, null); - Assert.That(item.IsVisible(new GUIMod(ckan_mod, registry, manager.CurrentInstance.VersionCriteria()))); + Assert.That(item.IsVisible( + new GUIMod(ckan_mod, registry, manager.CurrentInstance.VersionCriteria()), + manager.CurrentInstance.Name + )); manager.Dispose(); } @@ -102,11 +105,14 @@ public void ConstructModList_NumberOfRows_IsEqualToNumberOfMods() registry.AddAvailable(TestData.FireSpitterModule()); registry.AddAvailable(TestData.kOS_014_module()); var main_mod_list = new MainModList(null, null); - var mod_list = main_mod_list.ConstructModList(new List - { - new GUIMod(TestData.FireSpitterModule(), registry, manager.CurrentInstance.VersionCriteria()), - new GUIMod(TestData.kOS_014_module(), registry, manager.CurrentInstance.VersionCriteria()) - }); + var mod_list = main_mod_list.ConstructModList( + new List + { + new GUIMod(TestData.FireSpitterModule(), registry, manager.CurrentInstance.VersionCriteria()), + new GUIMod(TestData.kOS_014_module(), registry, manager.CurrentInstance.VersionCriteria()) + }, + manager.CurrentInstance.Name + ); Assert.That(mod_list, Has.Count.EqualTo(2)); manager.Dispose();