Skip to content

Commit

Permalink
Merge #3205 Allow sort by multiple columns
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed Nov 23, 2020
2 parents b77482a + 3e6ce5d commit e39d735
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 129 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
- [Multiple] Allow upgrading manually installed modules (#3190 by: HebaruSan; reviewed: DasSkelett)
- [Build] Generate APT repo for releases and under dev builds (#3197, #3201, #3202, #3203, #3208 by: HebaruSan, techman83; reviewed: DasSkelett, HebaruSan)
- [Cmdline] Confirmation prompt for Cmdline upgrades (#3204 by: HebaruSan; reviewed: DasSkelett)
- [GUI] Allow sort by multiple columns (#3205 by: HebaruSan; reviewed: DasSkelett)

### Bugfixes

Expand Down
267 changes: 141 additions & 126 deletions GUI/Controls/ManageMods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ public ManageMods()
private Dictionary<GUIMod, string> conflicts;

public readonly ModList mainModList;
private List<string> sortColumns
{
get
{
return Main.Instance.configuration.SortColumns;
}
}

private List<bool> descending
{
get
{
return Main.Instance.configuration.MultiSortDescending;
}
}

public event Action<GUIMod> OnSelectedModuleChanged;
public event Action<IEnumerable<ModChange>> OnChangeSetChanged;
Expand Down Expand Up @@ -392,7 +407,8 @@ public void MarkAllUpdates()
// only sort by Update column if checkbox in settings checked
if (Main.Instance.configuration.AutoSortByUpdate)
{
SetSortColumn(UpdateCol, false);
SetSort(UpdateCol);
UpdateFilters();
// Select the top row and scroll the list to it.
if (ModGrid.Rows.Count > 0)
{
Expand Down Expand Up @@ -459,20 +475,6 @@ private void ModList_SelectedIndexChanged(object sender, EventArgs e)
}
}

private void SetSortColumn(DataGridViewColumn col, bool? descending = null)
{
var prevSortCol = ModGrid.Columns[Main.Instance.configuration.SortByColumnIndex];

// Reverse the sort order if the current sorting column is clicked again.
Main.Instance.configuration.SortDescending = descending
?? col == prevSortCol && !Main.Instance.configuration.SortDescending;

// Reset the glyph.
prevSortCol.HeaderCell.SortGlyphDirection = SortOrder.None;
Main.Instance.configuration.SortByColumnIndex = col.Index;
UpdateFilters();
}

/// <summary>
/// Called when there's a click on the ModGrid header row.
/// Handles sorting and the header right click context menu.
Expand All @@ -482,7 +484,15 @@ private void ModList_HeaderMouseClick(object sender, DataGridViewCellMouseEventA
// Left click -> sort by new column / change sorting direction.
if (e.Button == MouseButtons.Left)
{
SetSortColumn(ModGrid.Columns[e.ColumnIndex]);
if ((Control.ModifierKeys & Keys.Shift) == Keys.Shift)
{
AddSort(ModGrid.Columns[e.ColumnIndex]);
}
else
{
SetSort(ModGrid.Columns[e.ColumnIndex]);
}
UpdateFilters();
}
// Right click -> Bring up context menu to change visibility of columns.
else if (e.Button == MouseButtons.Right)
Expand Down Expand Up @@ -973,9 +983,8 @@ private void _UpdateFilters()
row.Visible = mainModList.IsVisible(mod, Main.Instance.CurrentInstance.Name);
}

var sorted = this._SortRowsByColumn(rows.Where(row => row.Visible));

ModGrid.Rows.AddRange(sorted.ToArray());
ApplyHeaderGlyphs();
ModGrid.Rows.AddRange(Sort(rows.Where(row => row.Visible)).ToArray());

// Find and select the previously selected row
if (selected_mod != null)
Expand Down Expand Up @@ -1154,110 +1163,141 @@ private void ModList_CurrentCellDirtyStateChanged(object sender, EventArgs e)
ModList_CellContentClick(sender, null);
}

private IEnumerable<DataGridViewRow> _SortRowsByColumn(IEnumerable<DataGridViewRow> rows)
private void SetSort(DataGridViewColumn col)
{
switch (Main.Instance.configuration.SortByColumnIndex)
if (sortColumns.Count == 1 && sortColumns[0] == col.Name)
{
descending[0] = !descending[0];
}
else
{
// XXX: There should be a better way to identify checkbox columns than hardcoding their indices here
case 0: case 1:
case 2: case 3: return Sort(rows, CheckboxSorter);
case 8: return Sort(rows, KSPCompatComparison);
case 9: return Sort(rows, DownloadSizeSorter);
case 10: return Sort(rows, ReleaseDateSorter);
case 11: return Sort(rows, InstallDateSorter);
case 12: return Sort(rows, r => (r.Tag as GUIMod)?.DownloadCount ?? 0);
sortColumns.Clear();
descending.Clear();
AddSort(col);
}
return Sort(rows, DefaultSorter);
}

private IEnumerable<DataGridViewRow> Sort<T>(IEnumerable<DataGridViewRow> rows, Func<DataGridViewRow, T> sortFunction)
private void AddSort(DataGridViewColumn col)
{
var get_row_mod_name = new Func<DataGridViewRow, string>(row => ((GUIMod)row.Tag).Name);
DataGridViewColumnHeaderCell header =
ModGrid.Columns[Main.Instance.configuration.SortByColumnIndex].HeaderCell;

// The columns will be sorted by mod name in addition to whatever the current sorting column is
if (Main.Instance.configuration.SortDescending)
if (sortColumns.Count > 0 && sortColumns[sortColumns.Count - 1] == col.Name)
{
header.SortGlyphDirection = SortOrder.Descending;
return rows.OrderByDescending(sortFunction).ThenBy(get_row_mod_name);
descending[descending.Count - 1] = !descending[descending.Count - 1];
}
else
{
int middlePosition = sortColumns.IndexOf(col.Name);
if (middlePosition > -1)
{
sortColumns.RemoveAt(middlePosition);
descending.RemoveAt(middlePosition);
}
sortColumns.Add(col.Name);
descending.Add(false);
}

header.SortGlyphDirection = SortOrder.Ascending;
return rows.OrderBy(sortFunction).ThenBy(get_row_mod_name);
}

private IEnumerable<DataGridViewRow> Sort(IEnumerable<DataGridViewRow> rows, Comparison<DataGridViewRow> comparison)
private IEnumerable<DataGridViewRow> Sort(IEnumerable<DataGridViewRow> rows)
{
DataGridViewColumnHeaderCell header =
ModGrid.Columns[Main.Instance.configuration.SortByColumnIndex].HeaderCell;

var descending = Main.Instance.configuration.SortDescending;
var newRows = rows.ToList();
header.SortGlyphDirection = descending
? SortOrder.Descending
: SortOrder.Ascending;
// The columns will be sorted by mod name in addition to whatever the current sorting column is
newRows.Sort(CompareThenByName(comparison, descending));
return newRows;
var sorted = rows.ToList();
sorted.Sort(CompareRows);
return sorted;
}

/// <summary>
/// Compare two rows, first by an arbitrary comparison, then by name
/// </summary>
/// <param name="comparison">First comparison to check</param>
/// <param name="descending">true to reverse the comparison, false to leave as-is</param>
/// <returns>
/// Wrapper around comparison parameter that falls back to checking name if equal
/// </returns>
private Comparison<DataGridViewRow> CompareThenByName(Comparison<DataGridViewRow> comparison, bool descending = false)

private void ApplyHeaderGlyphs()
{
// If we check descending inside the lambda, it has to be checked for every row,
// which would be slightly slower. This way we build just the logic we need.
return descending
? (Comparison<DataGridViewRow>)((DataGridViewRow a, DataGridViewRow b) =>
{
int result = comparison(a, b);
return result != 0 ? -result
: ((GUIMod)a.Tag).Name.CompareTo(((GUIMod)b.Tag).Name);
})
: (DataGridViewRow a, DataGridViewRow b) =>
{
int result = comparison(a, b);
return result != 0 ? result
: ((GUIMod)a.Tag).Name.CompareTo(((GUIMod)b.Tag).Name);
};
foreach (DataGridViewColumn col in ModGrid.Columns)
{
col.HeaderCell.SortGlyphDirection = SortOrder.None;
}
for (int i = 0; i < sortColumns.Count; ++i)
{
ModGrid.Columns[sortColumns[i]].HeaderCell.SortGlyphDirection = descending[i]
? SortOrder.Descending : SortOrder.Ascending;
}
}

/// <summary>
/// Transforms a DataGridViewRow's into a generic value suitable for sorting.
/// Uses this.m_Configuration.SortByColumnIndex to determine which
/// field to sort on.
/// </summary>
private string DefaultSorter(DataGridViewRow row)
private int CompareRows(DataGridViewRow a, DataGridViewRow b)
{
// changed so that it never returns null
var cellVal = row.Cells[Main.Instance.configuration.SortByColumnIndex].Value as string;
return string.IsNullOrWhiteSpace(cellVal) ? string.Empty : cellVal;
for (int i = 0; i < sortColumns.Count; ++i)
{
var val = CompareColumn(a, b, ModGrid.Columns[sortColumns[i]]);
if (val != 0)
{
return descending[i] ? -val : val;
}
}
return CompareColumn(a, b, ModName);
}

/// <summary>
/// Transforms a DataGridViewRow's checkbox status into a value suitable for sorting.
/// Uses this.m_Configuration.SortByColumnIndex to determine which
/// field to sort on.
/// </summary>
private string CheckboxSorter(DataGridViewRow row)
private int CompareColumn(DataGridViewRow a, DataGridViewRow b, DataGridViewColumn col)
{
var cell = row.Cells[Main.Instance.configuration.SortByColumnIndex];
if (cell.ValueType == typeof(bool))
GUIMod gmodA = a.Tag as GUIMod;
GUIMod gmodB = b.Tag as GUIMod;
CkanModule modA = gmodA.ToModule();
CkanModule modB = gmodB.ToModule();
var cellA = a.Cells[col.Index];
var cellB = b.Cells[col.Index];
if (col is DataGridViewCheckBoxColumn cbcol)
{
return (bool)cell.Value ? "a" : "c";
if (cellA is DataGridViewCheckBoxCell checkboxA)
{
return cellB is DataGridViewCheckBoxCell checkboxB
? -((bool)checkboxA.Value).CompareTo((bool)checkboxB.Value)
: -1;
}
else
{
return cellB is DataGridViewCheckBoxCell ? 1: 0;
}
}
else
{
// If it's a "-" cell, let it be ordered last
// Otherwise put it after the checked boxes
return (string)cell.Value == "-" ? "d" : "b";
switch (col.Name)
{
case "ModName":
return gmodA.Name.CompareTo(gmodB.Name);
case "KSPCompatibility":
return KSPCompatComparison(a, b);
case "InstallDate":
if (gmodA.InstallDate.HasValue)
{
return gmodB.InstallDate.HasValue
? gmodA.InstallDate.Value.CompareTo(gmodB.InstallDate.Value)
: 1;
}
else
{
return gmodB.InstallDate.HasValue ? -1 : 0;
}
case "ReleaseDate":
if (modA.release_date.HasValue)
{
return modB.release_date.HasValue
? modA.release_date.Value.CompareTo(modB.release_date.Value)
: 1;
}
else
{
return modB.release_date.HasValue ? -1 : 0;
}
case "SizeCol":
return modA.download_size.CompareTo(modB.download_size);
case "DownloadCount":
if (gmodA.DownloadCount.HasValue)
{
return gmodB.DownloadCount.HasValue
? gmodA.DownloadCount.Value.CompareTo(gmodB.DownloadCount.Value)
: 1;
}
else
{
return gmodB.DownloadCount.HasValue ? -1 : 0;
}
default:
var valA = cellA.Value as string ?? "";
var valB = cellB.Value as string ?? "";
return valA.CompareTo(valB);
}
}
}

Expand Down Expand Up @@ -1305,7 +1345,7 @@ private int KSPCompatComparison(DataGridViewRow a, DataGridViewRow b)
}
}
}

