Skip to content

Commit 614c815

Browse files
committed
fix(injection): improve shell injection to modify main layout grid
- Fix System.Text.Json version to 8.0.5 for .NET Framework 4.8 compatibility - Add retry logic with delays for injection to wait for VS UI initialization - Change injection strategy from ContentPresenter manipulation to Grid column insertion - Add diagnostic logging throughout injection process - Handle grids with no column definitions by adding Star column for existing content - Bar now spans full height from title bar to status bar
1 parent f345b04 commit 614c815

4 files changed

Lines changed: 171 additions & 113 deletions

File tree

src/CodingWithCalvin.LaunchyBar/CodingWithCalvin.LaunchyBar.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<PrivateAssets>all</PrivateAssets>
1818
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
1919
</PackageReference>
20-
<PackageReference Include="System.Text.Json" Version="10.0.2" />
20+
<PackageReference Include="System.Text.Json" Version="8.0.5" />
2121
</ItemGroup>
2222

2323
<ItemGroup>

src/CodingWithCalvin.LaunchyBar/LaunchyBarPackage.cs

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics;
23
using System.Runtime.InteropServices;
34
using System.Threading;
45
using CodingWithCalvin.LaunchyBar.Options;
@@ -44,17 +45,49 @@ protected override async Task InitializeAsync(CancellationToken cancellationToke
4445

4546
builder.Initialize();
4647

47-
// Initialize services
48-
_configurationService = new ConfigurationService();
49-
_launchService = new LaunchService(this);
48+
try
49+
{
50+
Debug.WriteLine("LaunchyBar: Initializing services...");
5051

51-
// Delay injection slightly to ensure VS UI is fully loaded
52-
await Task.Delay(1000, cancellationToken);
53-
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
52+
// Initialize services
53+
_configurationService = new ConfigurationService();
54+
Debug.WriteLine("LaunchyBar: ConfigurationService created");
55+
56+
_launchService = new LaunchService(this);
57+
Debug.WriteLine("LaunchyBar: LaunchService created");
58+
59+
// Inject the bar into VS shell with retries
60+
Debug.WriteLine("LaunchyBar: Creating ShellInjectionService...");
61+
_shellInjectionService = new ShellInjectionService(_configurationService, _launchService);
62+
Debug.WriteLine("LaunchyBar: ShellInjectionService created");
63+
64+
// Retry injection until successful or max attempts reached
65+
const int maxAttempts = 10;
66+
for (int attempt = 1; attempt <= maxAttempts; attempt++)
67+
{
68+
Debug.WriteLine($"LaunchyBar: Injection attempt {attempt}/{maxAttempts}...");
69+
await Task.Delay(1000, cancellationToken);
70+
await JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
5471

55-
// Inject the bar into VS shell
56-
_shellInjectionService = new ShellInjectionService(_configurationService, _launchService);
57-
_shellInjectionService.Inject();
72+
var result = _shellInjectionService.Inject();
73+
if (result)
74+
{
75+
Debug.WriteLine("LaunchyBar: Injection successful!");
76+
break;
77+
}
78+
79+
Debug.WriteLine($"LaunchyBar: Attempt {attempt} failed, will retry...");
80+
}
81+
}
82+
catch (Exception ex)
83+
{
84+
Debug.WriteLine($"LaunchyBar: EXCEPTION - {ex.GetType().Name}: {ex.Message}");
85+
Debug.WriteLine($"LaunchyBar: Stack trace: {ex.StackTrace}");
86+
if (ex.InnerException != null)
87+
{
88+
Debug.WriteLine($"LaunchyBar: Inner exception - {ex.InnerException.GetType().Name}: {ex.InnerException.Message}");
89+
}
90+
}
5891
}
5992

6093
protected override void Dispose(bool disposing)

src/CodingWithCalvin.LaunchyBar/Services/ShellInjectionService.cs

Lines changed: 120 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
24
using System.Linq;
35
using System.Windows;
46
using System.Windows.Controls;
@@ -17,9 +19,8 @@ public sealed class ShellInjectionService : IShellInjectionService
1719
private readonly ILaunchService _launchService;
1820

