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
17 changes: 17 additions & 0 deletions src/SwitchifyPc.App/Chrome/SwitchifyTitleBar.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,23 @@
AutomationProperties.Name="Minimize"
Visibility="{Binding ShowMinimizeButton, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource BooleanToVisibilityConverter}}"
Click="MinimizeButton_Click" />
<Button x:Name="MaximizeButton"
Content="□"
Visibility="{Binding ShowMaximizeButton, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource BooleanToVisibilityConverter}}"
Click="MaximizeButton_Click">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource CaptionButton}">
<Setter Property="AutomationProperties.Name" Value="Maximize" />
<Style.Triggers>
<DataTrigger Binding="{Binding WindowState, RelativeSource={RelativeSource AncestorType=Window}}"
Value="Maximized">
<Setter Property="Content" Value="❐" />
<Setter Property="AutomationProperties.Name" Value="Restore" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button x:Name="CloseButton"
Content="x"
Style="{StaticResource CloseCaptionButton}"
Expand Down
41 changes: 40 additions & 1 deletion src/SwitchifyPc.App/Chrome/SwitchifyTitleBar.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public partial class SwitchifyTitleBar : WpfUserControl
typeof(SwitchifyTitleBar),
new PropertyMetadata(true));

public static readonly DependencyProperty ShowMaximizeButtonProperty = DependencyProperty.Register(
nameof(ShowMaximizeButton),
typeof(bool),
typeof(SwitchifyTitleBar),
new PropertyMetadata(false));

public static readonly DependencyProperty StatusTextProperty = DependencyProperty.Register(
nameof(StatusText),
typeof(string),
Expand Down Expand Up @@ -56,6 +62,12 @@ public bool ShowMinimizeButton
set => SetValue(ShowMinimizeButtonProperty, value);
}

public bool ShowMaximizeButton
{
get => (bool)GetValue(ShowMaximizeButtonProperty);
set => SetValue(ShowMaximizeButtonProperty, value);
}

public string? StatusText
{
get => (string?)GetValue(StatusTextProperty);
Expand All @@ -76,14 +88,25 @@ public bool ShowStatusBadge

private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount != 1 || IsInsideButton(e.OriginalSource as DependencyObject))
if (IsInsideButton(e.OriginalSource as DependencyObject))
{
return;
}

Window? window = Window.GetWindow(this);
if (window is null) return;

if (e.ClickCount == 2 && ShowMaximizeButton)
{
ToggleMaximize(window);
return;
}

if (e.ClickCount != 1)
{
return;
}

try
{
window.DragMove();
Expand All @@ -93,6 +116,15 @@ private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
}
}

private void MaximizeButton_Click(object sender, RoutedEventArgs e)
{
Window? window = Window.GetWindow(this);
if (window is not null)
{
ToggleMaximize(window);
}
}

private void MinimizeButton_Click(object sender, RoutedEventArgs e)
{
Window? window = Window.GetWindow(this);
Expand All @@ -107,6 +139,13 @@ private void CloseButton_Click(object sender, RoutedEventArgs e)
Window.GetWindow(this)?.Close();
}

private static void ToggleMaximize(Window window)
{
window.WindowState = window.WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
}

