Browse Source

Indeterminate Windows taskbar progress while decompiling

Assisted-by: Claude:claude-opus-4-7:Claude Code

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
fd86239a53
  1. 28
      ILSpy.Tests/MainWindow/MainWindowTests.cs
  2. 146
      ILSpy/TaskbarProgressService.cs
  3. 15
      ILSpy/TextView/DecompilerTabPageModel.cs

28
ILSpy.Tests/MainWindow/MainWindowTests.cs

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
@ -24,8 +25,10 @@ using Avalonia.VisualTree; @@ -24,8 +25,10 @@ using Avalonia.VisualTree;
using AwesomeAssertions;
using ILSpy;
using ILSpy.AppEnv;
using ILSpy.AssemblyTree;
using ILSpy.TreeNodes;
using ILSpy.ViewModels;
using ILSpy.Views;
@ -60,4 +63,29 @@ public class MainWindowTests @@ -60,4 +63,29 @@ public class MainWindowTests
pane.Bounds.Width.Should().BeGreaterThan(0);
pane.Bounds.Height.Should().BeGreaterThan(0);
}
[AvaloniaTest]
public async Task Taskbar_Progress_Goes_Indeterminate_While_Decompiling_Then_Clears()
{
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3);
var service = AppComposition.Current.GetExport<TaskbarProgressService>();
var states = new List<TaskbarProgressState>();
void Observe(TaskbarProgressState s) => states.Add(s);
service.StateChanged += Observe;
var node = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>("System.Linq");
vm.AssemblyTreeModel.SelectedItem = node;
await vm.DockWorkspace.WaitForDecompiledTextAsync();
service.StateChanged -= Observe;
states.Should().Contain(TaskbarProgressState.Indeterminate,
"taskbar must show indeterminate progress while a decompile is in flight");
states.Last().Should().Be(TaskbarProgressState.None,
"taskbar progress must clear after decompile completes");
}
}

146
ILSpy/TaskbarProgressService.cs

@ -0,0 +1,146 @@ @@ -0,0 +1,146 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Composition;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
namespace ILSpy
{
public enum TaskbarProgressState
{
None,
Indeterminate,
Normal,
}
/// <summary>
/// Window-level taskbar progress indicator. Mirrors WPF's
/// <c>System.Windows.Shell.TaskbarItemInfo</c> just enough for the decompile spinner —
/// only the <see cref="TaskbarProgressState.Indeterminate"/> / <c>None</c> cycle is wired
/// up so far. On non-Windows hosts the state is still tracked but the OS hook is a no-op.
/// </summary>
[Export]
[Shared]
public class TaskbarProgressService
{
public TaskbarProgressState State { get; private set; }
public event Action<TaskbarProgressState>? StateChanged;
public void SetState(TaskbarProgressState state)
{
if (State == state)
return;
State = state;
ApplyToOs(state);
StateChanged?.Invoke(state);
}
void ApplyToOs(TaskbarProgressState state)
{
if (!OperatingSystem.IsWindows())
return;
var hwnd = TryGetMainWindowHandle();
if (hwnd == IntPtr.Zero)
return;
ApplyToWindowsTaskbar(hwnd, state);
}
static IntPtr TryGetMainWindowHandle()
{
var window = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
return window?.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
}
[SupportedOSPlatform("windows")]
static void ApplyToWindowsTaskbar(IntPtr hwnd, TaskbarProgressState state)
{
var taskbar = WindowsTaskbarList.Instance;
if (taskbar == null)
return;
taskbar.SetProgressState(hwnd, state switch {
TaskbarProgressState.Indeterminate => TBPFLAG.TBPF_INDETERMINATE,
TaskbarProgressState.Normal => TBPFLAG.TBPF_NORMAL,
_ => TBPFLAG.TBPF_NOPROGRESS,
});
}
}
[SupportedOSPlatform("windows")]
static class WindowsTaskbarList
{
static ITaskbarList3? instance;
static bool initialised;
public static ITaskbarList3? Instance {
get {
if (initialised)
return instance;
initialised = true;
try
{
var t = Type.GetTypeFromCLSID(new Guid("56FDF344-FD6D-11d0-958A-006097C9A090"));
if (t == null)
return null;
var obj = Activator.CreateInstance(t);
if (obj is ITaskbarList3 list)
{
list.HrInit();
instance = list;
}
}
catch
{
// COM init failed — leave instance null and silently fall back.
}
return instance;
}
}
}
internal enum TBPFLAG
{
TBPF_NOPROGRESS = 0,
TBPF_INDETERMINATE = 0x1,
TBPF_NORMAL = 0x2,
}
[ComImport]
[Guid("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface ITaskbarList3
{
// ITaskbarList
void HrInit();
void AddTab(IntPtr hwnd);
void DeleteTab(IntPtr hwnd);
void ActivateTab(IntPtr hwnd);
void SetActiveAlt(IntPtr hwnd);
// ITaskbarList2
void MarkFullscreenWindow(IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen);
// ITaskbarList3
void SetProgressValue(IntPtr hwnd, ulong completed, ulong total);
void SetProgressState(IntPtr hwnd, TBPFLAG flags);
}
}

15
ILSpy/TextView/DecompilerTabPageModel.cs

@ -142,6 +142,19 @@ namespace ILSpy.TextView @@ -142,6 +142,19 @@ namespace ILSpy.TextView
Title = IsDecompiling ? ComposeSpinnerTitle(0, text) : text;
}
// Resolved lazily so unit tests / design-time previews that bypass the composition host
// still construct cleanly.
TaskbarProgressService? taskbarProgress;
TaskbarProgressService? TaskbarProgress
=> taskbarProgress ??= TryGetExport<TaskbarProgressService>();
static T? TryGetExport<T>() where T : class
{
try
{ return AppEnv.AppComposition.Current.GetExport<T>(); }
catch { return null; }
}
public DecompilerTabPageModel()
{
Title = "Empty";
@ -168,6 +181,7 @@ namespace ILSpy.TextView @@ -168,6 +181,7 @@ namespace ILSpy.TextView
// Spinner appears as a glyph prefix on the tab title while the decompile runs;
// editor state is left untouched so cancellation falls back cleanly.
Title = ComposeSpinnerTitle(0, currentNode?.Text?.ToString() ?? "(unnamed)");
TaskbarProgress?.SetState(TaskbarProgressState.Indeterminate);
_ = RunSpinnerAsync(cts.Token);
try
@ -235,6 +249,7 @@ namespace ILSpy.TextView @@ -235,6 +249,7 @@ namespace ILSpy.TextView
// from the title — the editor still shows the previous decompile.
if (currentNode != null)
Title = currentNode.Text?.ToString() ?? "(unnamed)";
TaskbarProgress?.SetState(TaskbarProgressState.None);
}
if (Dispatcher.UIThread.CheckAccess())
StopSpinner();

Loading…
Cancel
Save