1921
private LaunchyBarControl? _barControl;
20-
private Grid? _injectedGrid;
21-
private FrameworkElement? _originalContent;
22-
private ContentPresenter? _targetPresenter;
22+
private Grid? _targetGrid;
23+
private ColumnDefinition? _injectedColumn;
2324

2425
private const double BarWidth = 48;
2526

@@ -35,56 +36,91 @@ public bool Inject()
3536
{
3637
ThreadHelper.ThrowIfNotOnUIThread();
3738

39+
Debug.WriteLine("LaunchyBar: Inject() called");
40+
3841
if (IsInjected)
42+
{
43+
Debug.WriteLine("LaunchyBar: Already injected");
3944
return true;
45+
}
4046

4147
try
4248
{
4349
var mainWindow = Application.Current.MainWindow;
4450
if (mainWindow == null)
51+
{
52+
Debug.WriteLine("LaunchyBar: MainWindow is null");
4553
return false;
54+
}
55+
56+
Debug.WriteLine($"LaunchyBar: MainWindow found - {mainWindow.GetType().FullName}, Size: {mainWindow.ActualWidth}x{mainWindow.ActualHeight}");
4657

47-
// Find the main content area - we're looking for the area between toolbar and status bar
48-
// This requires walking VS's visual tree to find the right injection point
49-
var injectionTarget = FindInjectionTarget(mainWindow);
50-
if (injectionTarget == null)
58+
// Find the main layout grid
59+
_targetGrid = FindMainLayoutGrid(mainWindow);
60+
if (_targetGrid == null)
61+
{
62+
Debug.WriteLine("LaunchyBar: Could not find main layout grid");
5163
return false;
64+
}
5265

53-
_targetPresenter = injectionTarget;
54-
_originalContent = injectionTarget.Content as FrameworkElement;
66+
Debug.WriteLine($"LaunchyBar: Found target grid with {_targetGrid.RowDefinitions.Count} rows, {_targetGrid.ColumnDefinitions.Count} columns");
67+
Debug.WriteLine($"LaunchyBar: Grid size: {_targetGrid.ActualWidth}x{_targetGrid.ActualHeight}");
5568

56-
if (_originalContent == null)
57-
return false;
69+
// Log existing children
70+
foreach (UIElement child in _targetGrid.Children)
71+
{
72+
var childRow = Grid.GetRow(child);
73+
var childCol = Grid.GetColumn(child);
74+
var childRowSpan = Grid.GetRowSpan(child);
75+
var childColSpan = Grid.GetColumnSpan(child);
76+
Debug.WriteLine($"LaunchyBar: Child: {child.GetType().Name} at Row={childRow}, Col={childCol}, RowSpan={childRowSpan}, ColSpan={childColSpan}");
77+
}
5878

5979
// Create our bar control
6080
_barControl = new LaunchyBarControl(_configurationService, _launchService);
6181
_barControl.Width = BarWidth;
6282
_barControl.HorizontalAlignment = HorizontalAlignment.Left;
6383
_barControl.VerticalAlignment = VerticalAlignment.Stretch;
6484

65-
// Create a new grid to hold both the bar and the original content
66-
_injectedGrid = new Grid();
67-
_injectedGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(BarWidth) });
68-
_injectedGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
85+
// Check if grid originally had column definitions
86+
var hadColumns = _targetGrid.ColumnDefinitions.Count > 0;
87+
Debug.WriteLine($"LaunchyBar: Grid originally had {_targetGrid.ColumnDefinitions.Count} columns");
6988

70-
// Remove original content from its parent
71-
injectionTarget.Content = null;
89+
// If the grid had no columns, we need to add one for the existing content
90+
if (!hadColumns)
91+
{
92+
// Add a Star column for existing content first
93+
_targetGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
94+
Debug.WriteLine("LaunchyBar: Added Star column for existing content");
95+
}
7296

