Skip to content

Commit 10b7cc2

Browse files
authored
feat(tool-window): highlight active tool windows on launch bar (#15)
Add visual indicator showing which tool windows are currently open. A left-edge accent bar appears next to active tool window buttons, updated every 500ms via a monitor service.
1 parent 51becca commit 10b7cc2

4 files changed

Lines changed: 160 additions & 12 deletions

File tree

src/CodingWithCalvin.LaunchyBar/LaunchyBarPackage.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public sealed class LaunchyBarPackage : AsyncPackage
2020
private ILaunchService? _launchService;
2121
private IShellInjectionService? _shellInjectionService;
2222
private DebugStateService? _debugStateService;
23+
private ToolWindowMonitorService? _toolWindowMonitorService;
2324

2425
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
2526
{
@@ -59,6 +60,7 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
5960
if (_shellInjectionService.Inject())
6061
{
6162
_debugStateService = new DebugStateService(this, _configurationService);
63+
_toolWindowMonitorService = new ToolWindowMonitorService(this, _configurationService);
6264
break;
6365
}
6466

@@ -78,6 +80,7 @@ protected override void Dispose(bool disposing)
7880
{
7981
if (disposing)
8082
{
83+
_toolWindowMonitorService?.Dispose();
8184
_debugStateService?.Dispose();
8285
_shellInjectionService?.Dispose();
8386
Instance = null;

src/CodingWithCalvin.LaunchyBar/Models/LaunchItem.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public sealed class LaunchItem : INotifyPropertyChanged
1414
{
1515
private string _iconPath = string.Empty;
1616
private string _name = string.Empty;
17+
private bool _isActive;
1718

1819
/// <inheritdoc/>
1920
public event PropertyChangedEventHandler? PropertyChanged;
@@ -91,6 +92,23 @@ public string IconPath
9192
/// </summary>
9293
public bool IsEnabled { get; set; } = true;
9394

95+
/// <summary>
96+
/// Whether this item's tool window is currently visible.
97+
/// </summary>
98+
[JsonIgnore]
99+
public bool IsActive
100+
{
101+
get => _isActive;
102+
set
103+
{
104+
if (_isActive != value)
105+
{
106+
_isActive = value;
107+
OnPropertyChanged();
108+
}
109+
}
110+
}
111+
94112
/// <summary>
95113
/// Gets the ImageMoniker for this item's icon.
96114
/// </summary>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Windows.Threading;
5+
using CodingWithCalvin.LaunchyBar.Models;
6+
using Microsoft.VisualStudio.Shell;
7+
using Microsoft.VisualStudio.Shell.Interop;
8+
9+
namespace CodingWithCalvin.LaunchyBar.Services;
10+
11+
/// <summary>
12+
/// Monitors tool window visibility and updates IsActive on configured launch items.
13+
/// </summary>
14+
public sealed class ToolWindowMonitorService : IDisposable
15+
{
16+
private readonly AsyncPackage _package;
17+
private readonly IConfigurationService _configurationService;
18+
private readonly DispatcherTimer _timer;
19+
private bool _disposed;
20+
21+
private static readonly Dictionary<string, Guid> ToolWindowGuids = new(StringComparer.OrdinalIgnoreCase)
22+
{
23+
{ "View.SolutionExplorer", new Guid(ToolWindowGuids80.SolutionExplorer) },
24+
{ "View.Output", new Guid(ToolWindowGuids80.Outputwindow) },
25+
{ "View.ErrorList", new Guid(ToolWindowGuids80.ErrorList) },
26+
{ "View.TaskList", new Guid(ToolWindowGuids80.TaskList) },
27+
{ "View.Toolbox", new Guid(ToolWindowGuids80.Toolbox) },
28+
{ "View.PropertiesWindow", new Guid(ToolWindowGuids80.PropertiesWindow) },
29+
{ "View.ClassView", new Guid(ToolWindowGuids80.ClassView) },
30+
{ "View.Terminal", new Guid("d212f56b-c48a-434c-a121-1c5d80b59b9f") },
31+
{ "View.GitWindow", new Guid("1c64b9c2-e352-428e-a56d-0ace190b99a6") },
32+
};
33+
34+
public ToolWindowMonitorService(AsyncPackage package, IConfigurationService configurationService)
35+
{
36+
_package = package;
37+
_configurationService = configurationService;
38+
39+
_timer = new DispatcherTimer
40+
{
41+
Interval = TimeSpan.FromMilliseconds(500)
42+
};
43+
_timer.Tick += OnTimerTick;
44+
_timer.Start();
45+
}
46+
47+
private void OnTimerTick(object sender, EventArgs e)
48+
{
49+
ThreadHelper.ThrowIfNotOnUIThread();
50+
UpdateActiveStates();
51+
}
52+
53+
private void UpdateActiveStates()
54+
{
55+
ThreadHelper.ThrowIfNotOnUIThread();
56+
57+
var shell = _package.GetService<SVsUIShell, IVsUIShell>();
58+
if (shell == null) return;
59+
60+
// Build a set of visible tool window GUIDs
61+
var visibleGuids = new HashSet<Guid>();
62+
shell.GetToolWindowEnum(out var windowEnum);
63+
if (windowEnum != null)
64+
{
65+
var frames = new IVsWindowFrame[1];
66+
while (windowEnum.Next(1, frames, out var fetched) == 0 && fetched == 1)
67+
{
68+
var frame = frames[0];
69+
if (frame == null) continue;
70+
71+
try
72+
{
73+
frame.GetGuidProperty((int)__VSFPROPID.VSFPROPID_GuidPersistenceSlot, out var persistGuid);
74+
frame.IsOnScreen(out var isOnScreen);
75+
if (isOnScreen != 0)
76+
{
77+
visibleGuids.Add(persistGuid);
78+
}
79+
}
80+
catch
81+
{
82+
// Some frames may throw
83+
}
84+
}
85+
}
86+
87+
// Update IsActive on each configured tool window item
88+
foreach (var item in _configurationService.Configuration.Items
89+
.Where(i => i.Type == LaunchItemType.ToolWindow))
90+
{
91+
if (ToolWindowGuids.TryGetValue(item.Target, out var guid))
92+
{
93+
item.IsActive = visibleGuids.Contains(guid);
94+
}
95+
}
96+
}
97+
98+
public void Dispose()
99+
{
100+
if (_disposed) return;
101+
_disposed = true;
102+
103+
_timer.Stop();
104+
_timer.Tick -= OnTimerTick;
105+
}
106+
}

src/CodingWithCalvin.LaunchyBar/UI/LaunchyBarControl.xaml

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,45 @@
1010
Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}">
1111

1212
<UserControl.Resources>
13+
<BooleanToVisibilityConverter x:Key="BoolToVisibility"/>
14+
1315
<Style x:Key="LaunchyBarButtonStyle" TargetType="Button">
14-
<Setter Property="Width" Value="40"/>
16+
<Setter Property="Width" Value="44"/>
1517
<Setter Property="Height" Value="40"/>
16-
<Setter Property="Margin" Value="4"/>
17-
<Setter Property="Padding" Value="8"/>
18+
<Setter Property="Margin" Value="0,4"/>
19+
<Setter Property="Padding" Value="0"/>
1820
<Setter Property="BorderThickness" Value="0"/>
1921
<Setter Property="Background" Value="Transparent"/>
2022
<Setter Property="Foreground" Value="{DynamicResource {x:Static vs:VsBrushes.CommandBarTextActiveKey}}"/>
2123
<Setter Property="Cursor" Value="Hand"/>
2224
<Setter Property="Template">
2325
<Setter.Value>
2426
<ControlTemplate TargetType="Button">
25-
<Border x:Name="Border"
26-
Background="{TemplateBinding Background}"
27-
BorderBrush="{TemplateBinding BorderBrush}"
28-
BorderThickness="{TemplateBinding BorderThickness}"
29-
CornerRadius="4"
30-
Padding="{TemplateBinding Padding}">
31-
<ContentPresenter HorizontalAlignment="Center"
32-
VerticalAlignment="Center"/>
33-
</Border>
27+
<Grid>
28+
<Grid.ColumnDefinitions>
29+
<ColumnDefinition Width="3"/>
30+
<ColumnDefinition Width="*"/>
31+
</Grid.ColumnDefinitions>
32+
33+
<!-- Active indicator bar on the left edge -->
34+
<Rectangle x:Name="ActiveIndicator"
35+
Grid.Column="0"
36+
Fill="{DynamicResource {x:Static vs:VsBrushes.AccentMediumKey}}"
37+
RadiusX="1" RadiusY="1"
38+
Margin="0,6"
39+
Visibility="Collapsed"/>
40+
41+
<Border x:Name="Border"
42+
Grid.Column="1"
43+
Background="{TemplateBinding Background}"
44+
BorderBrush="{TemplateBinding BorderBrush}"
45+
BorderThickness="{TemplateBinding BorderThickness}"
46+
CornerRadius="4"
47+
Padding="8">
48+
<ContentPresenter HorizontalAlignment="Center"
49+
VerticalAlignment="Center"/>
50+
</Border>
51+
</Grid>
3452
<ControlTemplate.Triggers>
3553
<Trigger Property="IsMouseOver" Value="True">
3654
<Setter TargetName="Border" Property="Background"
@@ -43,6 +61,9 @@
4361
<Trigger Property="IsEnabled" Value="False">
4462
<Setter Property="Opacity" Value="0.5"/>
4563
</Trigger>
64+
<DataTrigger Binding="{Binding IsActive}" Value="True">
65+
<Setter TargetName="ActiveIndicator" Property="Visibility" Value="Visible"/>
66+
</DataTrigger>
4667
</ControlTemplate.Triggers>
4768
</ControlTemplate>
4869
</Setter.Value>

0 commit comments

Comments
 (0)