Skip to content
Open
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
21 changes: 20 additions & 1 deletion VdLabel/DesktopCatalog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,32 @@
<DockPanel HorizontalAlignment="Center">
<emoji:TextBlock
MaxHeight="120"
Margin="8"
Margin="4,8"
HorizontalAlignment="Center"
DockPanel.Dock="{Binding Position, Mode=OneWay}"
FontSize="28"
Text="{Binding Label}"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap" />
<ItemsControl
HorizontalAlignment="Center"
DockPanel.Dock="Bottom"
ItemsSource="{Binding WindowIcons}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Image
Width="24"
Height="24"
Margin="2"
Source="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid>
<ui:Image
CornerRadius="8"
Expand Down
17 changes: 14 additions & 3 deletions VdLabel/DesktopCatalogViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

Expand All @@ -10,6 +11,8 @@ internal sealed partial class DesktopCatalogViewModel : ObservableObject, IDispo
private readonly IVirualDesktopService virualDesktopService;
private readonly ICommandService commandService;
private readonly IConfigStore configStore;
private readonly IWindowMonitor windowMonitor;
private readonly IWindowIconCache windowIconCache;
private readonly int maxColumns;
[ObservableProperty]
private IReadOnlyList<DesktopViewModel> desktops = [];
Expand All @@ -28,11 +31,13 @@ internal sealed partial class DesktopCatalogViewModel : ObservableObject, IDispo
[ObservableProperty]
private double height;

public DesktopCatalogViewModel(IVirualDesktopService virualDesktopService, ICommandService commandService, IConfigStore configStore)
public DesktopCatalogViewModel(IVirualDesktopService virualDesktopService, ICommandService commandService, IConfigStore configStore, IWindowMonitor windowMonitor, IWindowIconCache windowIconCache)
{
this.virualDesktopService = virualDesktopService;
this.commandService = commandService;
this.configStore = configStore;
this.windowMonitor = windowMonitor;
this.windowIconCache = windowIconCache;
this.configStore.Saved += ConfigStore_Saved;
this.commandService.BadgeResultsUpdated += CommandService_BadgeResultsUpdated;
this.maxColumns = (int)(SystemParameters.PrimaryScreenWidth * 0.8 / 280);
Expand Down Expand Up @@ -69,7 +74,11 @@ private async void Setup()
var cached = this.commandService.GetBadgeResult(b.Id, c.Id);
return cached ?? new ResolvedBadge(b.Label, b.Color);
});
return new DesktopViewModel(i + 1, c, commandLabel, wallpaperPath, pos, resolvedBadges, ToggleBadgeAsync);
var windowIcons = this.windowMonitor.GetDesktopWindows(c.Id)
.Select(this.windowIconCache.Get)
.OfType<ImageSource>()
.ToArray();
return new DesktopViewModel(i + 1, c, commandLabel, wallpaperPath, pos, resolvedBadges, windowIcons, ToggleBadgeAsync);
})
.ToArray();
var currentDesktop = this.virualDesktopService.GetCurrent();
Expand Down Expand Up @@ -121,7 +130,7 @@ partial void OnLeftChanged(double value)
=> this.Left = (SystemParameters.PrimaryScreenWidth - this.Width) / 2;
}

