diff --git a/src/CodingWithCalvin.LaunchyBar/LaunchyBarPackage.cs b/src/CodingWithCalvin.LaunchyBar/LaunchyBarPackage.cs index 72abd2a..3bf0976 100644 --- a/src/CodingWithCalvin.LaunchyBar/LaunchyBarPackage.cs +++ b/src/CodingWithCalvin.LaunchyBar/LaunchyBarPackage.cs @@ -20,6 +20,7 @@ public sealed class LaunchyBarPackage : AsyncPackage private ILaunchService? _launchService; private IShellInjectionService? _shellInjectionService; private DebugStateService? _debugStateService; + private ToolWindowMonitorService? _toolWindowMonitorService; protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { @@ -59,6 +60,7 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke if (_shellInjectionService.Inject()) { _debugStateService = new DebugStateService(this, _configurationService); + _toolWindowMonitorService = new ToolWindowMonitorService(this, _configurationService); break; } @@ -78,6 +80,7 @@ protected override void Dispose(bool disposing) { if (disposing) { + _toolWindowMonitorService?.Dispose(); _debugStateService?.Dispose(); _shellInjectionService?.Dispose(); Instance = null; diff --git a/src/CodingWithCalvin.LaunchyBar/Models/LaunchItem.cs b/src/CodingWithCalvin.LaunchyBar/Models/LaunchItem.cs index fb3e5a5..1166b6d 100644 --- a/src/CodingWithCalvin.LaunchyBar/Models/LaunchItem.cs +++ b/src/CodingWithCalvin.LaunchyBar/Models/LaunchItem.cs @@ -14,6 +14,7 @@ public sealed class LaunchItem : INotifyPropertyChanged { private string _iconPath = string.Empty; private string _name = string.Empty; + private bool _isActive; /// public event PropertyChangedEventHandler? PropertyChanged; @@ -91,6 +92,23 @@ public string IconPath /// public bool IsEnabled { get; set; } = true; + /// + /// Whether this item's tool window is currently visible. + /// + [JsonIgnore] + public bool IsActive + { + get => _isActive; + set + { + if (_isActive != value) + { + _isActive = value; + OnPropertyChanged(); + } + } + } + /// /// Gets the ImageMoniker for this item's icon. /// diff --git a/src/CodingWithCalvin.LaunchyBar/Services/ToolWindowMonitorService.cs b/src/CodingWithCalvin.LaunchyBar/Services/ToolWindowMonitorService.cs new file mode 100644 index 0000000..a3140bb --- /dev/null +++ b/src/CodingWithCalvin.LaunchyBar/Services/ToolWindowMonitorService.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Threading; +using CodingWithCalvin.LaunchyBar.Models; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +namespace CodingWithCalvin.LaunchyBar.Services; + +/// +/// Monitors tool window visibility and updates IsActive on configured launch items. +/// +public sealed class ToolWindowMonitorService : IDisposable +{ + private readonly AsyncPackage _package; + private readonly IConfigurationService _configurationService; + private readonly DispatcherTimer _timer; + private bool _disposed; + + private static readonly Dictionary ToolWindowGuids = new(StringComparer.OrdinalIgnoreCase) + { + { "View.SolutionExplorer", new Guid(ToolWindowGuids80.SolutionExplorer) }, + { "View.Output", new Guid(ToolWindowGuids80.Outputwindow) }, + { "View.ErrorList", new Guid(ToolWindowGuids80.ErrorList) }, + { "View.TaskList", new Guid(ToolWindowGuids80.TaskList) }, + { "View.Toolbox", new Guid(ToolWindowGuids80.Toolbox) }, + { "View.PropertiesWindow", new Guid(ToolWindowGuids80.PropertiesWindow) }, + { "View.ClassView", new Guid(ToolWindowGuids80.ClassView) }, + { "View.Terminal", new Guid("d212f56b-c48a-434c-a121-1c5d80b59b9f") }, + { "View.GitWindow", new Guid("1c64b9c2-e352-428e-a56d-0ace190b99a6") }, + }; + + public ToolWindowMonitorService(AsyncPackage package, IConfigurationService configurationService) + { + _package = package; + _configurationService = configurationService; + + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(500) + }; + _timer.Tick += OnTimerTick; + _timer.Start(); + } + + private void OnTimerTick(object sender, EventArgs e) + { + ThreadHelper.ThrowIfNotOnUIThread(); + UpdateActiveStates(); + } + + private void UpdateActiveStates() + { + ThreadHelper.ThrowIfNotOnUIThread(); + + var shell = _package.GetService(); + if (shell == null) return; + + // Build a set of visible tool window GUIDs + var visibleGuids = new HashSet(); + shell.GetToolWindowEnum(out var windowEnum); + if (windowEnum != null) + { + var frames = new IVsWindowFrame[1]; + while (windowEnum.Next(1, frames, out var fetched) == 0 && fetched == 1) + { + var frame = frames[0]; + if (frame == null) continue; + + try + { + frame.GetGuidProperty((int)__VSFPROPID.VSFPROPID_GuidPersistenceSlot, out var persistGuid); + frame.IsOnScreen(out var isOnScreen); + if (isOnScreen != 0) + { + visibleGuids.Add(persistGuid); + } + } + catch + { + // Some frames may throw + } + } + } + + // Update IsActive on each configured tool window item + foreach (var item in _configurationService.Configuration.Items + .Where(i => i.Type == LaunchItemType.ToolWindow)) + { + if (ToolWindowGuids.TryGetValue(item.Target, out var guid)) + { + item.IsActive = visibleGuids.Contains(guid); + } + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + _timer.Stop(); + _timer.Tick -= OnTimerTick; + } +} diff --git a/src/CodingWithCalvin.LaunchyBar/UI/LaunchyBarControl.xaml b/src/CodingWithCalvin.LaunchyBar/UI/LaunchyBarControl.xaml index 6e97207..a3fed0d 100644 --- a/src/CodingWithCalvin.LaunchyBar/UI/LaunchyBarControl.xaml +++ b/src/CodingWithCalvin.LaunchyBar/UI/LaunchyBarControl.xaml @@ -10,11 +10,13 @@ Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}"> + +