diff --git a/AndroidSideloader.csproj b/AndroidSideloader.csproj
index f2de4fd..0fe2ebb 100644
--- a/AndroidSideloader.csproj
+++ b/AndroidSideloader.csproj
@@ -190,6 +190,7 @@
Component
+
Component
diff --git a/ColumnSort.cs b/ColumnSort.cs
index 9ba5922..acb6cd6 100644
--- a/ColumnSort.cs
+++ b/ColumnSort.cs
@@ -1,4 +1,5 @@
-using System.Collections;
+using System;
+using System.Collections;
using System.Windows.Forms;
///
@@ -41,23 +42,41 @@ public class ListViewColumnSorter : IComparer
ListViewItem listviewX = (ListViewItem)x;
ListViewItem listviewY = (ListViewItem)y;
- // Determine if the column requires numeric comparison
- if (SortColumn == 3 || SortColumn == 5) // Numeric columns: VersionCodeIndex, VersionNameIndex
+ // Special handling for column 6 (Popularity ranking)
+ if (SortColumn == 6)
{
- try
- {
- // Parse and compare numeric values directly
- int xNum = ParseNumber(listviewX.SubItems[SortColumn].Text);
- int yNum = ParseNumber(listviewY.SubItems[SortColumn].Text);
+ string textX = listviewX.SubItems[SortColumn].Text;
+ string textY = listviewY.SubItems[SortColumn].Text;
- // Compare numerically
- compareResult = xNum.CompareTo(yNum);
- }
- catch
+ // Extract numeric values from "#1", "#10", etc.
+ // "-" represents unranked and should go to the end
+ int rankX = int.MaxValue; // Default for unranked (-)
+ int rankY = int.MaxValue;
+
+ if (textX.StartsWith("#") && int.TryParse(textX.Substring(1), out int parsedX))
{
- // Fallback to string comparison if parsing fails
- compareResult = ObjectCompare.Compare(listviewX.SubItems[SortColumn].Text, listviewY.SubItems[SortColumn].Text);
+ rankX = parsedX;
}
+
+ if (textY.StartsWith("#") && int.TryParse(textY.Substring(1), out int parsedY))
+ {
+ rankY = parsedY;
+ }
+
+ // Compare the numeric ranks
+ compareResult = rankX.CompareTo(rankY);
+ }
+ // Special handling for column 5 (Size)
+ else if (SortColumn == 5)
+ {
+ string textX = listviewX.SubItems[SortColumn].Text;
+ string textY = listviewY.SubItems[SortColumn].Text;
+
+ double sizeX = ParseSize(textX);
+ double sizeY = ParseSize(textY);
+
+ // Compare the numeric sizes
+ compareResult = sizeX.CompareTo(sizeY);
}
else
{
@@ -91,6 +110,49 @@ public class ListViewColumnSorter : IComparer
return int.TryParse(text, out int result) ? result : 0;
}
+ ///
+ /// Parses size strings with units (GB/MB) and converts them to MB for comparison.
+ ///
+ /// Size string (e.g., "1.23 GB", "123 MB")
+ /// Size in MB as a double
+ private double ParseSize(string sizeStr)
+ {
+ if (string.IsNullOrEmpty(sizeStr))
+ return 0;
+
+ // Remove whitespace
+ sizeStr = sizeStr.Trim();
+
+ // Handle new format: "1.23 GB" or "123 MB"
+ if (sizeStr.EndsWith(" GB", StringComparison.OrdinalIgnoreCase))
+ {
+ string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
+ if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out double gb))
+ {
+ return gb * 1024.0; // Convert GB to MB for consistent sorting
+ }
+ }
+ else if (sizeStr.EndsWith(" MB", StringComparison.OrdinalIgnoreCase))
+ {
+ string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
+ if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out double mb))
+ {
+ return mb;
+ }
+ }
+
+ // Fallback: try parsing as raw number
+ if (double.TryParse(sizeStr, System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out double rawMb))
+ {
+ return rawMb;
+ }
+
+ return 0;
+ }
+
///
/// Gets or sets the index of the column to be sorted (default is '0').
///
diff --git a/GalleryView.cs b/GalleryView.cs
index a029266..b78b154 100644
--- a/GalleryView.cs
+++ b/GalleryView.cs
@@ -6,7 +6,9 @@ using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Windows.Forms;
+using static System.Windows.Forms.AxHost;
public enum SortField { Name, LastUpdated, Size, Popularity }
public enum SortDirection { Ascending, Descending }
@@ -90,6 +92,24 @@ public class FastGalleryPanel : Control
public event EventHandler TileRightClicked;
public event EventHandler SortChanged;
+ [DllImport("dwmapi.dll")]
+ private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);
+
+ [DllImport("uxtheme.dll", CharSet = CharSet.Unicode)]
+ private static extern int SetWindowTheme(IntPtr hwnd, string pszSubAppName, string pszSubIdList);
+
+ private void ApplyModernScrollbars()
+ {
+ if (_scrollBar == null || !_scrollBar.IsHandleCreated) return;
+
+ int dark = 1;
+ int hr = DwmSetWindowAttribute(_scrollBar.Handle, 20, ref dark, sizeof(int));
+ if (hr != 0)
+ DwmSetWindowAttribute(_scrollBar.Handle, 19, ref dark, sizeof(int));
+
+ if (SetWindowTheme(_scrollBar.Handle, "DarkMode_Explorer", null) != 0)
+ SetWindowTheme(_scrollBar.Handle, "Explorer", null);
+ }
private class TileAnimationState
{
public float Scale = 1.0f;
@@ -155,6 +175,8 @@ public class FastGalleryPanel : Control
_isScrolling = false;
Invalidate();
};
+
+ _scrollBar.HandleCreated += (s, e) => ApplyModernScrollbars();
Controls.Add(_scrollBar);
// Animation timer (~120fps)
@@ -353,10 +375,10 @@ public class FastGalleryPanel : Control
case SortField.Popularity:
if (_currentSortDirection == SortDirection.Ascending)
- _items = _items.OrderBy(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "0"))
+ _items = _items.OrderByDescending(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "-"))
.ThenBy(i => i.Text, new GameNameComparer()).ToList();
else
- _items = _items.OrderByDescending(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "0"))
+ _items = _items.OrderBy(i => ParsePopularity(i.SubItems.Count > 6 ? i.SubItems[6].Text : "-"))
.ThenBy(i => i.Text, new GameNameComparer()).ToList();
break;
}
@@ -378,12 +400,35 @@ public class FastGalleryPanel : Control
Invalidate();
}
- private double ParsePopularity(string popStr)
+ private int ParsePopularity(string popStr)
{
- if (double.TryParse(popStr?.Trim(), System.Globalization.NumberStyles.Any,
- System.Globalization.CultureInfo.InvariantCulture, out double pop))
- return pop;
- return 0;
+ if (string.IsNullOrEmpty(popStr))
+ return int.MaxValue; // Unranked goes to end
+
+ popStr = popStr.Trim();
+
+ // Handle new format: "#123" or "-"
+ if (popStr == "-")
+ {
+ return int.MaxValue; // Unranked items sort to the end
+ }
+
+ if (popStr.StartsWith("#"))
+ {
+ string numPart = popStr.Substring(1);
+ if (int.TryParse(numPart, out int rank))
+ {
+ return rank;
+ }
+ }
+
+ // Fallback: try parsing as raw number
+ if (int.TryParse(popStr, out int rawNum))
+ {
+ return rawNum;
+ }
+
+ return int.MaxValue; // Unparseable goes to end
}
// Custom sort to match list sort behaviour: '_' before digits, digits before letters (case-insensitive)
@@ -445,9 +490,39 @@ public class FastGalleryPanel : Control
private double ParseSize(string sizeStr)
{
- if (double.TryParse(sizeStr?.Trim(), System.Globalization.NumberStyles.Any,
- System.Globalization.CultureInfo.InvariantCulture, out double mb))
- return mb;
+ if (string.IsNullOrEmpty(sizeStr))
+ return 0;
+
+ // Remove whitespace
+ sizeStr = sizeStr.Trim();
+
+ // Handle new format: "1.23 GB" or "123 MB"
+ if (sizeStr.EndsWith(" GB", StringComparison.OrdinalIgnoreCase))
+ {
+ string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
+ if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out double gb))
+ {
+ return gb * 1024.0; // Convert GB to MB for consistent sorting
+ }
+ }
+ else if (sizeStr.EndsWith(" MB", StringComparison.OrdinalIgnoreCase))
+ {
+ string numPart = sizeStr.Substring(0, sizeStr.Length - 3).Trim();
+ if (double.TryParse(numPart, System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out double mb))
+ {
+ return mb;
+ }
+ }
+
+ // Fallback: try parsing as raw number
+ if (double.TryParse(sizeStr, System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out double rawMb))
+ {
+ return rawMb;
+ }
+
return 0;
}
@@ -899,7 +974,7 @@ public class FastGalleryPanel : Control
// Size badge (top right) - always visible
if (item.SubItems.Count > 5)
{
- string sizeText = FormatSize(item.SubItems[5].Text);
+ string sizeText = item.SubItems[5].Text;
if (!string.IsNullOrEmpty(sizeText))
{
DrawRightAlignedBadge(g, sizeText, x + scaledW - thumbPadding - 4, rightBadgeY, 1.0f);
diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs
index d42ea2d..8ec8f9c 100644
--- a/MainForm.Designer.cs
+++ b/MainForm.Designer.cs
@@ -91,6 +91,7 @@ namespace AndroidSideloader
this.speedLabel_Tooltip = new System.Windows.Forms.ToolTip(this.components);
this.etaLabel_Tooltip = new System.Windows.Forms.ToolTip(this.components);
this.progressDLbtnContainer = new System.Windows.Forms.Panel();
+ this.progressBar = new AndroidSideloader.ModernProgressBar();
this.diskLabel = new System.Windows.Forms.Label();
this.questStorageProgressBar = new System.Windows.Forms.Panel();
this.batteryLevImg = new System.Windows.Forms.PictureBox();
@@ -119,14 +120,9 @@ namespace AndroidSideloader
this.deviceIdLabel = new System.Windows.Forms.Label();
this.rookieStatusLabel = new System.Windows.Forms.Label();
this.sidebarMediaPanel = new System.Windows.Forms.Panel();
+ this.downloadInstallGameButton = new AndroidSideloader.RoundButton();
this.selectedGameLabel = new System.Windows.Forms.Label();
this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
- this.webView21 = new Microsoft.Web.WebView2.WinForms.WebView2();
- this.favoriteGame = new System.Windows.Forms.ContextMenuStrip(this.components);
- this.favoriteButton = new System.Windows.Forms.ToolStripMenuItem();
- this.gamesGalleryView = new System.Windows.Forms.FlowLayoutPanel();
- this.btnViewToggle_Tooltip = new System.Windows.Forms.ToolTip(this.components);
- this.webViewPlaceholderPanel = new System.Windows.Forms.Panel();
this.searchPanel = new AndroidSideloader.RoundButton();
this.searchIconPictureBox = new System.Windows.Forms.PictureBox();
this.searchTextBox = new System.Windows.Forms.TextBox();
@@ -135,8 +131,12 @@ namespace AndroidSideloader
this.btnInstalled = new AndroidSideloader.RoundButton();
this.btnUpdateAvailable = new AndroidSideloader.RoundButton();
this.btnNewerThanList = new AndroidSideloader.RoundButton();
- this.progressBar = new AndroidSideloader.ModernProgressBar();
- this.downloadInstallGameButton = new AndroidSideloader.RoundButton();
+ this.webView21 = new Microsoft.Web.WebView2.WinForms.WebView2();
+ this.favoriteGame = new System.Windows.Forms.ContextMenuStrip(this.components);
+ this.favoriteButton = new System.Windows.Forms.ToolStripMenuItem();
+ this.gamesGalleryView = new System.Windows.Forms.FlowLayoutPanel();
+ this.btnViewToggle_Tooltip = new System.Windows.Forms.ToolTip(this.components);
+ this.webViewPlaceholderPanel = new System.Windows.Forms.Panel();
((System.ComponentModel.ISupportInitialize)(this.gamesPictureBox)).BeginInit();
this.gamesPictureBox.SuspendLayout();
this.progressDLbtnContainer.SuspendLayout();
@@ -152,10 +152,10 @@ namespace AndroidSideloader
this.statusInfoPanel.SuspendLayout();
this.sidebarMediaPanel.SuspendLayout();
this.tableLayoutPanel1.SuspendLayout();
- ((System.ComponentModel.ISupportInitialize)(this.webView21)).BeginInit();
- this.favoriteGame.SuspendLayout();
this.searchPanel.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).BeginInit();
+ ((System.ComponentModel.ISupportInitialize)(this.webView21)).BeginInit();
+ this.favoriteGame.SuspendLayout();
this.SuspendLayout();
//
// m_combo
@@ -166,7 +166,7 @@ namespace AndroidSideloader
this.m_combo.ForeColor = global::AndroidSideloader.Properties.Settings.Default.FontColor;
this.m_combo.Location = new System.Drawing.Point(253, 9);
this.m_combo.Name = "m_combo";
- this.m_combo.Size = new System.Drawing.Size(374, 24);
+ this.m_combo.Size = new System.Drawing.Size(374, 25);
this.m_combo.TabIndex = 0;
this.m_combo.Text = "Select an Installed App...";
this.m_combo.Visible = false;
@@ -230,7 +230,7 @@ namespace AndroidSideloader
this.devicesComboBox.Location = new System.Drawing.Point(253, 39);
this.devicesComboBox.Margin = new System.Windows.Forms.Padding(2);
this.devicesComboBox.Name = "devicesComboBox";
- this.devicesComboBox.Size = new System.Drawing.Size(164, 24);
+ this.devicesComboBox.Size = new System.Drawing.Size(164, 25);
this.devicesComboBox.TabIndex = 1;
this.devicesComboBox.Text = "Select your device";
this.devicesComboBox.Visible = false;
@@ -246,7 +246,7 @@ namespace AndroidSideloader
this.remotesList.Location = new System.Drawing.Point(567, 40);
this.remotesList.Margin = new System.Windows.Forms.Padding(2);
this.remotesList.Name = "remotesList";
- this.remotesList.Size = new System.Drawing.Size(67, 24);
+ this.remotesList.Size = new System.Drawing.Size(67, 25);
this.remotesList.TabIndex = 3;
this.remotesList.Visible = false;
this.remotesList.SelectedIndexChanged += new System.EventHandler(this.remotesList_SelectedIndexChanged);
@@ -268,10 +268,12 @@ namespace AndroidSideloader
this.DownloadsIndex});
this.gamesListView.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.gamesListView.ForeColor = System.Drawing.Color.White;
+ this.gamesListView.FullRowSelect = true;
this.gamesListView.HideSelection = false;
this.gamesListView.ImeMode = System.Windows.Forms.ImeMode.Off;
this.gamesListView.Location = new System.Drawing.Point(258, 44);
this.gamesListView.Name = "gamesListView";
+ this.gamesListView.OwnerDraw = true;
this.gamesListView.ShowGroups = false;
this.gamesListView.Size = new System.Drawing.Size(984, 409);
this.gamesListView.TabIndex = 6;
@@ -287,7 +289,7 @@ namespace AndroidSideloader
// GameNameIndex
//
this.GameNameIndex.Text = "Game Name";
- this.GameNameIndex.Width = 158;
+ this.GameNameIndex.Width = 160;
//
// ReleaseNameIndex
//
@@ -297,29 +299,31 @@ namespace AndroidSideloader
// PackageNameIndex
//
this.PackageNameIndex.Text = "Package Name";
- this.PackageNameIndex.Width = 140;
+ this.PackageNameIndex.Width = 120;
//
// VersionCodeIndex
//
- this.VersionCodeIndex.Text = "Version (vs Installed)";
- this.VersionCodeIndex.Width = 140;
+ this.VersionCodeIndex.Text = "Version (Rookie/Local)";
+ this.VersionCodeIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.VersionCodeIndex.Width = 164;
//
// ReleaseAPKPathIndex
//
this.ReleaseAPKPathIndex.Text = "Last Updated";
- this.ReleaseAPKPathIndex.Width = 120;
+ this.ReleaseAPKPathIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.ReleaseAPKPathIndex.Width = 135;
//
// VersionNameIndex
//
- this.VersionNameIndex.Text = "Size (MB)";
- this.VersionNameIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
- this.VersionNameIndex.Width = 80;
+ this.VersionNameIndex.Text = "Size";
+ this.VersionNameIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.VersionNameIndex.Width = 85;
//
// DownloadsIndex
//
this.DownloadsIndex.Text = "Popularity";
- this.DownloadsIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Right;
- this.DownloadsIndex.Width = 80;
+ this.DownloadsIndex.TextAlign = System.Windows.Forms.HorizontalAlignment.Center;
+ this.DownloadsIndex.Width = 100;
//
// gamesQueueLabel
//
@@ -773,6 +777,31 @@ namespace AndroidSideloader
this.progressDLbtnContainer.Size = new System.Drawing.Size(984, 40);
this.progressDLbtnContainer.TabIndex = 96;
//
+ // progressBar
+ //
+ this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ | System.Windows.Forms.AnchorStyles.Right)));
+ this.progressBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45)))));
+ this.progressBar.BackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(32)))), ((int)(((byte)(38)))));
+ this.progressBar.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
+ this.progressBar.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
+ this.progressBar.IndeterminateColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
+ this.progressBar.IsIndeterminate = false;
+ this.progressBar.Location = new System.Drawing.Point(1, 23);
+ this.progressBar.Maximum = 100F;
+ this.progressBar.Minimum = 0F;
+ this.progressBar.MinimumSize = new System.Drawing.Size(200, 13);
+ this.progressBar.Name = "progressBar";
+ this.progressBar.OperationType = "";
+ this.progressBar.ProgressEndColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(160)))), ((int)(((byte)(130)))));
+ this.progressBar.ProgressStartColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(220)))), ((int)(((byte)(190)))));
+ this.progressBar.Radius = 6;
+ this.progressBar.Size = new System.Drawing.Size(983, 13);
+ this.progressBar.StatusText = "";
+ this.progressBar.TabIndex = 7;
+ this.progressBar.TextColor = System.Drawing.Color.FromArgb(((int)(((byte)(230)))), ((int)(((byte)(230)))), ((int)(((byte)(230)))));
+ this.progressBar.Value = 0F;
+ //
// diskLabel
//
this.diskLabel.BackColor = System.Drawing.Color.Transparent;
@@ -1238,6 +1267,35 @@ namespace AndroidSideloader
this.sidebarMediaPanel.Size = new System.Drawing.Size(233, 214);
this.sidebarMediaPanel.TabIndex = 101;
//
+ // downloadInstallGameButton
+ //
+ this.downloadInstallGameButton.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190)))));
+ this.downloadInstallGameButton.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190)))));
+ this.downloadInstallGameButton.BackColor = System.Drawing.Color.Transparent;
+ this.downloadInstallGameButton.Cursor = System.Windows.Forms.Cursors.Hand;
+ this.downloadInstallGameButton.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.downloadInstallGameButton.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22)))));
+ this.downloadInstallGameButton.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22)))));
+ this.downloadInstallGameButton.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65)))));
+ this.downloadInstallGameButton.Enabled = false;
+ this.downloadInstallGameButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
+ this.downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82)))));
+ this.downloadInstallGameButton.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
+ this.downloadInstallGameButton.Inactive2 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
+ this.downloadInstallGameButton.Location = new System.Drawing.Point(6, 177);
+ this.downloadInstallGameButton.Margin = new System.Windows.Forms.Padding(0);
+ this.downloadInstallGameButton.Name = "downloadInstallGameButton";
+ this.downloadInstallGameButton.Radius = 4;
+ this.downloadInstallGameButton.Size = new System.Drawing.Size(238, 30);
+ this.downloadInstallGameButton.Stroke = true;
+ this.downloadInstallGameButton.StrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
+ this.downloadInstallGameButton.TabIndex = 94;
+ this.downloadInstallGameButton.Text = "DOWNLOAD AND INSTALL";
+ this.downloadInstallGameButton.Transparency = false;
+ this.downloadInstallGameButton.Click += new System.EventHandler(this.downloadInstallGameButton_Click);
+ this.downloadInstallGameButton.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
+ this.downloadInstallGameButton.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);
+ //
// selectedGameLabel
//
this.selectedGameLabel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(24)))), ((int)(((byte)(29)))));
@@ -1276,58 +1334,6 @@ namespace AndroidSideloader
this.tableLayoutPanel1.Size = new System.Drawing.Size(984, 34);
this.tableLayoutPanel1.TabIndex = 97;
//
- // webView21
- //
- this.webView21.AllowExternalDrop = true;
- this.webView21.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
- this.webView21.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
- this.webView21.CreationProperties = null;
- this.webView21.DefaultBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
- this.webView21.Location = new System.Drawing.Point(259, 496);
- this.webView21.Name = "webView21";
- this.webView21.Size = new System.Drawing.Size(384, 216);
- this.webView21.TabIndex = 98;
- this.webView21.ZoomFactor = 1D;
- //
- // favoriteGame
- //
- this.favoriteGame.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48)))));
- this.favoriteGame.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
- this.favoriteButton});
- this.favoriteGame.Name = "favoriteGame";
- this.favoriteGame.ShowImageMargin = false;
- this.favoriteGame.Size = new System.Drawing.Size(149, 26);
- //
- // favoriteButton
- //
- this.favoriteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48)))));
- this.favoriteButton.ForeColor = System.Drawing.Color.White;
- this.favoriteButton.Name = "favoriteButton";
- this.favoriteButton.Size = new System.Drawing.Size(148, 22);
- this.favoriteButton.Text = "★ Add to Favorites";
- this.favoriteButton.Click += new System.EventHandler(this.favoriteButton_Click);
- //
- // gamesGalleryView
- //
- this.gamesGalleryView.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.gamesGalleryView.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
- this.gamesGalleryView.Location = new System.Drawing.Point(258, 44);
- this.gamesGalleryView.Name = "gamesGalleryView";
- this.gamesGalleryView.Size = new System.Drawing.Size(984, 409);
- this.gamesGalleryView.TabIndex = 102;
- //
- // webViewPlaceholderPanel
- //
- this.webViewPlaceholderPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
- this.webViewPlaceholderPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
- this.webViewPlaceholderPanel.Location = new System.Drawing.Point(259, 496);
- this.webViewPlaceholderPanel.Name = "webViewPlaceholderPanel";
- this.webViewPlaceholderPanel.Size = new System.Drawing.Size(384, 217);
- this.webViewPlaceholderPanel.TabIndex = 103;
- this.webViewPlaceholderPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.webViewPlaceholderPanel_Paint);
- //
// searchPanel
//
this.searchPanel.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(51)))), ((int)(((byte)(56)))), ((int)(((byte)(70)))));
@@ -1511,59 +1517,57 @@ namespace AndroidSideloader
this.btnNewerThanList.Transparency = false;
this.btnNewerThanList.Click += new System.EventHandler(this.btnNewerThanList_Click);
//
- // progressBar
+ // webView21
//
- this.progressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
+ this.webView21.AllowExternalDrop = true;
+ this.webView21.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
+ this.webView21.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
+ this.webView21.CreationProperties = null;
+ this.webView21.DefaultBackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
+ this.webView21.Location = new System.Drawing.Point(259, 496);
+ this.webView21.Name = "webView21";
+ this.webView21.Size = new System.Drawing.Size(384, 216);
+ this.webView21.TabIndex = 98;
+ this.webView21.ZoomFactor = 1D;
+ //
+ // favoriteGame
+ //
+ this.favoriteGame.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48)))));
+ this.favoriteGame.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
+ this.favoriteButton});
+ this.favoriteGame.Name = "favoriteGame";
+ this.favoriteGame.ShowImageMargin = false;
+ this.favoriteGame.Size = new System.Drawing.Size(149, 26);
+ //
+ // favoriteButton
+ //
+ this.favoriteButton.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(40)))), ((int)(((byte)(42)))), ((int)(((byte)(48)))));
+ this.favoriteButton.ForeColor = System.Drawing.Color.White;
+ this.favoriteButton.Name = "favoriteButton";
+ this.favoriteButton.Size = new System.Drawing.Size(148, 22);
+ this.favoriteButton.Text = "★ Add to Favorites";
+ this.favoriteButton.Click += new System.EventHandler(this.favoriteButton_Click);
+ //
+ // gamesGalleryView
+ //
+ this.gamesGalleryView.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.progressBar.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(32)))), ((int)(((byte)(35)))), ((int)(((byte)(45)))));
- this.progressBar.BackgroundColor = System.Drawing.Color.FromArgb(((int)(((byte)(28)))), ((int)(((byte)(32)))), ((int)(((byte)(38)))));
- this.progressBar.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
- this.progressBar.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
- this.progressBar.IndeterminateColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
- this.progressBar.IsIndeterminate = false;
- this.progressBar.Location = new System.Drawing.Point(1, 23);
- this.progressBar.Maximum = 100F;
- this.progressBar.Minimum = 0F;
- this.progressBar.MinimumSize = new System.Drawing.Size(200, 13);
- this.progressBar.Name = "progressBar";
- this.progressBar.OperationType = "";
- this.progressBar.ProgressEndColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(160)))), ((int)(((byte)(130)))));
- this.progressBar.ProgressStartColor = System.Drawing.Color.FromArgb(((int)(((byte)(120)))), ((int)(((byte)(220)))), ((int)(((byte)(190)))));
- this.progressBar.Radius = 6;
- this.progressBar.Size = new System.Drawing.Size(983, 13);
- this.progressBar.StatusText = "";
- this.progressBar.TabIndex = 7;
- this.progressBar.TextColor = System.Drawing.Color.FromArgb(((int)(((byte)(230)))), ((int)(((byte)(230)))), ((int)(((byte)(230)))));
- this.progressBar.Value = 0F;
+ this.gamesGalleryView.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
+ this.gamesGalleryView.Location = new System.Drawing.Point(258, 44);
+ this.gamesGalleryView.Name = "gamesGalleryView";
+ this.gamesGalleryView.Size = new System.Drawing.Size(984, 409);
+ this.gamesGalleryView.TabIndex = 102;
//
- // downloadInstallGameButton
+ // webViewPlaceholderPanel
//
- this.downloadInstallGameButton.Active1 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190)))));
- this.downloadInstallGameButton.Active2 = System.Drawing.Color.FromArgb(((int)(((byte)(110)))), ((int)(((byte)(215)))), ((int)(((byte)(190)))));
- this.downloadInstallGameButton.BackColor = System.Drawing.Color.Transparent;
- this.downloadInstallGameButton.Cursor = System.Windows.Forms.Cursors.Hand;
- this.downloadInstallGameButton.DialogResult = System.Windows.Forms.DialogResult.OK;
- this.downloadInstallGameButton.Disabled1 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22)))));
- this.downloadInstallGameButton.Disabled2 = System.Drawing.Color.FromArgb(((int)(((byte)(16)))), ((int)(((byte)(18)))), ((int)(((byte)(22)))));
- this.downloadInstallGameButton.DisabledStrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(50)))), ((int)(((byte)(55)))), ((int)(((byte)(65)))));
- this.downloadInstallGameButton.Enabled = false;
- this.downloadInstallGameButton.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold);
- this.downloadInstallGameButton.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(59)))), ((int)(((byte)(67)))), ((int)(((byte)(82)))));
- this.downloadInstallGameButton.Inactive1 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
- this.downloadInstallGameButton.Inactive2 = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
- this.downloadInstallGameButton.Location = new System.Drawing.Point(6, 177);
- this.downloadInstallGameButton.Margin = new System.Windows.Forms.Padding(0);
- this.downloadInstallGameButton.Name = "downloadInstallGameButton";
- this.downloadInstallGameButton.Radius = 4;
- this.downloadInstallGameButton.Size = new System.Drawing.Size(238, 30);
- this.downloadInstallGameButton.Stroke = true;
- this.downloadInstallGameButton.StrokeColor = System.Drawing.Color.FromArgb(((int)(((byte)(93)))), ((int)(((byte)(203)))), ((int)(((byte)(173)))));
- this.downloadInstallGameButton.TabIndex = 94;
- this.downloadInstallGameButton.Text = "DOWNLOAD AND INSTALL";
- this.downloadInstallGameButton.Transparency = false;
- this.downloadInstallGameButton.Click += new System.EventHandler(this.downloadInstallGameButton_Click);
- this.downloadInstallGameButton.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
- this.downloadInstallGameButton.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);
+ this.webViewPlaceholderPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
+ this.webViewPlaceholderPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(24)))), ((int)(((byte)(26)))), ((int)(((byte)(30)))));
+ this.webViewPlaceholderPanel.Location = new System.Drawing.Point(259, 496);
+ this.webViewPlaceholderPanel.Name = "webViewPlaceholderPanel";
+ this.webViewPlaceholderPanel.Size = new System.Drawing.Size(384, 217);
+ this.webViewPlaceholderPanel.TabIndex = 103;
+ this.webViewPlaceholderPanel.Paint += new System.Windows.Forms.PaintEventHandler(this.webViewPlaceholderPanel_Paint);
//
// MainForm
//
@@ -1615,11 +1619,11 @@ namespace AndroidSideloader
this.statusInfoPanel.ResumeLayout(false);
this.sidebarMediaPanel.ResumeLayout(false);
this.tableLayoutPanel1.ResumeLayout(false);
- ((System.ComponentModel.ISupportInitialize)(this.webView21)).EndInit();
- this.favoriteGame.ResumeLayout(false);
this.searchPanel.ResumeLayout(false);
this.searchPanel.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.searchIconPictureBox)).EndInit();
+ ((System.ComponentModel.ISupportInitialize)(this.webView21)).EndInit();
+ this.favoriteGame.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
@@ -1732,5 +1736,6 @@ namespace AndroidSideloader
private Label activeMirrorLabel;
private Label sideloadingStatusLabel;
private Label rookieStatusLabel;
+ private ModernListView _listViewRenderer;
}
}
\ No newline at end of file
diff --git a/MainForm.cs b/MainForm.cs
index 68dcc72..c1bdda3 100755
--- a/MainForm.cs
+++ b/MainForm.cs
@@ -127,9 +127,18 @@ namespace AndroidSideloader
gamesQueListBox.DataSource = gamesQueueList;
SetCurrentLogPath();
StartTimers();
+
lvwColumnSorter = new ListViewColumnSorter();
gamesListView.ListViewItemSorter = lvwColumnSorter;
+ // Initialize modern ListView renderer
+ _listViewRenderer = new ModernListView(gamesListView, lvwColumnSorter);
+
+ // Set a larger item height for increased spacing between rows
+ ImageList rowSpacingImageList = new ImageList();
+ rowSpacingImageList.ImageSize = new Size(1, 28);
+ gamesListView.SmallImageList = rowSpacingImageList;
+
SubscribeToHoverEvents(questInfoPanel);
this.Resize += MainForm_Resize;
@@ -2163,6 +2172,63 @@ namespace AndroidSideloader
Logger.Log($"Cloud versions precomputed in {sw.ElapsedMilliseconds}ms");
sw.Restart();
+ // Calculate popularity rankings
+ var popularityScores = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (string[] release in SideloaderRCLONE.games)
+ {
+ string packagename = release[SideloaderRCLONE.PackageNameIndex];
+
+ // Parse popularity score from column 6
+ if (release.Length > 6 && double.TryParse(release[6], out double score))
+ {
+ // Track the highest score per package
+ if (popularityScores.TryGetValue(packagename, out var existing))
+ {
+ if (score > existing)
+ popularityScores[packagename] = score;
+ }
+ else
+ {
+ popularityScores[packagename] = score;
+ }
+ }
+ }
+
+ // Sort packages by popularity (descending) and assign rankings
+ var rankedPackages = popularityScores
+ .Where(kvp => kvp.Value > 0) // Exclude 0.00 scores
+ .OrderByDescending(kvp => kvp.Value)
+ .Select((kvp, index) => new { Package = kvp.Key, Rank = index + 1 })
+ .ToDictionary(x => x.Package, x => x.Rank, StringComparer.OrdinalIgnoreCase);
+
+ Logger.Log($"Popularity rankings calculated for {rankedPackages.Count} games in {sw.ElapsedMilliseconds}ms");
+ sw.Restart();
+
+ // Build MR-Fix lookup. map base game name to whether an MR-Fix exists
+ var mrFixGames = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (string[] release in SideloaderRCLONE.games)
+ {
+ string gameName = release[SideloaderRCLONE.GameNameIndex];
+ // Check if game name contains "(MR-Fix)" using IndexOf for case-insensitive search
+ if (gameName.IndexOf("(MR-Fix)", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ // Extract base game name by removing "(MR-Fix)" suffix
+ int mrFixIndex = gameName.IndexOf("(MR-Fix)", StringComparison.OrdinalIgnoreCase);
+ string baseGameName = gameName.Substring(0, mrFixIndex).Trim();
+ if (gameName.Length > mrFixIndex + 8)
+ {
+ baseGameName += gameName.Substring(mrFixIndex + 8);
+ }
+ baseGameName = baseGameName.Trim();
+ mrFixGames.Add(baseGameName);
+ }
+ }
+
+ Logger.Log($"MR-Fix lookup built with {mrFixGames.Count} games in {sw.ElapsedMilliseconds}ms");
+ sw.Restart();
+
// Process games on background thread
await Task.Run(() =>
{
@@ -2179,7 +2245,28 @@ namespace AndroidSideloader
var item = new ListViewItem(release);
- // Add installed version as additional column
+ // Check if this is a 0 MB entry that should be excluded
+ bool shouldSkip = false;
+ if (release.Length > 5 && double.TryParse(release[5], out double sizeInMB))
+ {
+ // If size is 0 MB and this is not already an MR-Fix version
+ if (sizeInMB == 0 && gameName.IndexOf("(MR-Fix)", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ // Check if there's an MR-Fix version of this game
+ if (mrFixGames.Contains(gameName))
+ {
+ shouldSkip = true;
+ //Logger.Log($"Skipping 0 MB entry for '{gameName}' - MR-Fix version exists");
+ }
+ }
+ }
+
+ if (shouldSkip)
+ {
+ continue; // Skip this entry
+ }
+
+ // Show the installed version
ulong installedVersion = 0;
if (installedVersions.TryGetValue(packagename, out ulong installedVersionInt))
@@ -2231,10 +2318,43 @@ namespace AndroidSideloader
}
}
- // Add the installed version to the ListView item
if (installedVersion != 0)
{
- item.SubItems[3].Text = $"{item.SubItems[3].Text} ({installedVersion})";
+ // Show the installed version and attach 'v' to both versions
+ item.SubItems[3].Text = $"v{item.SubItems[3].Text} / v{installedVersion}";
+ }
+ else
+ {
+ // Attach 'v' to remote version
+ item.SubItems[3].Text = $"v{item.SubItems[3].Text}";
+ }
+
+ // Remove ' UTC' from last updated
+ item.SubItems[4].Text = item.SubItems[4].Text.Replace(" UTC", "");
+
+ // Convert size to GB or MB
+ if (double.TryParse(item.SubItems[5].Text, out double itemSizeInMB))
+ {
+ if (itemSizeInMB >= 1024)
+ {
+ double sizeInGB = itemSizeInMB / 1024;
+ item.SubItems[5].Text = $"{sizeInGB:F2} GB";
+ }
+ else
+ {
+ item.SubItems[5].Text = $"{itemSizeInMB:F0} MB";
+ }
+ }
+
+ // STEP 4: Replace popularity score with ranking
+ if (rankedPackages.TryGetValue(packagename, out int rank))
+ {
+ item.SubItems[6].Text = $"#{rank}";
+ }
+ else
+ {
+ // Unranked (0.00 popularity or not found)
+ item.SubItems[6].Text = "-";
}
if (favoriteView)
@@ -4382,19 +4502,34 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord
private void listView1_ColumnClick(object sender, ColumnClickEventArgs e)
{
- // Determine if clicked column is already the column that is being sorted.
+ // Determine sort order
if (e.Column == lvwColumnSorter.SortColumn)
{
- // Reverse the current sort direction for this column.
lvwColumnSorter.Order = lvwColumnSorter.Order == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending;
}
else
{
lvwColumnSorter.SortColumn = e.Column;
- lvwColumnSorter.Order = e.Column == 4 ? SortOrder.Descending : SortOrder.Ascending;
+ lvwColumnSorter.Order =
+ (e.Column == 4 || e.Column == 5) ? SortOrder.Descending :
+ (e.Column == 6) ? SortOrder.Ascending :
+ SortOrder.Ascending;
}
- // Perform the sort with these new sort options.
- gamesListView.Sort();
+
+ // Suspend drawing during sort
+ gamesListView.BeginUpdate();
+ try
+ {
+ gamesListView.Sort();
+ }
+ finally
+ {
+ gamesListView.EndUpdate();
+ }
+
+ // Invalidate header to update sort indicators
+ gamesListView.Invalidate(new Rectangle(0, 0, gamesListView.ClientSize.Width,
+ gamesListView.Font.Height + 8));
}
private void CheckEnter(object sender, System.Windows.Forms.KeyPressEventArgs e)
@@ -7253,5 +7388,12 @@ function onYouTubeIframeAPIReady() {
action.Invoke();
}
}
+
+ public static void SetStyle(this Control control, ControlStyles styles, bool value)
+ {
+ typeof(Control).GetMethod("SetStyle",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.Invoke(control, new object[] { styles, value });
+ }
}
}
diff --git a/ModernListView.cs b/ModernListView.cs
new file mode 100644
index 0000000..522e66e
--- /dev/null
+++ b/ModernListView.cs
@@ -0,0 +1,1232 @@
+using System;
+using System.Diagnostics;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Text;
+using System.Runtime.InteropServices;
+using System.Windows.Forms;
+
+namespace AndroidSideloader
+{
+ public class ModernListView : NativeWindow
+ {
+ private const int WM_PAINT = 0x000F;
+ private const int WM_ERASEBKGND = 0x0014;
+ private const int WM_VSCROLL = 0x0115;
+ private const int WM_HSCROLL = 0x0114;
+ private const int WM_MOUSEWHEEL = 0x020A;
+ private const int WM_KEYDOWN = 0x0100;
+ private const int WM_SETCURSOR = 0x0020;
+ private const int WM_SETFONT = 0x0030;
+ private const int WM_PRINTCLIENT = 0x0318;
+
+ private const int LVM_GETHEADER = 0x101F;
+ private const int LVM_SETEXTENDEDLISTVIEWSTYLE = 0x1036;
+ private const int LVS_EX_DOUBLEBUFFER = 0x00010000;
+
+ private const int HDM_HITTEST = 0x1206;
+ private const int HDM_LAYOUT = 0x1205;
+ private const int HHT_ONDIVIDER = 0x00000004;
+ private const int HHT_ONDIVOPEN = 0x00000008;
+
+ private const int CellPaddingX = 10;
+ private const int CellPaddingTotalX = CellPaddingX * 2;
+ private const int CUSTOM_HEADER_HEIGHT = 32;
+
+ private static readonly Color HeaderBg = Color.FromArgb(28, 32, 38);
+ private static readonly Color HeaderBorder = Color.FromArgb(50, 55, 65);
+ private static readonly Color HeaderText = Color.FromArgb(140, 145, 150);
+ private static readonly Color HeaderTextSorted = Color.FromArgb(93, 203, 173);
+
+ private static readonly Color RowAlt = Color.FromArgb(30, 32, 38);
+ private static readonly Color RowNormal = Color.FromArgb(24, 26, 30);
+ private static readonly Color RowHover = Color.FromArgb(42, 48, 58);
+
+ private static readonly Color RowSelectedActive = Color.FromArgb(50, 65, 85);
+ private static readonly Color RowSelectedActiveBorder = Color.FromArgb(93, 203, 173);
+
+ private static readonly Color RowSelectedInactive = Color.FromArgb(64, 66, 80);
+ private static readonly Color RowSelectedInactiveBorder = Color.FromArgb(180, 185, 190);
+
+ private static readonly Font HeaderFont = new Font("Segoe UI", 9f, FontStyle.Bold);
+ private static readonly Font ItemFont = new Font("Segoe UI", 9.75f, FontStyle.Regular);
+ private static readonly Font ItemFontBold = new Font("Segoe UI", 9.75f, FontStyle.Bold);
+
+ private static readonly SolidBrush HeaderBgBrush = new SolidBrush(HeaderBg);
+ private static readonly SolidBrush RowAltBrush = new SolidBrush(RowAlt);
+ private static readonly SolidBrush RowNormalBrush = new SolidBrush(RowNormal);
+ private static readonly SolidBrush RowHoverBrush = new SolidBrush(RowHover);
+ private static readonly SolidBrush RowSelectedActiveBrush = new SolidBrush(RowSelectedActive);
+ private static readonly SolidBrush RowSelectedInactiveBrush = new SolidBrush(RowSelectedInactive);
+
+ private static readonly Pen HeaderBorderPen = new Pen(HeaderBorder, 1);
+ private static readonly Pen HeaderSeparatorPen = new Pen(Color.FromArgb(55, 60, 70), 1);
+ private static readonly Pen SelectedActiveBorderPen = new Pen(RowSelectedActiveBorder, 4);
+ private static readonly Pen SelectedInactiveBorderPen = new Pen(RowSelectedInactiveBorder, 4);
+ private static readonly SolidBrush SortArrowBrush = new SolidBrush(HeaderTextSorted);
+
+ private readonly ListView _listView;
+ private readonly ListViewColumnSorter _columnSorter;
+ private readonly HeaderCursorWindow _headerCursor;
+ private readonly Timer _columnResizeDebounce = new Timer { Interval = 150 };
+
+ public int DefaultSortColumn { get; set; } = 0;
+ public SortOrder DefaultSortOrder { get; set; } = SortOrder.Ascending;
+
+ public enum ColumnFillMode { StretchLastColumn, Proportional }
+ private ColumnFillMode _fillMode = ColumnFillMode.Proportional;
+ public ColumnFillMode FillMode
+ {
+ get => _fillMode;
+ set { _fillMode = value; AutoFitColumnsToWidth(); RecalcMarqueeForSelection(); }
+ }
+
+ public bool MarqueeEnabled { get; set; } = true;
+ public bool MarqueeOnlyWhenFocused { get; set; } = false;
+ public int MarqueeStartDelayMs { get; set; } = 250;
+ public int MarqueePauseMs { get; set; } = 500;
+ public float MarqueeSpeedPxPerSecond { get; set; } = 32f;
+ public bool MarqueeFadeEdges { get; set; } = true;
+ public int MarqueeFadeWidthPx { get; set; } = 8;
+ public int MarqueeOvershootPx { get; set; } = 2;
+ public int MinOverflowForMarqueePx { get; set; } = 2;
+ public float MarqueeMinProgressPerSecond { get; set; } = 0.20f;
+
+ private readonly Timer _marqueeTimer = new Timer { Interval = 8 }; // ~120 FPS
+ private readonly Stopwatch _marqueeSw = Stopwatch.StartNew();
+
+ private float[] _marqueeOffsets = new float[0];
+ private float[] _marqueeMax = new float[0];
+ private float[] _marqueeProgress = new float[0];
+ private int[] _marqueeDirs = new int[0];
+ private int[] _marqueeHoldMs = new int[0];
+
+ private int _marqueeSelectedIndex = -1;
+ private long _marqueeLastTickMs;
+ private long _marqueeStartAtMs;
+
+ private int _hoveredItemIndex = -1;
+ private Rectangle _lastIconBounds = Rectangle.Empty;
+ private bool _inPostPaintOverlay;
+ private bool _inAutoFit;
+ private bool _userIsResizingColumns;
+ private float[] _columnRatios = new float[0];
+
+ private IntPtr _headerHfont = IntPtr.Zero;
+
+ public int HoveredItemIndex => _hoveredItemIndex;
+ public Rectangle LastIconBounds
+ {
+ get => _lastIconBounds;
+ set
+ {
+ if (_lastIconBounds != value)
+ {
+ if (!_lastIconBounds.IsEmpty)
+ _listView.Invalidate(Rectangle.Inflate(_lastIconBounds, 2, 2));
+ _lastIconBounds = value;
+ }
+ }
+ }
+
+ [DllImport("uxtheme.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+ private static extern int SetWindowTheme(IntPtr hWnd, string pszSubAppName, string pszSubIdList);
+
+ [DllImport("dwmapi.dll", PreserveSig = true)]
+ private static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, ref int pvAttribute, int cbAttribute);
+
+ [DllImport("user32.dll", CharSet = CharSet.Auto)]
+ private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
+
+ [DllImport("user32.dll")]
+ private static extern bool GetCursorPos(out Point lpPoint);
+
+ [DllImport("user32.dll")]
+ private static extern bool ScreenToClient(IntPtr hWnd, ref Point lpPoint);
+
+ [DllImport("user32.dll")]
+ private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);
+
+ [DllImport("gdi32.dll")]
+ private static extern bool DeleteObject(IntPtr hObject);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct HDHITTESTINFO
+ {
+ public Point pt;
+ public uint flags;
+ public int iItem;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct HDLAYOUT
+ {
+ public IntPtr prc;
+ public IntPtr pwpos;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct WINDOWPOS
+ {
+ public IntPtr hwnd;
+ public IntPtr hwndInsertAfter;
+ public int x;
+ public int y;
+ public int cx;
+ public int cy;
+ public uint flags;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RECT
+ {
+ public int left;
+ public int top;
+ public int right;
+ public int bottom;
+ }
+
+ private sealed class HeaderCursorWindow : NativeWindow
+ {
+ private readonly ModernListView _owner;
+
+ public HeaderCursorWindow(ModernListView owner)
+ {
+ _owner = owner;
+ }
+
+ public void Attach(IntPtr headerHwnd)
+ {
+ if (headerHwnd != IntPtr.Zero)
+ AssignHandle(headerHwnd);
+ }
+
+ protected override void WndProc(ref Message m)
+ {
+ if (m.Msg == WM_ERASEBKGND)
+ {
+ m.Result = (IntPtr)1;
+ return;
+ }
+
+ if (m.Msg == WM_SETCURSOR)
+ {
+ if (IsOnDivider())
+ {
+ base.WndProc(ref m);
+ return;
+ }
+
+ Cursor.Current = Cursors.Hand;
+ m.Result = (IntPtr)1;
+ return;
+ }
+
+ if (m.Msg == HDM_LAYOUT)
+ {
+ base.WndProc(ref m);
+
+ try
+ {
+ HDLAYOUT hdl = Marshal.PtrToStructure(m.LParam);
+ WINDOWPOS wpos = Marshal.PtrToStructure(hdl.pwpos);
+ RECT rc = Marshal.PtrToStructure(hdl.prc);
+
+ wpos.cy = CUSTOM_HEADER_HEIGHT;
+ rc.top = CUSTOM_HEADER_HEIGHT;
+
+ Marshal.StructureToPtr(wpos, hdl.pwpos, false);
+ Marshal.StructureToPtr(rc, hdl.prc, false);
+ }
+ catch { }
+
+ m.Result = (IntPtr)1;
+ return;
+ }
+
+ base.WndProc(ref m);
+
+ if (m.Msg == WM_PAINT || m.Msg == WM_PRINTCLIENT)
+ _owner.PaintHeaderRightGap(Handle);
+ }
+
+ private bool IsOnDivider()
+ {
+ if (Handle == IntPtr.Zero || !GetCursorPos(out var screenPt)) return false;
+
+ var pt = screenPt;
+ if (!ScreenToClient(Handle, ref pt)) return false;
+
+ var hti = new HDHITTESTINFO { pt = pt };
+ IntPtr mem = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(HDHITTESTINFO)));
+ try
+ {
+ Marshal.StructureToPtr(hti, mem, false);
+ SendMessage(Handle, HDM_HITTEST, IntPtr.Zero, mem);
+ hti = Marshal.PtrToStructure(mem);
+ return (hti.flags & (HHT_ONDIVIDER | HHT_ONDIVOPEN)) != 0;
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(mem);
+ }
+ }
+ }
+
+ public ModernListView(ListView listView, ListViewColumnSorter columnSorter)
+ {
+ _listView = listView;
+ _columnSorter = columnSorter;
+ _headerCursor = new HeaderCursorWindow(this);
+
+ _listView.OwnerDraw = true;
+ _listView.FullRowSelect = true;
+ _listView.View = View.Details;
+ _listView.HotTracking = false;
+ _listView.HoverSelection = false;
+ _listView.GridLines = false;
+
+ EnableManagedDoubleBuffering();
+
+ if (_listView.IsHandleCreated)
+ AssignHandle(_listView.Handle);
+
+ _listView.DrawColumnHeader += DrawColumnHeader;
+ _listView.DrawItem += DrawItem;
+ _listView.DrawSubItem += DrawSubItem;
+ _listView.MouseMove += OnMouseMove;
+ _listView.MouseLeave += OnMouseLeave;
+ _listView.Paint += OnPaint;
+ _listView.Resize += OnResize;
+ _listView.ColumnWidthChanging += OnColumnWidthChanging;
+ _listView.ColumnWidthChanged += OnColumnWidthChanged;
+ _listView.HandleCreated += OnHandleCreated;
+ _listView.HandleDestroyed += OnHandleDestroyed;
+
+ _listView.SelectedIndexChanged += (s, e) => RecalcMarqueeForSelection();
+ _listView.ItemSelectionChanged += (s, e) => RecalcMarqueeForSelection();
+ _listView.GotFocus += (s, e) => UpdateMarqueeTimerState();
+ _listView.LostFocus += (s, e) => UpdateMarqueeTimerState();
+
+ _columnResizeDebounce.Tick += (s, e) =>
+ {
+ _columnResizeDebounce.Stop();
+ _userIsResizingColumns = false;
+ CaptureColumnRatios();
+ AutoFitColumnsToWidth();
+ RecalcMarqueeForSelection();
+ _listView.Invalidate();
+ };
+
+ _marqueeTimer.Tick += (s, e) => UpdateMarquee();
+
+ InitializeColumnSizing();
+ RecalcMarqueeForSelection();
+ }
+
+ protected override void WndProc(ref Message m)
+ {
+ if (m.Msg == WM_ERASEBKGND)
+ {
+ m.Result = (IntPtr)1;
+ return;
+ }
+
+ base.WndProc(ref m);
+
+ if (m.Msg == WM_PAINT && !_inPostPaintOverlay)
+ {
+ try
+ {
+ _inPostPaintOverlay = true;
+ PaintEmptyAreaOverlay();
+ }
+ finally
+ {
+ _inPostPaintOverlay = false;
+ }
+ }
+
+ switch (m.Msg)
+ {
+ case WM_VSCROLL:
+ case WM_HSCROLL:
+ case WM_MOUSEWHEEL:
+ OnScrollDetected();
+ break;
+ case WM_KEYDOWN:
+ int key = m.WParam.ToInt32();
+ if (key == 0x21 || key == 0x22 || key == 0x26 || key == 0x28)
+ OnScrollDetected();
+ break;
+ }
+ }
+
+ private void OnScrollDetected()
+ {
+ _lastIconBounds = Rectangle.Empty;
+ _listView.Invalidate();
+ }
+
+ private void OnHandleCreated(object sender, EventArgs e)
+ {
+ AssignHandle(_listView.Handle);
+ ApplyModernScrollbars();
+ EnableNativeDoubleBuffering();
+
+ _listView.BeginInvoke(new Action(() =>
+ {
+ if (!_listView.IsHandleCreated) return;
+
+ IntPtr header = SendMessage(_listView.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);
+ _headerCursor.Attach(header);
+
+ ApplyHeaderFont(header);
+ PaintHeaderRightGap(header);
+ }));
+
+ InitializeColumnSizing();
+ RecalcMarqueeForSelection();
+ }
+
+ private void OnHandleDestroyed(object sender, EventArgs e)
+ {
+ _columnResizeDebounce.Stop();
+ _marqueeTimer.Stop();
+ _headerCursor.ReleaseHandle();
+ ReleaseHandle();
+
+ if (_headerHfont != IntPtr.Zero)
+ {
+ DeleteObject(_headerHfont);
+ _headerHfont = IntPtr.Zero;
+ }
+ }
+
+ private void ApplyHeaderFont(IntPtr headerHandle)
+ {
+ if (headerHandle == IntPtr.Zero) return;
+
+ if (_headerHfont == IntPtr.Zero)
+ _headerHfont = HeaderFont.ToHfont();
+
+ SendMessage(headerHandle, WM_SETFONT, _headerHfont, (IntPtr)1);
+ _listView.Invalidate();
+ }
+
+ private void EnableManagedDoubleBuffering()
+ {
+ typeof(Control).GetProperty("DoubleBuffered",
+ System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
+ ?.SetValue(_listView, true, null);
+ }
+
+ private void EnableNativeDoubleBuffering()
+ {
+ if (!_listView.IsHandleCreated) return;
+ SendMessage(_listView.Handle, LVM_SETEXTENDEDLISTVIEWSTYLE,
+ (IntPtr)LVS_EX_DOUBLEBUFFER, (IntPtr)LVS_EX_DOUBLEBUFFER);
+ }
+
+ private void ApplyModernScrollbars()
+ {
+ if (!_listView.IsHandleCreated) return;
+
+ int dark = 1;
+ int hr = DwmSetWindowAttribute(_listView.Handle, 20, ref dark, sizeof(int));
+ if (hr != 0)
+ DwmSetWindowAttribute(_listView.Handle, 19, ref dark, sizeof(int));
+
+ if (SetWindowTheme(_listView.Handle, "DarkMode_Explorer", null) != 0)
+ SetWindowTheme(_listView.Handle, "Explorer", null);
+ }
+
+ private void InitializeColumnSizing()
+ {
+ if (!_listView.IsHandleCreated || _listView.Columns.Count == 0) return;
+ CaptureColumnRatios();
+ AutoFitColumnsToWidth();
+ }
+
+ private void OnResize(object sender, EventArgs e)
+ {
+ AutoFitColumnsToWidth();
+ RecalcMarqueeForSelection();
+ _listView.Invalidate();
+ }
+
+ private void OnColumnWidthChanging(object sender, ColumnWidthChangingEventArgs e)
+ {
+ _userIsResizingColumns = true;
+ _columnResizeDebounce.Stop();
+ }
+
+ private void OnColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e)
+ {
+ if (_inAutoFit) return;
+
+ if (_userIsResizingColumns)
+ {
+ _columnResizeDebounce.Stop();
+ _columnResizeDebounce.Start();
+ return;
+ }
+
+ RecalcMarqueeForSelection();
+ _listView.Invalidate();
+ }
+
+ private void CaptureColumnRatios()
+ {
+ int count = _listView.Columns.Count;
+ if (count == 0) { _columnRatios = new float[0]; return; }
+
+ float total = 0f;
+ for (int i = 0; i < count; i++)
+ total += Math.Max(1, _listView.Columns[i].Width);
+
+ if (total <= 0.01f) total = 1f;
+
+ _columnRatios = new float[count];
+ for (int i = 0; i < count; i++)
+ _columnRatios[i] = Math.Max(1, _listView.Columns[i].Width) / total;
+ }
+
+ private void AutoFitColumnsToWidth()
+ {
+ if (_inAutoFit || _userIsResizingColumns || !_listView.IsHandleCreated || _listView.Columns.Count == 0)
+ return;
+
+ try
+ {
+ _inAutoFit = true;
+ _listView.BeginUpdate();
+
+ int clientWidth = Math.Max(0, _listView.ClientSize.Width);
+ if (clientWidth <= 0) return;
+
+ if (_fillMode == ColumnFillMode.StretchLastColumn)
+ StretchLastColumn(clientWidth);
+ else
+ ProportionalFill(clientWidth);
+ }
+ finally
+ {
+ _listView.EndUpdate();
+ _inAutoFit = false;
+ }
+ }
+
+ private void StretchLastColumn(int clientWidth)
+ {
+ int count = _listView.Columns.Count;
+ if (count == 1)
+ {
+ _listView.Columns[0].Width = clientWidth;
+ return;
+ }
+
+ int otherTotal = 0;
+ for (int i = 0; i < count - 1; i++)
+ otherTotal += _listView.Columns[i].Width;
+
+ _listView.Columns[count - 1].Width = Math.Max(60, clientWidth - otherTotal);
+ }
+
+ private void ProportionalFill(int clientWidth)
+ {
+ int count = _listView.Columns.Count;
+ if (_columnRatios.Length != count)
+ CaptureColumnRatios();
+
+ int used = 0;
+ for (int i = 0; i < count - 1; i++)
+ {
+ int w = Math.Max(30, (int)Math.Round(clientWidth * _columnRatios[i]));
+ _listView.Columns[i].Width = w;
+ used += w;
+ }
+
+ _listView.Columns[count - 1].Width = Math.Max(60, clientWidth - used);
+ }
+
+ private int GetHeaderHeight()
+ {
+ if (_listView.Items.Count > 0 && _listView.Items[0].Bounds.Top > 0)
+ return _listView.Items[0].Bounds.Top;
+
+ return CUSTOM_HEADER_HEIGHT;
+ }
+
+ private bool IsPointInHeader(Point pt) => pt.Y >= 0 && pt.Y < GetHeaderHeight();
+
+ private SolidBrush GetRowBrush(int itemIndex, bool isSelected, bool isHovered)
+ {
+ if (isSelected)
+ return _listView.Focused ? RowSelectedActiveBrush : RowSelectedInactiveBrush;
+ if (isHovered)
+ return RowHoverBrush;
+ return (itemIndex % 2 == 1) ? RowAltBrush : RowNormalBrush;
+ }
+
+ private Color GetRowColor(int itemIndex, bool isSelected, bool isHovered)
+ {
+ if (isSelected) return _listView.Focused ? RowSelectedActive : RowSelectedInactive;
+ if (isHovered) return RowHover;
+ return (itemIndex % 2 == 1) ? RowAlt : RowNormal;
+ }
+
+ private int GetEffectiveOvershootPx()
+ {
+ int o = Math.Max(0, MarqueeOvershootPx);
+ if (MarqueeFadeEdges)
+ o = Math.Max(o, Math.Max(0, MarqueeFadeWidthPx));
+ return o;
+ }
+
+ private void PaintHeaderRightGap(IntPtr headerHandle)
+ {
+ if (headerHandle == IntPtr.Zero || !_listView.IsHandleCreated) return;
+
+ RECT rc;
+ if (!GetClientRect(headerHandle, out rc)) return;
+
+ int width = rc.right - rc.left;
+ int height = rc.bottom - rc.top;
+ if (width <= 0 || height <= 0) return;
+
+ int total = 0;
+ foreach (ColumnHeader col in _listView.Columns)
+ total += col.Width;
+
+ if (total >= width) return;
+
+ using (var g = Graphics.FromHwnd(headerHandle))
+ {
+ g.FillRectangle(HeaderBgBrush, new Rectangle(total, 0, width - total + 1, height));
+ g.DrawLine(HeaderBorderPen, total, height - 1, width, height - 1);
+ }
+ }
+
+ private void DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
+ {
+ var g = e.Graphics;
+ int listViewWidth = _listView.ClientSize.Width;
+
+ g.FillRectangle(HeaderBgBrush, e.Bounds);
+
+ if (e.ColumnIndex == _listView.Columns.Count - 1)
+ {
+ int rightEdge = e.Bounds.Right;
+ if (rightEdge < listViewWidth)
+ {
+ var extended = new Rectangle(rightEdge, e.Bounds.Top, listViewWidth - rightEdge + 1, e.Bounds.Height);
+ g.FillRectangle(HeaderBgBrush, extended);
+ g.DrawLine(HeaderBorderPen, rightEdge, e.Bounds.Bottom - 1, listViewWidth, e.Bounds.Bottom - 1);
+ }
+ }
+
+ g.DrawLine(HeaderBorderPen, e.Bounds.Left, e.Bounds.Bottom - 1, e.Bounds.Right, e.Bounds.Bottom - 1);
+
+ if (e.ColumnIndex < _listView.Columns.Count - 1)
+ {
+ int separatorX = e.Bounds.Right - 1;
+ g.DrawLine(HeaderSeparatorPen, separatorX, e.Bounds.Top + 4, separatorX, e.Bounds.Bottom - 4);
+ }
+
+ var textBounds = new Rectangle(e.Bounds.X + CellPaddingX, e.Bounds.Y, e.Bounds.Width - CellPaddingTotalX, e.Bounds.Height);
+
+ var flags = TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine;
+ if (e.Header.TextAlign == HorizontalAlignment.Center)
+ flags |= TextFormatFlags.HorizontalCenter;
+ else if (e.Header.TextAlign == HorizontalAlignment.Right)
+ flags |= TextFormatFlags.Right;
+ else
+ flags |= TextFormatFlags.Left;
+
+ bool hasActiveSort = _columnSorter != null && _columnSorter.Order != SortOrder.None;
+ bool isActiveSortedColumn = hasActiveSort && _columnSorter.SortColumn == e.ColumnIndex;
+
+ bool showDefaultSort = !hasActiveSort && DefaultSortOrder != SortOrder.None && e.ColumnIndex == DefaultSortColumn;
+ bool tint = isActiveSortedColumn || showDefaultSort;
+
+ Color headerTextColor = tint ? HeaderTextSorted : HeaderText;
+ TextRenderer.DrawText(g, e.Header.Text, HeaderFont, textBounds, headerTextColor, flags);
+
+ SortOrder drawOrder = SortOrder.None;
+ if (isActiveSortedColumn) drawOrder = _columnSorter.Order;
+ else if (showDefaultSort) drawOrder = DefaultSortOrder;
+
+ if (e.ColumnIndex == 6 && drawOrder != SortOrder.None)
+ drawOrder = (drawOrder == SortOrder.Ascending) ? SortOrder.Descending : SortOrder.Ascending;
+
+ if (drawOrder != SortOrder.None)
+ DrawSortArrow(g, e.Bounds, drawOrder);
+
+ e.DrawDefault = false;
+ }
+
+ private static void DrawSortArrow(Graphics g, Rectangle bounds, SortOrder order)
+ {
+ int centerX = bounds.Right - 12;
+ int centerY = bounds.Top + (bounds.Height / 2) - 1;
+
+ int halfWidth = 4;
+ int halfHeight = 3;
+
+ Point[] arrow = (order == SortOrder.Ascending)
+ ? new[]
+ {
+ new Point(centerX, centerY - halfHeight),
+ new Point(centerX - halfWidth, centerY + halfHeight),
+ new Point(centerX + halfWidth, centerY + halfHeight),
+ }
+ : new[]
+ {
+ new Point(centerX, centerY + halfHeight),
+ new Point(centerX - halfWidth, centerY - halfHeight),
+ new Point(centerX + halfWidth, centerY - halfHeight),
+ };
+
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+ g.FillPolygon(SortArrowBrush, arrow);
+ g.SmoothingMode = SmoothingMode.None;
+ }
+
+ private void DrawItem(object sender, DrawListViewItemEventArgs e)
+ {
+ e.DrawDefault = false;
+ }
+
+ private void DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
+ {
+ var g = e.Graphics;
+
+ bool isSelected = e.Item.Selected;
+ bool isHovered = (e.ItemIndex == _hoveredItemIndex) && !isSelected;
+
+ if (e.ColumnIndex == 0)
+ DrawRowBackground(g, e.Item, e.ItemIndex, isSelected, isHovered);
+
+ string text = e.SubItem?.Text ?? "";
+ var font = isSelected ? ItemFontBold : ItemFont;
+
+ var textBounds = new Rectangle(e.Bounds.X + CellPaddingX, e.Bounds.Y, e.Bounds.Width - CellPaddingTotalX, e.Bounds.Height);
+ Color textColor = GetTextColor(e.Item.ForeColor, isSelected, isHovered);
+
+ if (ShouldDrawMarquee(e.ItemIndex, e.ColumnIndex, isSelected, textBounds, text))
+ {
+ DrawMarqueeText(g, textBounds, e.Bounds, text, font, textColor, _marqueeOffsets[e.ColumnIndex]);
+
+ if (MarqueeFadeEdges)
+ DrawFadeEdgesOverlay(g, textBounds, GetRowColor(e.ItemIndex, isSelected, isHovered), MarqueeFadeWidthPx);
+
+ e.DrawDefault = false;
+ return;
+ }
+
+ var flags = TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine;
+ flags |= (e.ColumnIndex > 2) ? TextFormatFlags.HorizontalCenter : TextFormatFlags.Left;
+ TextRenderer.DrawText(g, text, font, textBounds, textColor, flags);
+
+ e.DrawDefault = false;
+ }
+
+ private void DrawRowBackground(Graphics g, ListViewItem item, int itemIndex, bool isSelected, bool isHovered)
+ {
+ int listViewWidth = _listView.ClientSize.Width;
+ var bgBrush = GetRowBrush(itemIndex, isSelected, isHovered);
+ var fullRowRect = new Rectangle(0, item.Bounds.Top, listViewWidth, item.Bounds.Height);
+ g.FillRectangle(bgBrush, fullRowRect);
+
+ if (isSelected)
+ {
+ Pen pen = _listView.Focused ? SelectedActiveBorderPen : SelectedInactiveBorderPen;
+ g.DrawLine(pen, 1, item.Bounds.Top + 1, 1, item.Bounds.Bottom - 1);
+ }
+ }
+
+ private static Color GetTextColor(Color baseColor, bool isSelected, bool isHovered)
+ {
+ Color c = baseColor;
+ if (c == SystemColors.WindowText || c == Color.Empty || c == Color.Black)
+ c = Color.White;
+
+ if (isSelected || isHovered)
+ {
+ int boost = isSelected ? 40 : 20;
+ c = Color.FromArgb(
+ Math.Min(255, c.R + boost),
+ Math.Min(255, c.G + boost),
+ Math.Min(255, c.B + boost));
+ }
+
+ return c;
+ }
+
+ private bool ShouldDrawMarquee(int itemIndex, int columnIndex, bool isSelected, Rectangle textBounds, string text)
+ {
+ if (!MarqueeEnabled) return false;
+ if (!isSelected) return false;
+
+ if (MarqueeOnlyWhenFocused && !_listView.Focused)
+ return false;
+
+ if (itemIndex != _marqueeSelectedIndex) return false;
+ if (string.IsNullOrEmpty(text)) return false;
+ if (textBounds.Width <= 8) return false;
+
+ if (columnIndex < 0 || columnIndex >= _marqueeMax.Length) return false;
+ return _marqueeMax[columnIndex] > 1f;
+ }
+
+ private static void DrawMarqueeText(Graphics g, Rectangle clipBounds, Rectangle itemBounds, string text, Font font, Color textColor, float offset)
+ {
+ float x = clipBounds.X - offset;
+ float y = itemBounds.Y + (itemBounds.Height - font.Height) / 2f;
+
+ var state = g.Save();
+ try
+ {
+ g.SetClip(clipBounds);
+ g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
+
+ using (var b = new SolidBrush(textColor))
+ using (var sf = StringFormat.GenericTypographic)
+ {
+ sf.FormatFlags |= StringFormatFlags.NoWrap;
+ sf.Trimming = StringTrimming.None;
+ g.DrawString(text, font, b, new PointF(x, y), sf);
+ }
+ }
+ finally
+ {
+ g.Restore(state);
+ }
+ }
+
+ private static void DrawFadeEdgesOverlay(Graphics g, Rectangle bounds, Color bg, int fadeWidthPx)
+ {
+ int w = Math.Max(0, Math.Min(fadeWidthPx, bounds.Width / 2));
+ if (w <= 0) return;
+
+ const int overlap = 2;
+
+ var leftRect = new Rectangle(bounds.Left - overlap, bounds.Top, w + overlap, bounds.Height);
+ using (var lb = new LinearGradientBrush(leftRect, Color.FromArgb(255, bg), Color.FromArgb(0, bg), LinearGradientMode.Horizontal))
+ g.FillRectangle(lb, leftRect);
+
+ var rightRect = new Rectangle(bounds.Right - w, bounds.Top, w + overlap, bounds.Height);
+ using (var rb = new LinearGradientBrush(rightRect, Color.FromArgb(0, bg), Color.FromArgb(255, bg), LinearGradientMode.Horizontal))
+ g.FillRectangle(rb, rightRect);
+ }
+
+ private void OnMouseMove(object sender, MouseEventArgs e)
+ {
+ int newHoveredIndex = -1;
+
+ if (!IsPointInHeader(e.Location))
+ {
+ var hitTest = _listView.HitTest(e.Location);
+ newHoveredIndex = hitTest.Item?.Index ?? -1;
+ }
+
+ if (newHoveredIndex == _hoveredItemIndex)
+ return;
+
+ int oldIndex = _hoveredItemIndex;
+ _hoveredItemIndex = newHoveredIndex;
+
+ if (oldIndex >= 0 && oldIndex < _listView.Items.Count)
+ _listView.RedrawItems(oldIndex, oldIndex, true);
+
+ if (newHoveredIndex >= 0 && newHoveredIndex < _listView.Items.Count)
+ _listView.RedrawItems(newHoveredIndex, newHoveredIndex, true);
+ }
+
+ private void OnMouseLeave(object sender, EventArgs e)
+ {
+ if (_hoveredItemIndex < 0 || _hoveredItemIndex >= _listView.Items.Count)
+ return;
+
+ int oldIndex = _hoveredItemIndex;
+ _hoveredItemIndex = -1;
+ _listView.RedrawItems(oldIndex, oldIndex, true);
+ }
+
+ private void OnPaint(object sender, PaintEventArgs e)
+ {
+ var g = e.Graphics;
+ int listViewWidth = _listView.ClientSize.Width;
+
+ int totalColumnWidth = 0;
+ foreach (ColumnHeader col in _listView.Columns)
+ totalColumnWidth += col.Width;
+
+ int headerHeight = GetHeaderHeight();
+
+ if (totalColumnWidth < listViewWidth)
+ {
+ var emptyHeaderRect = new Rectangle(totalColumnWidth, 0, listViewWidth - totalColumnWidth + 1, headerHeight);
+ g.FillRectangle(HeaderBgBrush, emptyHeaderRect);
+ g.DrawLine(HeaderBorderPen, totalColumnWidth, headerHeight - 1, listViewWidth, headerHeight - 1);
+ }
+
+ FillEmptyBottomArea(g, headerHeight, listViewWidth);
+ }
+
+ private void FillEmptyBottomArea(Graphics g, int headerHeight, int listViewWidth)
+ {
+ if (_listView.Items.Count == 0)
+ {
+ var emptyRect = new Rectangle(0, headerHeight, listViewWidth, _listView.ClientSize.Height - headerHeight);
+ g.FillRectangle(RowNormalBrush, emptyRect);
+ return;
+ }
+
+ int firstVisibleIndex = _listView.TopItem != null ? _listView.TopItem.Index : 0;
+ int itemHeight = _listView.Items[0].Bounds.Height;
+ int visibleCount = (_listView.ClientSize.Height - headerHeight) / Math.Max(1, itemHeight) + 2;
+ int lastVisibleIndex = Math.Min(firstVisibleIndex + visibleCount, _listView.Items.Count - 1);
+
+ var lastItem = _listView.Items[lastVisibleIndex];
+ int bottomY = lastItem.Bounds.Bottom;
+
+ if (bottomY < _listView.ClientSize.Height)
+ {
+ var emptyBottomRect = new Rectangle(0, bottomY, listViewWidth, _listView.ClientSize.Height - bottomY);
+ g.FillRectangle(RowNormalBrush, emptyBottomRect);
+ }
+ }
+
+ private void PaintEmptyAreaOverlay()
+ {
+ int w = _listView.ClientSize.Width;
+ int h = _listView.ClientSize.Height;
+ if (w <= 0 || h <= 0) return;
+
+ int headerHeight = GetHeaderHeight();
+ int fillTop = headerHeight;
+
+ if (_listView.Items.Count > 0)
+ {
+ int firstVisibleIndex = _listView.TopItem != null ? _listView.TopItem.Index : 0;
+ int itemHeight = _listView.Items[0].Bounds.Height;
+ int visibleCount = (h - headerHeight) / Math.Max(1, itemHeight) + 2;
+ int lastVisibleIndex = Math.Min(firstVisibleIndex + visibleCount, _listView.Items.Count - 1);
+
+ fillTop = Math.Max(fillTop, _listView.Items[lastVisibleIndex].Bounds.Bottom);
+ }
+
+ if (fillTop < h)
+ {
+ int overlayTop = (fillTop == headerHeight) ? Math.Max(0, fillTop - 10) : fillTop;
+ using (var g = Graphics.FromHwnd(_listView.Handle))
+ {
+ g.FillRectangle(RowNormalBrush, new Rectangle(0, overlayTop, w, h - overlayTop));
+ }
+ }
+ }
+
+ private ListViewItem GetActiveSelectedItem()
+ {
+ var focused = _listView.FocusedItem;
+ if (focused != null && focused.Selected)
+ return focused;
+
+ if (_listView.SelectedItems.Count > 0)
+ return _listView.SelectedItems[0];
+
+ return null;
+ }
+
+ private void EnsureMarqueeArrays()
+ {
+ int count = _listView.Columns.Count;
+ if (count < 0) count = 0;
+
+ if (_marqueeOffsets.Length == count &&
+ _marqueeMax.Length == count &&
+ _marqueeProgress.Length == count &&
+ _marqueeDirs.Length == count &&
+ _marqueeHoldMs.Length == count)
+ return;
+
+ _marqueeOffsets = new float[count];
+ _marqueeMax = new float[count];
+ _marqueeProgress = new float[count];
+ _marqueeDirs = new int[count];
+ _marqueeHoldMs = new int[count];
+
+ for (int i = 0; i < count; i++)
+ _marqueeDirs[i] = 1;
+ }
+
+ private void RecalcMarqueeForSelection()
+ {
+ if (!_listView.IsHandleCreated) return;
+
+ var item = GetActiveSelectedItem();
+ int idx = item != null ? item.Index : -1;
+
+ if (idx != _marqueeSelectedIndex)
+ {
+ _marqueeSelectedIndex = idx;
+ ResetMarqueeState();
+ return;
+ }
+
+ ResetMarqueeExtentsOnly();
+ }
+
+ private void ResetMarqueeState()
+ {
+ EnsureMarqueeArrays();
+
+ Array.Clear(_marqueeOffsets, 0, _marqueeOffsets.Length);
+ Array.Clear(_marqueeMax, 0, _marqueeMax.Length);
+ Array.Clear(_marqueeProgress, 0, _marqueeProgress.Length);
+ Array.Clear(_marqueeHoldMs, 0, _marqueeHoldMs.Length);
+
+ for (int i = 0; i < _marqueeDirs.Length; i++)
+ _marqueeDirs[i] = 1;
+
+ _marqueeLastTickMs = _marqueeSw.ElapsedMilliseconds;
+ _marqueeStartAtMs = _marqueeLastTickMs + Math.Max(0, MarqueeStartDelayMs);
+
+ ComputeMarqueeMaxForSelectedRow();
+ UpdateMarqueeTimerState();
+
+ if (_marqueeSelectedIndex >= 0 && _marqueeSelectedIndex < _listView.Items.Count)
+ _listView.RedrawItems(_marqueeSelectedIndex, _marqueeSelectedIndex, true);
+ else
+ _listView.Invalidate();
+ }
+
+ private void ResetMarqueeExtentsOnly()
+ {
+ EnsureMarqueeArrays();
+
+ float[] oldMax = (float[])_marqueeMax.Clone();
+ ComputeMarqueeMaxForSelectedRow();
+
+ int overshoot = GetEffectiveOvershootPx();
+
+ for (int i = 0; i < _marqueeOffsets.Length; i++)
+ {
+ if (_marqueeMax[i] <= 0f)
+ {
+ _marqueeOffsets[i] = 0f;
+ _marqueeProgress[i] = 0f;
+ _marqueeHoldMs[i] = 0;
+ _marqueeDirs[i] = 1;
+ continue;
+ }
+
+ float overflow = _marqueeMax[i];
+ float travel = overflow + (2f * overshoot);
+ float p = Clamp01(_marqueeProgress[i]);
+ _marqueeProgress[i] = p;
+ _marqueeOffsets[i] = (Ease(p) * travel) - overshoot;
+ }
+
+ UpdateMarqueeTimerState();
+
+ bool changed = false;
+ for (int i = 0; i < oldMax.Length && i < _marqueeMax.Length; i++)
+ {
+ if (Math.Abs(oldMax[i] - _marqueeMax[i]) > 0.5f) { changed = true; break; }
+ }
+
+ if (changed && _marqueeSelectedIndex >= 0 && _marqueeSelectedIndex < _listView.Items.Count)
+ _listView.RedrawItems(_marqueeSelectedIndex, _marqueeSelectedIndex, true);
+ }
+
+ private void ComputeMarqueeMaxForSelectedRow()
+ {
+ if (_marqueeSelectedIndex < 0 || _marqueeSelectedIndex >= _listView.Items.Count)
+ {
+ for (int i = 0; i < _marqueeMax.Length; i++) _marqueeMax[i] = 0f;
+ return;
+ }
+
+ var item = _listView.Items[_marqueeSelectedIndex];
+ int colCount = _listView.Columns.Count;
+ int minOverflow = Math.Max(0, MinOverflowForMarqueePx);
+
+ for (int col = 0; col < colCount; col++)
+ {
+ string text = (col == 0)
+ ? (item.Text ?? "")
+ : (col < item.SubItems.Count ? (item.SubItems[col]?.Text ?? "") : "");
+
+ if (string.IsNullOrEmpty(text))
+ {
+ _marqueeMax[col] = 0f;
+ continue;
+ }
+
+ int available = Math.Max(0, _listView.Columns[col].Width - CellPaddingTotalX);
+ if (available <= 6)
+ {
+ _marqueeMax[col] = 0f;
+ continue;
+ }
+
+ Size measured = TextRenderer.MeasureText(
+ text,
+ ItemFontBold,
+ new Size(int.MaxValue, ItemFontBold.Height),
+ TextFormatFlags.SingleLine | TextFormatFlags.NoPadding);
+
+ float overflow = measured.Width - available;
+ _marqueeMax[col] = (overflow > minOverflow) ? overflow : 0f;
+ }
+ }
+
+ private void UpdateMarqueeTimerState()
+ {
+ if (!MarqueeEnabled)
+ {
+ if (_marqueeTimer.Enabled) _marqueeTimer.Stop();
+ return;
+ }
+
+ if (MarqueeOnlyWhenFocused && !_listView.Focused)
+ {
+ if (_marqueeTimer.Enabled) _marqueeTimer.Stop();
+ return;
+ }
+
+ bool any = false;
+ for (int i = 0; i < _marqueeMax.Length; i++)
+ {
+ if (_marqueeMax[i] > 1f) { any = true; break; }
+ }
+
+ if (any && _marqueeSelectedIndex >= 0)
+ {
+ if (!_marqueeTimer.Enabled)
+ {
+ _marqueeLastTickMs = _marqueeSw.ElapsedMilliseconds;
+ _marqueeTimer.Start();
+ }
+ }
+ else
+ {
+ if (_marqueeTimer.Enabled)
+ _marqueeTimer.Stop();
+ }
+ }
+
+ private void UpdateMarquee()
+ {
+ if (!MarqueeEnabled) { _marqueeTimer.Stop(); return; }
+ if (MarqueeOnlyWhenFocused && !_listView.Focused) { _marqueeTimer.Stop(); return; }
+
+ var active = GetActiveSelectedItem();
+ int idx = active != null ? active.Index : -1;
+ if (idx != _marqueeSelectedIndex)
+ {
+ _marqueeSelectedIndex = idx;
+ ResetMarqueeState();
+ return;
+ }
+
+ if (_marqueeSelectedIndex < 0 || _marqueeSelectedIndex >= _listView.Items.Count)
+ {
+ _marqueeTimer.Stop();
+ return;
+ }
+
+ long nowMs = _marqueeSw.ElapsedMilliseconds;
+ float dt = (nowMs - _marqueeLastTickMs) / 1000f;
+ _marqueeLastTickMs = nowMs;
+
+ if (dt <= 0) return;
+ if (dt > 0.08f) dt = 0.08f;
+
+ if (nowMs < _marqueeStartAtMs) return;
+
+ bool changed = false;
+ int pauseMs = Math.Max(0, MarqueePauseMs);
+ float speed = Math.Max(1f, MarqueeSpeedPxPerSecond);
+
+ int overshoot = GetEffectiveOvershootPx();
+
+ for (int col = 0; col < _marqueeOffsets.Length; col++)
+ {
+ float overflow = _marqueeMax[col];
+ if (overflow <= 1f)
+ {
+ if (_marqueeOffsets[col] != 0f || _marqueeProgress[col] != 0f)
+ {
+ _marqueeOffsets[col] = 0f;
+ _marqueeProgress[col] = 0f;
+ _marqueeHoldMs[col] = 0;
+ _marqueeDirs[col] = 1;
+ changed = true;
+ }
+ continue;
+ }
+
+ if (_marqueeHoldMs[col] > 0)
+ {
+ _marqueeHoldMs[col] -= (int)Math.Round(dt * 1000);
+ if (_marqueeHoldMs[col] < 0) _marqueeHoldMs[col] = 0;
+ continue;
+ }
+
+ int dir = _marqueeDirs[col];
+ if (dir == 0) dir = 1;
+
+ float travel = overflow + (2f * overshoot);
+
+ float pSpeed = speed / (1.5f * travel);
+ pSpeed = Math.Max(pSpeed, MarqueeMinProgressPerSecond);
+
+ float p = _marqueeProgress[col] + dir * dt * pSpeed;
+
+ if (p <= 0f)
+ {
+ p = 0f;
+ _marqueeDirs[col] = 1;
+ _marqueeHoldMs[col] = pauseMs;
+ }
+ else if (p >= 1f)
+ {
+ p = 1f;
+ _marqueeDirs[col] = -1;
+ _marqueeHoldMs[col] = pauseMs;
+ }
+
+ float newOffset = (Ease(p) * travel) - overshoot;
+
+ if (Math.Abs(newOffset - _marqueeOffsets[col]) > 0.02f || Math.Abs(p - _marqueeProgress[col]) > 0.0005f)
+ {
+ _marqueeProgress[col] = p;
+ _marqueeOffsets[col] = newOffset;
+ changed = true;
+ }
+ }
+
+ if (changed)
+ _listView.RedrawItems(_marqueeSelectedIndex, _marqueeSelectedIndex, true);
+ }
+
+ private static float Ease(float p)
+ {
+ p = Clamp01(p);
+ float t = 0.5f - 0.5f * (float)Math.Cos(Math.PI * p);
+ const float gamma = 0.65f;
+ return (float)Math.Pow(t, gamma);
+ }
+
+ private static float Clamp01(float v)
+ {
+ if (v < 0f) return 0f;
+ if (v > 1f) return 1f;
+ return v;
+ }
+ }
+}
\ No newline at end of file