private static bool IsInsideButton(DependencyObject? source)
{
while (source is not null)
Expand Down
45 changes: 28 additions & 17 deletions src/SwitchifyPc.App/SwitchControlProfileWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
xmlns:chrome="clr-namespace:SwitchifyPc.App.Chrome"
Title="PC Switch Control profiles"
MinWidth="760"
MinHeight="560"
MinWidth="520"
MinHeight="300"
Width="900"
Height="690"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize"
WindowStartupLocation="Manual"
Icon="Assets/icon.ico"
FontFamily="Segoe UI"
Background="{DynamicResource ChromeBackground}"
Foreground="{DynamicResource Text}"
WindowStyle="None">
WindowStyle="None"
SizeChanged="Window_SizeChanged">
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome CaptionHeight="44"
CornerRadius="0"
Expand Down Expand Up @@ -345,14 +346,18 @@

<chrome:SwitchifyTitleBar Grid.Row="0"
TitleText="PC Switch Control profiles"
ShowMinimizeButton="True" />
ShowMinimizeButton="True"
ShowMaximizeButton="True" />

<Border Grid.Row="1"
<Border x:Name="ContentBackground"
Grid.Row="1"
Grid.RowSpan="2"
Margin="0,80,0,0"
Background="{DynamicResource AppBackground}" />

<StackPanel Grid.Row="1" Margin="24,20,24,18">
<StackPanel x:Name="IntroPanel"
Grid.Row="1"
Margin="24,20,24,18">
<TextBlock Text="PC Switch Control"
FontSize="18"
FontWeight="Bold"
Expand All @@ -363,18 +368,22 @@
Style="{StaticResource MutedText}" />
</StackPanel>

<Grid Grid.Row="2" Margin="24,0,24,24">
<Grid x:Name="ProfileBody"
Grid.Row="2"
Margin="24,0,24,24">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition x:Name="PrimaryContentRow" Height="*" />
<RowDefinition x:Name="SecondaryContentRow" Height="0" />
<RowDefinition x:Name="FooterRow" Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="230" />
<ColumnDefinition Width="18" />
<ColumnDefinition Width="*" />
<ColumnDefinition x:Name="ProfilesColumn" Width="230" />
<ColumnDefinition x:Name="ContentGapColumn" Width="18" />
<ColumnDefinition x:Name="EditorColumn" Width="*" />
</Grid.ColumnDefinitions>

<Border Grid.Row="0"
<Border x:Name="ProfilesPanel"
Grid.Row="0"
Grid.Column="0"
Style="{StaticResource PanelBorder}"
Padding="12">
Expand All @@ -394,7 +403,8 @@
</Grid>
</Border>

<Border Grid.Row="0"
<Border x:Name="EditorPanel"
Grid.Row="0"
Grid.Column="2"
Style="{StaticResource PanelBorder}">
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
Expand Down Expand Up @@ -475,7 +485,8 @@
</ScrollViewer>
</Border>

<Grid Grid.Row="1"
<Grid x:Name="FooterPanel"
Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="0,14,0,0">
Expand Down
65 changes: 65 additions & 0 deletions src/SwitchifyPc.App/SwitchControlProfileWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public partial class SwitchControlProfileWindow : Window
private bool isDirty;
private bool suppressDirtyTracking;
private bool suppressSelectionChange;
private bool isCompactLayout;
private readonly BindingRowViewModel[] rows =
Enumerable.Range(1, 8).Select(id => new BindingRowViewModel(id)).ToArray();

Expand All @@ -48,9 +49,73 @@ internal SwitchControlProfileWindow(
{
row.PropertyChanged += (_, _) => RefreshDirtyState();
}
Loaded += (_, _) => ApplyWorkArea(SystemParameters.WorkArea);
Reload();
}

internal void ApplyWorkArea(Rect workArea)
{
const double workAreaMargin = 16;
double availableWidth = Math.Max(320, workArea.Width - workAreaMargin * 2);
double availableHeight = Math.Max(240, workArea.Height - workAreaMargin * 2);

MinWidth = Math.Min(520, availableWidth);
MinHeight = Math.Min(300, availableHeight);
Width = Math.Min(900, availableWidth);
Height = Math.Min(690, availableHeight);
Left = workArea.Left + Math.Max(0, (workArea.Width - Width) / 2);
Top = workArea.Top + Math.Max(0, (workArea.Height - Height) / 2);
ApplyResponsiveLayout(Width);
}

private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
ApplyResponsiveLayout(e.NewSize.Width);
}

private void ApplyResponsiveLayout(double width)
{
bool useCompactLayout = width < 720;
if (useCompactLayout == isCompactLayout)
{
return;
}

isCompactLayout = useCompactLayout;
IntroPanel.Visibility = useCompactLayout ? Visibility.Collapsed : Visibility.Visible;
ContentBackground.Margin = useCompactLayout
? new Thickness(0)
: new Thickness(0, 80, 0, 0);
ProfileBody.Margin = useCompactLayout
? new Thickness(16, 12, 16, 16)
: new Thickness(24, 0, 24, 24);
ProfilesPanel.Margin = useCompactLayout
? new Thickness(0, 0, 0, 12)
: new Thickness(0);

ProfilesColumn.Width = useCompactLayout
? new GridLength(1, GridUnitType.Star)
: new GridLength(230);
ContentGapColumn.Width = useCompactLayout ? new GridLength(0) : new GridLength(18);
EditorColumn.Width = useCompactLayout ? new GridLength(0) : new GridLength(1, GridUnitType.Star);
PrimaryContentRow.Height = useCompactLayout
? new GridLength(92)
: new GridLength(1, GridUnitType.Star);
SecondaryContentRow.Height = useCompactLayout
? new GridLength(1, GridUnitType.Star)
: new GridLength(0);

Grid.SetRow(ProfilesPanel, 0);
Grid.SetColumn(ProfilesPanel, 0);
Grid.SetColumnSpan(ProfilesPanel, 1);
Grid.SetRow(EditorPanel, useCompactLayout ? 1 : 0);
Grid.SetColumn(EditorPanel, useCompactLayout ? 0 : 2);
Grid.SetColumnSpan(EditorPanel, 1);
Grid.SetRow(FooterPanel, 2);
Grid.SetColumn(FooterPanel, 0);
Grid.SetColumnSpan(FooterPanel, useCompactLayout ? 1 : 3);
}