73-
// Add bar to column 0
74-
Grid.SetColumn(_barControl, 0);
75-
_injectedGrid.Children.Add(_barControl);
97+
// Insert our bar column at the beginning
98+
_injectedColumn = new ColumnDefinition { Width = new GridLength(BarWidth) };
99+
_targetGrid.ColumnDefinitions.Insert(0, _injectedColumn);
100+
101+
// Shift all existing children to the right by incrementing their column
102+
foreach (UIElement child in _targetGrid.Children)
103+
{
104+
var currentCol = Grid.GetColumn(child);
105+
Grid.SetColumn(child, currentCol + 1);
106+
Debug.WriteLine($"LaunchyBar: Shifted {child.GetType().Name} from col {currentCol} to {currentCol + 1}");
107+
}
76108

77-
// Add original content to column 1
78-
Grid.SetColumn(_originalContent, 1);
79-
_injectedGrid.Children.Add(_originalContent);
109+
// Add our bar at column 0, spanning all rows (top to bottom)
110+
Grid.SetColumn(_barControl, 0);
111+
Grid.SetRow(_barControl, 0);
112+
Grid.SetRowSpan(_barControl, Math.Max(1, _targetGrid.RowDefinitions.Count));
113+
_targetGrid.Children.Add(_barControl);
80114

81-
// Set our grid as the new content
82-
injectionTarget.Content = _injectedGrid;
115+
Debug.WriteLine("LaunchyBar: Injection successful!");
116+
Debug.WriteLine($"LaunchyBar: Bar at column 0, spanning {Grid.GetRowSpan(_barControl)} rows");
83117

