Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/CodingWithCalvin.LaunchyBar/LaunchyBarPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ServiceProgressData> progress)
{
Expand Down Expand Up @@ -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;
}

Expand All @@ -78,6 +80,7 @@ protected override void Dispose(bool disposing)
{
if (disposing)
{
_toolWindowMonitorService?.Dispose();
_debugStateService?.Dispose();
_shellInjectionService?.Dispose();
Instance = null;
Expand Down
18 changes: 18 additions & 0 deletions src/CodingWithCalvin.LaunchyBar/Models/LaunchItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public sealed class LaunchItem : INotifyPropertyChanged
{
private string _iconPath = string.Empty;
private string _name = string.Empty;
private bool _isActive;

/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
Expand Down Expand Up @@ -91,6 +92,23 @@ public string IconPath
/// </summary>
public bool IsEnabled { get; set; } = true;

/// <summary>
/// Whether this item's tool window is currently visible.
/// </summary>
[JsonIgnore]
public bool IsActive
{
get => _isActive;
set
{
if (_isActive != value)
{
_isActive = value;
OnPropertyChanged();
}
}
}

/// <summary>
/// Gets the ImageMoniker for this item's icon.
/// </summary>
Expand Down
106 changes: 106 additions & 0 deletions src/CodingWithCalvin.LaunchyBar/Services/ToolWindowMonitorService.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Monitors tool window visibility and updates IsActive on configured launch items.
/// </summary>
public sealed class ToolWindowMonitorService : IDisposable
{
private readonly AsyncPackage _package;
private readonly IConfigurationService _configurationService;
private readonly DispatcherTimer _timer;
private bool _disposed;

private static readonly Dictionary<string, Guid> 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<SVsUIShell, IVsUIShell>();
if (shell == null) return;

// Build a set of visible tool window GUIDs
var visibleGuids = new HashSet<Guid>();
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;
}
}
45 changes: 33 additions & 12 deletions src/CodingWithCalvin.LaunchyBar/UI/LaunchyBarControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,45 @@
Background="{DynamicResource {x:Static vs:VsBrushes.ToolWindowBackgroundKey}}">

<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibility"/>

<Style x:Key="LaunchyBarButtonStyle" TargetType="Button">
<Setter Property="Width" Value="40"/>
<Setter Property="Width" Value="44"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Margin" Value="4"/>
<Setter Property="Padding" Value="8"/>
<Setter Property="Margin" Value="0,4"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static vs:VsBrushes.CommandBarTextActiveKey}}"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<!-- Active indicator bar on the left edge -->
<Rectangle x:Name="ActiveIndicator"
Grid.Column="0"
Fill="{DynamicResource {x:Static vs:VsBrushes.AccentMediumKey}}"
RadiusX="1" RadiusY="1"
Margin="0,6"
Visibility="Collapsed"/>

<Border x:Name="Border"
Grid.Column="1"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
Padding="8">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background"
Expand All @@ -43,6 +61,9 @@
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5"/>
</Trigger>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter TargetName="ActiveIndicator" Property="Visibility" Value="Visible"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
Expand Down
Loading