private void Reload(string? selectId = null)
{
profiles = store.Load();
Expand Down
48 changes: 43 additions & 5 deletions src/SwitchifyPc.Tests/SwitchControlProfileWindowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,17 @@ public void ProfileWindowMatchesSettingsChromeAndLayout()
window.UpdateLayout();

Assert.Equal(WindowStyle.None, window.WindowStyle);
Assert.Equal(ResizeMode.NoResize, window.ResizeMode);
Assert.Equal(900, window.Width);
Assert.Equal(690, window.Height);
Assert.Equal(760, window.MinWidth);
Assert.Equal(560, window.MinHeight);
Assert.Equal(ResizeMode.CanResize, window.ResizeMode);
Assert.InRange(window.Width, window.MinWidth, 900);
Assert.InRange(window.Height, window.MinHeight, 690);
Assert.True(window.Width <= Math.Max(320, SystemParameters.WorkArea.Width - 32));
Assert.True(window.Height <= Math.Max(240, SystemParameters.WorkArea.Height - 32));
Assert.Equal(520, window.MinWidth);
Assert.Equal(300, window.MinHeight);
Assert.NotNull(WindowChrome.GetWindowChrome(window));
Assert.Contains("PC Switch Control profiles", TextBlocks(window));
Assert.NotNull(ButtonByAutomationName(window, "Minimize"));
Assert.NotNull(ButtonByAutomationName(window, "Maximize"));
Assert.NotNull(ButtonByAutomationName(window, "Close"));

WpfButton save = Assert.IsType<WpfButton>(window.FindName("SaveButton"));
Expand All @@ -52,6 +55,41 @@ public void ProfileWindowMatchesSettingsChromeAndLayout()
});
}

[Fact]
public void ProfileWindowClampsToWorkAreaAndUsesCompactLayout()
{
RunOnSta(() =>
{
WpfTestApplication.ApplyTheme(AppTheme.Light);
SwitchControlProfileWindow window = CreateWindow();
try
{
window.Show();
window.ApplyWorkArea(new Rect(100, 50, 680, 420));
window.UpdateLayout();

Assert.Equal(648, window.Width);
Assert.Equal(388, window.Height);
Assert.Equal(116, window.Left);
Assert.Equal(66, window.Top);
Assert.Equal(Visibility.Collapsed, Assert.IsType<StackPanel>(window.FindName("IntroPanel")).Visibility);

Border profiles = Assert.IsType<Border>(window.FindName("ProfilesPanel"));
Border editor = Assert.IsType<Border>(window.FindName("EditorPanel"));
Grid footer = Assert.IsType<Grid>(window.FindName("FooterPanel"));
Assert.Equal(0, Grid.GetRow(profiles));
Assert.Equal(1, Grid.GetRow(editor));
Assert.Equal(2, Grid.GetRow(footer));
Assert.Equal(0, Grid.GetColumn(editor));
Assert.Equal(1, Grid.GetColumnSpan(footer));
}
finally
{
window.Close();
}
});
}

[Fact]
public void ProfileWindowEnablesEditActionsOnlyWhileDirty()
{
Expand Down
40 changes: 40 additions & 0 deletions src/SwitchifyPc.Tests/SwitchifyTitleBarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,46 @@ public void CanHideMinimizeButton()
});
}

[Fact]
public void MaximizeButtonIsOptInAndTogglesParentWindowState()
{
RunOnSta(() =>
{
WpfTestApplication.ApplyTheme(AppTheme.Light);
SwitchifyTitleBar titleBar = new();
Window window = new()
{
Content = titleBar,
Width = 320,
Height = 120
};
try
{
window.Show();
window.UpdateLayout();

WpfButton maximize = Assert.IsType<WpfButton>(ButtonByAutomationName(window, "Maximize"));
Assert.Equal(Visibility.Collapsed, maximize.Visibility);

titleBar.ShowMaximizeButton = true;
window.UpdateLayout();
Assert.Equal(Visibility.Visible, maximize.Visibility);

maximize.RaiseEvent(new RoutedEventArgs(WpfButton.ClickEvent));
Assert.Equal(WindowState.Maximized, window.WindowState);
window.UpdateLayout();

WpfButton restore = Assert.IsType<WpfButton>(ButtonByAutomationName(window, "Restore"));
restore.RaiseEvent(new RoutedEventArgs(WpfButton.ClickEvent));
Assert.Equal(WindowState.Normal, window.WindowState);
}
finally
{
window.Close();
}
});
}

[Fact]
public void StatusBadgeIsHiddenByDefault()
{
Expand Down