/// <summary>
/// Compare pieces of two versions, each of which may be undefined,
/// sorting undefined toward the end.
Expand All @@ -1323,31 +1363,6 @@ private int VersionPieceCompare(bool definedA, int valA, bool definedB, int valB
? (definedB ? valA.CompareTo(valB) : -1)
: (definedB ? 1 : 0);
}

/// <summary>
/// Transforms a DataGridViewRow into a long representing the download size,
/// suitable for sorting.
/// </summary>
private long DownloadSizeSorter(DataGridViewRow row)
{
return (row.Tag as GUIMod)?.ToCkanModule()?.download_size ?? 0;
}

private long ReleaseDateSorter(DataGridViewRow row)
{
return -(row.Tag as GUIMod)?.ToModule().release_date?.Ticks ?? 0;
}

/// <summary>
/// Transforms a DataGridViewRow into a long representing the install date,
/// suitable for sorting.
/// The grid's default on first click is ascending, and sorting uninstalled mods to
/// the top is kind of useless, so we'll make this negative so ascending is useful.
/// </summary>
private long InstallDateSorter(DataGridViewRow row)
{
return -(row.Tag as GUIMod)?.InstallDate?.Ticks ?? 0;
}

public void ResetFilterAndSelectModOnList(string key)
{
Expand Down
5 changes: 2 additions & 3 deletions GUI/Model/GUIConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,8 @@ public class GUIConfiguration
/// </summary>
public string CustomLabelFilter = null;

// Sort by the mod name (index = 2) column by default
public int SortByColumnIndex = 2;
public bool SortDescending = false;
public List<string> SortColumns = new List<string>();
public List<bool> MultiSortDescending = new List<bool>();

[XmlArray, XmlArrayItem(ElementName = "ColumnName")]
public List<string> HiddenColumnNames = new List<string>();
Expand Down

0 comments on commit e39d735

Please sign in to comment.