84118
return true;
85119
}
86-
catch (Exception)
120+
catch (Exception ex)
87121
{
122+
Debug.WriteLine($"LaunchyBar: Exception during injection - {ex.Message}");
123+
Debug.WriteLine($"LaunchyBar: Stack trace: {ex.StackTrace}");
88124
Remove();
89125
return false;
90126
}
@@ -94,12 +130,25 @@ public void Remove()
94130
{
95131
ThreadHelper.ThrowIfNotOnUIThread();
96132

97-
if (_targetPresenter != null && _originalContent != null && _injectedGrid != null)
133+
if (_targetGrid != null && _barControl != null && _injectedColumn != null)
98134
{
99135
try
100136
{
101-
_injectedGrid.Children.Clear();
102-
_targetPresenter.Content = _originalContent;
137+
// Remove our bar
138+
_targetGrid.Children.Remove(_barControl);
139+
140+
// Shift all children back
141+
foreach (UIElement child in _targetGrid.Children)
142+
{
143+
var currentCol = Grid.GetColumn(child);
144+
if (currentCol > 0)
145+
{
146+
Grid.SetColumn(child, currentCol - 1);
147+
}
148+
}
149+
150+
// Remove our column
151+
_targetGrid.ColumnDefinitions.Remove(_injectedColumn);
103152
}
104153
catch
105154
{
@@ -108,108 +157,79 @@ public void Remove()
108157
}
109158

110159
_barControl = null;
111-
_injectedGrid = null;
112-
_originalContent = null;
113-
_targetPresenter = null;
160+
_targetGrid = null;
161+
_injectedColumn = null;
114162
}
115163

116164
/// <summary>
117-
/// Finds the ContentPresenter that contains the main VS content area.
118-
/// This is the area between the toolbar and status bar.
165+
/// Finds the main layout Grid in VS's visual tree.
166+
/// This should be the grid that contains the toolbar and main content area.
119167
/// </summary>
120-
private ContentPresenter? FindInjectionTarget(Window mainWindow)
168+
private Grid? FindMainLayoutGrid(Window mainWindow)
121169
{
122-
// Strategy: Walk the visual tree looking for a ContentPresenter
123-
// that contains the main dock/editor area.
124-
// This is fragile and may need adjustment for different VS versions.
125-
126-
// Look for a ContentPresenter with a specific name or structure
127-
// VS typically has a structure like:
128-
// MainWindow > ... > DockPanel > [Toolbar, ContentPresenter (main area), StatusBar]
129-
130-
return FindContentPresenterRecursive(mainWindow, 0);
170+
// Walk the visual tree to find the main layout grid
171+
// We're looking for a large grid that contains the main VS content
172+
return FindMainGridRecursive(mainWindow, 0);
131173
}
132174

133-
private ContentPresenter? FindContentPresenterRecursive(DependencyObject parent, int depth)
175+
private Grid? FindMainGridRecursive(DependencyObject parent, int depth)
134176
{
135-
if (depth > 20) // Prevent infinite recursion
177+
if (depth > 15)
136178
return null;
137179

138180
var childCount = VisualTreeHelper.GetChildrenCount(parent);
181+
var indent = new string(' ', depth * 2);
139182

140183
for (int i = 0; i < childCount; i++)
141184
{
142185
var child = VisualTreeHelper.GetChild(parent, i);
143186

144-
// Look for ContentPresenter that might be our target
145-
if (child is ContentPresenter cp)
187+
if (depth < 5)
146188
{
147-
// Check if this ContentPresenter's content looks like the main content area
148-
if (IsMainContentArea(cp))
149-
{
150-
return cp;
151-
}
189+
Debug.WriteLine($"LaunchyBar: {indent}[{depth}] {child.GetType().Name}");
152190
}
153191

154-
// Also check for Grid with DockPanel children - common VS structure
155192
if (child is Grid grid)
156193
{
157-
// Look for the main content grid that has the editor/tool area
158-
var result = FindContentPresenterRecursive(grid, depth + 1);
159-
if (result != null)
160-
return result;
161-
}
162-
163-
if (child is DockPanel dockPanel)
164-
{
165-
var result = FindContentPresenterRecursive(dockPanel, depth + 1);
166-
if (result != null)
167-
return result;
168-
}
169-
170-
if (child is Border border)
171-
{
172-
var result = FindContentPresenterRecursive(border, depth + 1);
173-
if (result != null)
174-
return result;
194+
// Look for a grid that:
195+
// 1. Is large enough
196+
// 2. Has row definitions (VS uses rows for title/toolbar/content/statusbar)
197+
// 3. Contains relevant VS controls
198+
if (grid.ActualWidth > 400 && grid.ActualHeight > 300)
199+
{
200+
Debug.WriteLine($"LaunchyBar: {indent} Grid candidate: {grid.ActualWidth}x{grid.ActualHeight}, Rows={grid.RowDefinitions.Count}, Cols={grid.ColumnDefinitions.Count}");
201+
202+
// Check if this grid contains VsToolBarHostControl or similar
203+
if (ContainsVsContent(grid))
204+
{
205+
Debug.WriteLine($"LaunchyBar: {indent} ** MATCHED - contains VS content **");
206+
return grid;
207+
}
208+
}
175209
}
176210

177-
if (child is Decorator decorator)
178-
{
179-
var result = FindContentPresenterRecursive(decorator, depth + 1);
180-
if (result != null)
181-
return result;
182-
}
211+
// Continue searching
212+
var result = FindMainGridRecursive(child, depth + 1);
213+
if (result != null)
214+
return result;
183215
}
184216

185217
return null;
186218
}
187219

188-
private bool IsMainContentArea(ContentPresenter cp)
220+
private bool ContainsVsContent(Grid grid)
189221
{
190-
// Heuristics to identify the main content area:
191-
// 1. Should be reasonably large
192-
// 2. Should contain dock-related content
193-
194-
if (cp.ActualWidth < 400 || cp.ActualHeight < 300)
195-
return false;
196-
197-
// Check if the content's type name contains dock-related keywords
198-
var content = cp.Content;
199-
if (content == null)
200-
return false;
201-
202-
var typeName = content.GetType().FullName ?? "";
203-
204-
// VS's main content area typically has these in the type hierarchy
205-
if (typeName.Contains("Dock") ||
206-
typeName.Contains("ViewManager") ||
207-
typeName.Contains("MainWindow") ||
208-
typeName.Contains("Workspace"))
222+
foreach (UIElement child in grid.Children)
209223
{
210-
return true;
224+
var typeName = child.GetType().Name;
225+
// Look for VS-specific controls that indicate this is the main layout grid
226+
if (typeName.Contains("ToolBar") ||
227+
typeName.Contains("DockPanel") ||
228+
typeName.Contains("MainWindowTitleBar"))
229+
{
230+
return true;
231+
}
211232
}
212-
213233
return false;
214234
}
215235

0 commit comments

Comments
 (0)