internal class DesktopViewModel(int index, DesktopConfig desktopConfig, string? commandLabel, string? wallpaperPath, Dock pos, IReadOnlyDictionary<Guid, ResolvedBadge> resolvedBadges, Func<Guid, Guid, Task> toggleBadge)
internal class DesktopViewModel(int index, DesktopConfig desktopConfig, string? commandLabel, string? wallpaperPath, Dock pos, IReadOnlyDictionary<Guid, ResolvedBadge> resolvedBadges, IReadOnlyList<ImageSource> windowIcons, Func<Guid, Guid, Task> toggleBadge)
{
private readonly int index = index;
private DesktopConfig desktopConfig = desktopConfig;
Expand All @@ -141,6 +150,8 @@ internal class DesktopViewModel(int index, DesktopConfig desktopConfig, string?
.Select(id => resolvedBadges[id])
.ToArray();

public IReadOnlyList<ImageSource> WindowIcons { get; } = windowIcons;

public IReadOnlyList<BadgeMenuItem> BadgeMenuItems { get; } = resolvedBadges
.Select(kvp => new BadgeMenuItem(kvp.Key, kvp.Value, desktopConfig.Id, desktopConfig.BadgeIds.Contains(kvp.Key), toggleBadge))
.ToArray();
Expand Down
5 changes: 4 additions & 1 deletion VdLabel/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
.AddSingleton<VirtualDesktopService>()
.AddSingleton<CommandService>()
.AddSingleton<UpdateChecker>()
.AddSingleton<WindowMonitor>()
.AddHostedService(sp => sp.GetRequiredService<VirtualDesktopService>())
.AddHostedService(sp => sp.GetRequiredService<CommandService>())
.AddHostedService(sp => sp.GetRequiredService<UpdateChecker>())
.AddHostedService<WindowMonitor>()
.AddHostedService(sp => sp.GetRequiredService<WindowMonitor>())
.AddSingleton<IVirualDesktopService>(sp => sp.GetRequiredService<VirtualDesktopService>())
.AddSingleton<ICommandService>(sp => sp.GetRequiredService<CommandService>())
.AddSingleton<IWindowMonitor>(sp => sp.GetRequiredService<WindowMonitor>())
.AddSingleton<IWindowIconCache, WindowIconCache>()
.AddSingleton<IUpdateChecker>(sp => sp.GetRequiredService<UpdateChecker>())
.AddSingleton<IConfigStore, ConfigStore>()
.AddSingleton<IContentDialogService, ContentDialogService>()
Expand Down
43 changes: 43 additions & 0 deletions VdLabel/WindowIconCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Collections.Concurrent;
using System.Drawing;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace VdLabel;

interface IWindowIconCache
{
ImageSource? Get(string processPath);
}

sealed class WindowIconCache : IWindowIconCache
{
private readonly ConcurrentDictionary<string, Lazy<ImageSource?>> icons = new(StringComparer.OrdinalIgnoreCase);

public ImageSource? Get(string processPath)
=> this.icons.GetOrAdd(processPath, static path => new(() => Extract(path))).Value;

private static ImageSource? Extract(string processPath)
{
try
{
using var icon = Icon.ExtractAssociatedIcon(processPath);
if (icon is null)
{
return null;
}
var source = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromWidthAndHeight(64, 64));
source.Freeze();
return source;
}
catch (Exception)
{
return null;
}
}
}
56 changes: 40 additions & 16 deletions VdLabel/WindowMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,32 @@
using PInvoke;
using System.Text.RegularExpressions;
using WindowsDesktop;
using static Windows.Win32.PInvoke;
using static VdLabel.ProcessUtility;

namespace VdLabel;
class WindowMonitor(ILogger<WindowMonitor> logger, IConfigStore configStore) : BackgroundService

class WindowMonitor(ILogger<WindowMonitor> logger, IConfigStore configStore, App app) : BackgroundService, IWindowMonitor
{
private readonly ILogger<WindowMonitor> logger = logger;
private readonly IConfigStore configStore = configStore;
private readonly App app = app;
private readonly Dictionary<IntPtr, string> checkedWindows = [];
private bool needReload = true;
private TargetWindow[] targetWindows = [];
private Dictionary<Guid, List<string>> desktopWindows = [];

private record TargetWindow(Guid DesktopId, WindowMatchType MatchType, Regex Regex);

public IReadOnlyList<string> GetDesktopWindows(Guid desktopId)
=> this.desktopWindows.TryGetValue(desktopId, out var windows) ? windows : [];

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
this.logger.LogInformation("ウィンドウ監視開始");
await ReloadTargetProcess().ConfigureAwait(false);
this.configStore.Saved += ConfigStore_Saved;
await this.app.WaitForStartupAsync().ConfigureAwait(false);

while (!stoppingToken.IsCancellationRequested)
{
Expand Down Expand Up @@ -67,11 +73,8 @@ private void CheckWindows()
{
var now = DateTime.Now;
this.logger.LogDebug("ウィンドウチェック開始");
if (this.targetWindows.Length == 0)
{
return;
}
var windows = new HashSet<nint>();
var desktopWindows = new Dictionary<Guid, List<string>>();
User32.EnumWindows((hWnd, lParam) =>
{
windows.Add(hWnd);
Expand All @@ -93,6 +96,27 @@ private void CheckWindows()
return true;
}

if (GetProcessPath(processId) is not { } path)
{
return true;
}

if (VirtualDesktop.FromHwnd(hWnd) is not { } desktop)
{
return true;
}
if (!desktopWindows.TryGetValue(desktop.Id, out var processPaths))
{
processPaths = [];
desktopWindows.Add(desktop.Id, processPaths);
}
processPaths.Add(path);

if (this.targetWindows.Length == 0)
{
return true;
}

// ウィンドウタイトルが取得できない場合はスキップ
if (GetWindowTitle(hWnd) is not { } windowTitle)
{
Expand All @@ -106,12 +130,6 @@ private void CheckWindows()
}

// ウィンドウが所属するプロセスのパスが取得できない場合はスキップ
if (GetProcessPath(processId) is not { } path)
{
this.checkedWindows[hWnd] = windowTitle;
return true;
}

// ウィンドウが所属するプロセスのコマンドラインが取得できない場合はスキップ
if (GetCommandLine(processId) is not { } commandLine)
{
Expand All @@ -135,12 +153,12 @@ private void CheckWindows()
VirtualDesktop.PinWindow(hWnd);
}
}
else if (VirtualDesktop.FromId(target.DesktopId) is { } desktop)
else if (VirtualDesktop.FromId(target.DesktopId) is { } targetDesktop)
{
if (VirtualDesktop.FromHwnd(hWnd)?.Id != desktop.Id)
if (VirtualDesktop.FromHwnd(hWnd)?.Id != targetDesktop.Id)
{
this.logger.LogDebug($"ウィンドウ検出: {windowTitle} to {desktop.Name}");
VirtualDesktop.MoveToDesktop(hWnd, desktop);
this.logger.LogDebug($"ウィンドウ検出: {windowTitle} to {targetDesktop.Name}");
VirtualDesktop.MoveToDesktop(hWnd, targetDesktop);
}
}

Expand All @@ -159,6 +177,7 @@ private void CheckWindows()
{
this.checkedWindows.Remove(hWnd);
}
this.desktopWindows = desktopWindows;
this.logger.LogDebug($"ウィンドウチェック終了: {DateTime.Now - now}");
}

Expand All @@ -171,3 +190,8 @@ private static string GetCheckText(WindowMatchType type, string path, string com
_ => string.Empty,
};
}

interface IWindowMonitor
{
IReadOnlyList<string> GetDesktopWindows(Guid desktopId);
}