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
16 changes: 13 additions & 3 deletions src/WindowController.App/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public partial class App : Application
private ProfileApplier? _profileApplier;
private ProfileStore? _profileStore;
private AppSettingsStore? _appSettingsStore;
private VirtualDesktopService? _vdService;
private ILogger? _log;

protected override void OnStartup(StartupEventArgs e)
Expand Down Expand Up @@ -79,14 +80,15 @@ protected override void OnStartup(StartupEventArgs e)

var urlRetriever = new BrowserUrlRetriever(_log);
var enumerator = new WindowEnumerator(_log, (hwnd, exe) => urlRetriever.TryGetUrl(hwnd, exe));
var arranger = new WindowArranger(_log);
var arranger = new WindowArranger(_log, _profileStore.Data.Settings);
var hookManager = new WinEventHookManager(_log);
_syncManager = new SyncManager(_profileStore, enumerator, hookManager, _log);
_vdService = new VirtualDesktopService(_log);

// Profile applier for hotkey access
_profileApplier = new ProfileApplier(_profileStore, enumerator, arranger, () => _syncManager.ScheduleRebuild(), _log);

_viewModel = new MainViewModel(_profileStore, enumerator, arranger, urlRetriever, _syncManager, _appSettingsStore, _log);
_viewModel = new MainViewModel(_profileStore, enumerator, arranger, urlRetriever, _syncManager, _vdService, _profileApplier, _appSettingsStore, _log);
_viewModel.Initialize();

// Start sync hooks if enabled
Expand Down Expand Up @@ -176,7 +178,13 @@ private void RegisterAllHotkeys()
{
if (_profileApplier != null)
{
var result = await _profileApplier.ApplyByIdAsync(capturedProfileId, false);
nint appHwnd = 0;
if (_mainWindow != null)
{
var helper = new WindowInteropHelper(_mainWindow);
appHwnd = helper.Handle;
}
var result = await _profileApplier.ApplyByIdAsync(capturedProfileId, false, appHwnd);
var profile = _profileStore?.FindById(capturedProfileId);
var name = profile?.Name ?? capturedProfileId;
if (_viewModel != null)
Expand Down Expand Up @@ -250,6 +258,7 @@ private void ExitApp()
_log?.Information("Window-Controller exiting");
_hotkeyManager?.Dispose();
_syncManager?.Dispose();
_vdService?.Dispose();
if (_trayIcon != null)
{
_trayIcon.Dispose();
Expand All @@ -263,6 +272,7 @@ protected override void OnExit(ExitEventArgs e)
{
_hotkeyManager?.Dispose();
_syncManager?.Dispose();
_vdService?.Dispose();
_trayIcon?.Dispose();
Log.CloseAndFlush();
_singleInstanceMutex?.ReleaseMutex();
Expand Down
68 changes: 68 additions & 0 deletions src/WindowController.App/DesktopPickerWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<ui:FluentWindow x:Class="WindowController.App.DesktopPickerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="仮想デスクトップを選択"
Width="440" SizeToContent="Height"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
Topmost="True"
ShowInTaskbar="False"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<ui:TitleBar Grid.Row="0" Title="仮想デスクトップを選択"
ShowMaximize="False" ShowMinimize="False"/>

<StackPanel Grid.Row="1" Margin="24,0,24,24">
<TextBlock Text="配置先のデスクトップを選択してください"
Style="{StaticResource SectionHeader}" Margin="0,0,0,4"/>
<TextBlock x:Name="MonitorInfoText"
Style="{StaticResource SectionDescription}"/>

<Border Style="{StaticResource SectionCard}">
<ItemsControl x:Name="DesktopList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:Button Click="DesktopButton_Click"
Margin="0,3" Padding="16,12"
HorizontalContentAlignment="Left"
HorizontalAlignment="Stretch"
Appearance="Secondary">
<ui:Button.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding NumberLabel}" FontSize="14"
FontWeight="SemiBold" Margin="0,0,12,0"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding DisplayName}" FontSize="14"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding CurrentBadge}" FontSize="11"
Foreground="{DynamicResource SystemAccentColorPrimaryBrush}"
VerticalAlignment="Center" Margin="8,0,0,0"/>
</StackPanel>
</ui:Button.Content>
</ui:Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>

<ui:Button Click="Cancel_Click"
Margin="0,12,0,0" Padding="16,8"
HorizontalAlignment="Right"
Appearance="Secondary">
<ui:Button.Icon>
<ui:SymbolIcon Symbol="Dismiss24"/>
</ui:Button.Icon>
<ui:Button.Content>
<TextBlock Text="キャンセル"/>
</ui:Button.Content>
</ui:Button>
</StackPanel>
</Grid>
</ui:FluentWindow>
86 changes: 86 additions & 0 deletions src/WindowController.App/DesktopPickerWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Windows;
using System.Windows.Input;
using WindowController.Win32;
using Wpf.Ui.Controls;

namespace WindowController.App;

/// <summary>
/// Item shown in the desktop picker list.
/// </summary>
public class DesktopPickerItem
{
public int Number { get; init; }
public Guid DesktopId { get; init; }
public string NumberLabel => $"{Number}:";
public string DisplayName { get; init; } = "";
public string CurrentBadge { get; init; } = "";
}

/// <summary>
/// Modal dialog that lists available virtual desktops and lets the user pick one.
/// Supports keyboard shortcuts (1–9, NumPad, Escape).
/// </summary>
public partial class DesktopPickerWindow : FluentWindow
{
private readonly List<DesktopPickerItem> _items;
public Guid? SelectedDesktopId { get; private set; }

public DesktopPickerWindow(
List<VirtualDesktopService.VirtualDesktopInfo> desktops,
Guid? currentDesktopId,
string monitorDescription)
{
InitializeComponent();
MonitorInfoText.Text = monitorDescription;

_items = desktops.Select(d => new DesktopPickerItem
{
Number = d.Number,
DesktopId = d.Id,
DisplayName = string.IsNullOrEmpty(d.Name)
? $"デスクトップ {d.Number}"
: d.Name,
CurrentBadge = d.Id == currentDesktopId ? "(現在)" : ""
}).ToList();
DesktopList.ItemsSource = _items;
}

private void DesktopButton_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement fe && fe.DataContext is DesktopPickerItem item)
{
SelectedDesktopId = item.DesktopId;
DialogResult = true;
}
}

private void Cancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}

protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);

int num = -1;
if (e.Key >= Key.D1 && e.Key <= Key.D9)
num = e.Key - Key.D0;
else if (e.Key >= Key.NumPad1 && e.Key <= Key.NumPad9)
num = e.Key - Key.NumPad0;
else if (e.Key == Key.Escape)
{
DialogResult = false;
e.Handled = true;
return;
}

if (num >= 1 && num <= _items.Count)
{
SelectedDesktopId = _items[num - 1].DesktopId;
DialogResult = true;
e.Handled = true;
}
}
}
9 changes: 9 additions & 0 deletions src/WindowController.App/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,14 @@
CellStyle="{StaticResource TransparentCell}"
PreviewMouseWheel="DataGrid_PreviewMouseWheel"
Margin="0,0,0,10">
<DataGrid.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="モニターを選択して配置"
Command="{Binding ApplyToMonitorCommand}"/>
<MenuItem Header="仮想デスクトップを選択して配置 ※整備中"
Command="{Binding ApplyToDesktopAndMonitorCommand}"/>
Comment on lines 174 to 179
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In WPF, ContextMenu does not inherit DataContext from its placement target, so Command="{Binding ApplyToMonitorCommand}" will likely resolve to null and the menu items won’t execute. Bind via PlacementTarget.DataContext (or explicitly set ContextMenu.DataContext) so the commands come from MainViewModel.

Copilot uses AI. Check for mistakes.
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTemplateColumn Header="連動" Width="60" MinWidth="60">
<DataGridTemplateColumn.CellStyle>
Expand All @@ -192,6 +200,7 @@
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Profile" Binding="{Binding Name, UpdateSourceTrigger=LostFocus}" Width="*"/>
<DataGridTextColumn Header="Desktop" Binding="{Binding TargetDesktopLabel}" Width="100" IsReadOnly="True"/>
<DataGridTextColumn Header="Windows" Binding="{Binding WindowCount}" Width="80" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
Expand Down
24 changes: 24 additions & 0 deletions src/WindowController.App/MonitorOverlayWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Window x:Class="WindowController.App.MonitorOverlayWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
Topmost="True"
ShowInTaskbar="False"
ShowActivated="False"
Focusable="False"
ResizeMode="NoResize"
Width="400" Height="320"
Left="-10000" Top="-10000">
<Border CornerRadius="20" Background="#DD1e1e2e">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock x:Name="NumberText" FontSize="160" FontWeight="Bold"
Foreground="White" HorizontalAlignment="Center"
Margin="0,-8,0,0"/>
<TextBlock x:Name="InfoText" FontSize="15"
Foreground="#AAAACC" HorizontalAlignment="Center"
TextAlignment="Center" LineHeight="22"/>
</StackPanel>
</Border>
</Window>
53 changes: 53 additions & 0 deletions src/WindowController.App/MonitorOverlayWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Windows;
using System.Windows.Interop;
using WindowController.Win32;

namespace WindowController.App;

/// <summary>
/// Semi-transparent overlay window that shows a large monitor number.
/// One instance is placed on each physical monitor while the monitor picker is open.
/// </summary>
public partial class MonitorOverlayWindow : Window
{
private readonly int _monX, _monY, _monW, _monH;

public MonitorOverlayWindow(int number, string info,
int monX, int monY, int monW, int monH)
{
InitializeComponent();
NumberText.Text = number.ToString();
InfoText.Text = info;
_monX = monX;
_monY = monY;
_monW = monW;
_monH = monH;
SourceInitialized += OnSourceInitialized;
}

/// <summary>
/// Position the overlay centered on the target monitor using physical pixel coordinates.
/// Done in SourceInitialized to avoid flicker (before the window is first rendered).
/// </summary>
private void OnSourceInitialized(object? sender, EventArgs e)
{
var hwnd = new WindowInteropHelper(this).Handle;
const int owPx = 400, ohPx = 320;

// WPF sizes are in DIPs; SetWindowPos expects device pixels.
// Set Width/Height so the physical size matches owPx/ohPx at current DPI.
var source = HwndSource.FromHwnd(hwnd);
var m = source?.CompositionTarget?.TransformToDevice;
var scaleX = m?.M11 ?? 1.0;
var scaleY = m?.M22 ?? 1.0;
if (scaleX <= 0) scaleX = 1.0;
if (scaleY <= 0) scaleY = 1.0;
Width = owPx / scaleX;
Height = ohPx / scaleY;

int cx = _monX + (_monW - owPx) / 2;
int cy = _monY + (_monH - ohPx) / 2;
NativeMethods.SetWindowPos(hwnd, 0, cx, cy, owPx, ohPx,
NativeMethods.SWP_NOACTIVATE | NativeMethods.SWP_NOZORDER);
Comment on lines 34 to 51
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetWindowPos is passed hard-coded pixel sizes/positions (ow/oh and monitor pixel coordinates), but the WPF window’s Width/Height are in DIPs and can be scaled by DPI. On non-100% scaling this can mis-size/misplace the overlay. Consider converting between DIPs and device pixels (via PresentationSource transforms) or setting Width/Height in code based on DPI before calling SetWindowPos.

Copilot uses AI. Check for mistakes.
}
}
60 changes: 60 additions & 0 deletions src/WindowController.App/MonitorPickerWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<ui:FluentWindow x:Class="WindowController.App.MonitorPickerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
Title="モニターを選択して配置"
Width="440" SizeToContent="Height"
MinHeight="0"
WindowStartupLocation="Manual"
ResizeMode="NoResize"
Topmost="True"
ShowInTaskbar="False"
ExtendsContentIntoTitleBar="True"
WindowBackdropType="Mica">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<ui:TitleBar Grid.Row="0" Title="モニターを選択して配置"
ShowMaximize="False" ShowMinimize="False"/>

<StackPanel Grid.Row="1" Margin="24,0,24,24">
<TextBlock Text="配置先のモニターを選択してください"
Style="{StaticResource SectionHeader}" Margin="0,0,0,4"/>
<TextBlock Text="番号キーまたはクリックで選択できます"
Style="{StaticResource SectionDescription}"/>

<Border Style="{StaticResource SectionCard}">
<ItemsControl x:Name="MonitorList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:Button Click="MonitorButton_Click"
Margin="0,3" Padding="16,12"
HorizontalContentAlignment="Left"
HorizontalAlignment="Stretch"
Appearance="Secondary">
<ui:Button.Content>
<TextBlock Text="{Binding Label}" FontSize="14"/>
</ui:Button.Content>
</ui:Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>

<ui:Button Click="Cancel_Click"
Margin="0,12,0,0" Padding="16,8"
HorizontalAlignment="Right"
Appearance="Secondary">
<ui:Button.Icon>
<ui:SymbolIcon Symbol="Dismiss24"/>
</ui:Button.Icon>
<ui:Button.Content>
<TextBlock Text="キャンセル"/>
</ui:Button.Content>
</ui:Button>
</StackPanel>
</Grid>
</ui:FluentWindow>
Loading