@@ -25,5 +25,6 @@ public static class MenuKeys
|
||||
public const string MenuKeyTagInput = "TagInput";
|
||||
public const string MenuKeyTimeline = "Timeline";
|
||||
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
|
||||
public const string MenuKeyThemeToggler = "ThemeToggler";
|
||||
|
||||
}
|
||||
26
demo/Ursa.Demo/Pages/ThemeTogglerDemo.axaml
Normal file
26
demo/Ursa.Demo/Pages/ThemeTogglerDemo.axaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:u="https://irihi.tech/ursa"
|
||||
mc:Ignorable="d" d:DesignWidth="800"
|
||||
d:DesignHeight="450"
|
||||
x:Class="Ursa.Demo.Pages.ThemeTogglerDemo">
|
||||
<Grid ColumnDefinitions="Auto, *">
|
||||
<StackPanel Grid.Column="0">
|
||||
<TextBlock Text="Global"></TextBlock>
|
||||
<u:ThemeToggleButton/>
|
||||
<TextBlock Text="Target To Scope"></TextBlock>
|
||||
<u:ThemeToggleButton TargetScope="{Binding #scope}"></u:ThemeToggleButton>
|
||||
</StackPanel>
|
||||
<ThemeVariantScope Grid.Column="1" Name="scope" RequestedThemeVariant="Dark">
|
||||
<Border Theme="{DynamicResource CardBorder}">
|
||||
<StackPanel>
|
||||
<Button Content="Hello World"></Button>
|
||||
<Calendar></Calendar>
|
||||
<u:ThemeToggleButton></u:ThemeToggleButton>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ThemeVariantScope>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
13
demo/Ursa.Demo/Pages/ThemeTogglerDemo.axaml.cs
Normal file
13
demo/Ursa.Demo/Pages/ThemeTogglerDemo.axaml.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Ursa.Demo.Pages;
|
||||
|
||||
public partial class ThemeTogglerDemo : UserControl
|
||||
{
|
||||
public ThemeTogglerDemo()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ public class MainViewViewModel : ViewModelBase
|
||||
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
|
||||
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
|
||||
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
|
||||
MenuKeys.MenuKeyThemeToggler => new ThemeTogglerDemoViewModel(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,7 @@ public class MenuViewModel: ViewModelBase
|
||||
new() { MenuHeader = "Pagination", Key = MenuKeys.MenuKeyPagination },
|
||||
new() { MenuHeader = "RangeSlider", Key = MenuKeys.MenuKeyRangeSlider, Status = "New"},
|
||||
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
|
||||
new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler },
|
||||
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "Updated" },
|
||||
new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon, Status = "New"},
|
||||
};
|
||||
|
||||
6
demo/Ursa.Demo/ViewModels/ThemeTogglerDemoViewModel.cs
Normal file
6
demo/Ursa.Demo/ViewModels/ThemeTogglerDemoViewModel.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Ursa.Demo.ViewModels;
|
||||
|
||||
public class ThemeTogglerDemoViewModel
|
||||
{
|
||||
|
||||
}
|
||||
@@ -77,13 +77,10 @@
|
||||
</u:NavigationMenu.ItemTemplate>
|
||||
</u:NavigationMenu>
|
||||
</Border>
|
||||
|
||||
<ToggleButton
|
||||
<u:ThemeToggleButton
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Content="Update Theme"
|
||||
IsCheckedChanged="ToggleButton_OnIsCheckedChanged" />
|
||||
HorizontalAlignment="Right"/>
|
||||
<ContentControl
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
|
||||
30
src/Ursa.Themes.Semi/Controls/ThemeSelector.axaml
Normal file
30
src/Ursa.Themes.Semi/Controls/ThemeSelector.axaml
Normal file
@@ -0,0 +1,30 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="https://irihi.tech/ursa">
|
||||
<!-- Add Resources Here -->
|
||||
<ControlTheme TargetType="u:ThemeToggleButton" x:Key="{x:Type u:ThemeToggleButton}">
|
||||
<Setter Property="Template">
|
||||
<ControlTemplate TargetType="u:ThemeToggleButton">
|
||||
<ToggleSwitch
|
||||
Padding="4"
|
||||
Name="{x:Static u:ThemeToggleButton.PART_ThemeToggleButton}"
|
||||
Theme="{DynamicResource ButtonToggleSwitch}">
|
||||
<ToggleSwitch.OnContent>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{DynamicResource ThemeSelectorButtonLightGlyph}"
|
||||
Foreground="{DynamicResource ButtonDefaultTertiaryForeground}" />
|
||||
</ToggleSwitch.OnContent>
|
||||
<ToggleSwitch.OffContent>
|
||||
<PathIcon
|
||||
Width="16"
|
||||
Height="16"
|
||||
Data="{DynamicResource ThemeSelectorButtonDarkGlyph}"
|
||||
Foreground="{DynamicResource ButtonDefaultTertiaryForeground}" />
|
||||
</ToggleSwitch.OffContent>
|
||||
</ToggleSwitch>
|
||||
</ControlTemplate>
|
||||
</Setter>
|
||||
</ControlTheme>
|
||||
</ResourceDictionary>
|
||||
@@ -22,6 +22,7 @@
|
||||
<ResourceInclude Source="Pagination.axaml" />
|
||||
<ResourceInclude Source="RangeSlider.axaml" />
|
||||
<ResourceInclude Source="TagInput.axaml" />
|
||||
<ResourceInclude Source="ThemeSelector.axaml" />
|
||||
<ResourceInclude Source="Timeline.axaml" />
|
||||
<ResourceInclude Source="TwoTonePathIcon.axaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
6
src/Ursa.Themes.Semi/Themes/Shared/ThemeSelector.axaml
Normal file
6
src/Ursa.Themes.Semi/Themes/Shared/ThemeSelector.axaml
Normal file
@@ -0,0 +1,6 @@
|
||||
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- Add Resources Here -->
|
||||
<StreamGeometry x:Key="ThemeSelectorButtonDarkGlyph">M12 23C18.0751 23 23 18.0751 23 12C23 5.92487 18.0751 1 12 1C5.92487 1 1 5.92487 1 12C1 18.0751 5.92487 23 12 23ZM17 15C17.476 15 17.9408 14.9525 18.3901 14.862C17.296 17.3011 14.8464 19 12 19C8.13401 19 5 15.866 5 12C5 8.60996 7.40983 5.78277 10.6099 5.13803C10.218 6.01173 10 6.98041 10 8C10 11.866 13.134 15 17 15Z</StreamGeometry>
|
||||
<StreamGeometry x:Key="ThemeSelectorButtonLightGlyph">M3.55 19.09L4.96 20.5L6.76 18.71L5.34 17.29M12 6C8.69 6 6 8.69 6 12S8.69 18 12 18 18 15.31 18 12C18 8.68 15.31 6 12 6M20 13H23V11H20M17.24 18.71L19.04 20.5L20.45 19.09L18.66 17.29M20.45 5L19.04 3.6L17.24 5.39L18.66 6.81M13 1H11V4H13M6.76 5.39L4.96 3.6L3.55 5L5.34 6.81L6.76 5.39M1 13H4V11H1M13 20H11V23H13</StreamGeometry>
|
||||
</ResourceDictionary>
|
||||
@@ -14,5 +14,6 @@
|
||||
<MergeResourceInclude Source="NavigationMenu.axaml" />
|
||||
<MergeResourceInclude Source="Pagination.axaml" />
|
||||
<MergeResourceInclude Source="TagInput.axaml" />
|
||||
<MergeResourceInclude Source="ThemeSelector.axaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -20,4 +20,22 @@ internal static class EventHelper
|
||||
if(button is not null) button.Click -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
public static void RegisterEvent<TArgs>(RoutedEvent<TArgs> routedEvent, EventHandler<TArgs> handler, params Button?[] controls)
|
||||
where TArgs : RoutedEventArgs
|
||||
{
|
||||
foreach (var control in controls)
|
||||
{
|
||||
control?.AddHandler(routedEvent, handler);
|
||||
}
|
||||
}
|
||||
|
||||
public static void UnregisterEvent<TArgs>(RoutedEvent<TArgs> routedEvent, EventHandler<TArgs> handler, params Button?[] controls)
|
||||
where TArgs : RoutedEventArgs
|
||||
{
|
||||
foreach (var control in controls)
|
||||
{
|
||||
control?.RemoveHandler(routedEvent, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/Ursa/Common/PropertyHelper.cs
Normal file
14
src/Ursa/Common/PropertyHelper.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Avalonia;
|
||||
|
||||
namespace Ursa.Common;
|
||||
|
||||
public static class PropertyHelper
|
||||
{
|
||||
public static void SetValue<TValue>(AvaloniaProperty<TValue> property, TValue value, params AvaloniaObject?[] elements)
|
||||
{
|
||||
foreach (var element in elements)
|
||||
{
|
||||
element?.SetValue(property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/Ursa/Controls/ThemeSelector/ThemeSelectorBase.cs
Normal file
133
src/Ursa/Controls/ThemeSelector/ThemeSelectorBase.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Styling;
|
||||
using Ursa.Common;
|
||||
using System;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public abstract class ThemeSelectorBase: TemplatedControl
|
||||
{
|
||||
private bool _syncFromScope;
|
||||
private Application? _application;
|
||||
private ThemeVariantScope? _scope;
|
||||
|
||||
public static readonly StyledProperty<ThemeVariant?> SelectedThemeProperty = AvaloniaProperty.Register<ThemeSelectorBase, ThemeVariant?>(
|
||||
nameof(SelectedTheme));
|
||||
|
||||
public ThemeVariant? SelectedTheme
|
||||
{
|
||||
get => GetValue(SelectedThemeProperty);
|
||||
set => SetValue(SelectedThemeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<ThemeVariantScope?> TargetScopeProperty =
|
||||
AvaloniaProperty.Register<ThemeSelectorBase, ThemeVariantScope?>(
|
||||
nameof(TargetScope));
|
||||
|
||||
public ThemeVariantScope? TargetScope
|
||||
{
|
||||
get => GetValue(TargetScopeProperty);
|
||||
set => SetValue(TargetScopeProperty, value);
|
||||
}
|
||||
|
||||
static ThemeSelectorBase()
|
||||
{
|
||||
SelectedThemeProperty.Changed.AddClassHandler<ThemeSelectorBase, ThemeVariant?>((s, e) => s.OnSelectedThemeChanged(e));
|
||||
TargetScopeProperty.Changed.AddClassHandler<ThemeSelectorBase, ThemeVariantScope?>((s, e) => s.OnTargetScopeChanged(e));
|
||||
}
|
||||
|
||||
private void OnTargetScopeChanged(AvaloniaPropertyChangedEventArgs<ThemeVariantScope?> args)
|
||||
{
|
||||
if (args.OldValue.Value is { } oldTarget)
|
||||
{
|
||||
oldTarget.ActualThemeVariantChanged -= OnScopeThemeChanged;
|
||||
}
|
||||
if (args.NewValue.Value is { } newTarget)
|
||||
{
|
||||
newTarget.ActualThemeVariantChanged += OnScopeThemeChanged;
|
||||
SyncThemeFromScope(newTarget.ActualThemeVariant);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScopeThemeChanged(object sender, System.EventArgs e)
|
||||
{
|
||||
_syncFromScope = true;
|
||||
if (this.TargetScope is { } target)
|
||||
{
|
||||
SyncThemeFromScope(target.ActualThemeVariant);
|
||||
}
|
||||
else if (this._scope is { } scope)
|
||||
{
|
||||
SyncThemeFromScope(scope.ActualThemeVariant);
|
||||
}
|
||||
else if (_application is { } app)
|
||||
{
|
||||
SyncThemeFromScope(app.ActualThemeVariant);
|
||||
}
|
||||
_syncFromScope = false;
|
||||
}
|
||||
|
||||
protected virtual void SyncThemeFromScope(ThemeVariant? theme)
|
||||
{
|
||||
this.SelectedTheme = theme;
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_application = Application.Current;
|
||||
if (_application is not null)
|
||||
{
|
||||
_application.ActualThemeVariantChanged += OnScopeThemeChanged;
|
||||
SyncThemeFromScope(_application.ActualThemeVariant);
|
||||
}
|
||||
_scope = this.GetLogicalAncestors().FirstOrDefault(a => a is ThemeVariantScope) as ThemeVariantScope;
|
||||
if (_scope is not null)
|
||||
{
|
||||
_scope.ActualThemeVariantChanged += OnScopeThemeChanged;
|
||||
SyncThemeFromScope(_scope.ActualThemeVariant);
|
||||
}
|
||||
if (TargetScope is not null)
|
||||
{
|
||||
SyncThemeFromScope(TargetScope.ActualThemeVariant);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
if (_application is not null)
|
||||
{
|
||||
_application.ActualThemeVariantChanged -= OnScopeThemeChanged;
|
||||
}
|
||||
if (_scope is not null)
|
||||
{
|
||||
_scope.ActualThemeVariantChanged -= OnScopeThemeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnSelectedThemeChanged(AvaloniaPropertyChangedEventArgs<ThemeVariant?> args)
|
||||
{
|
||||
if (_syncFromScope) return;
|
||||
ThemeVariant? newTheme = args.NewValue.Value;
|
||||
if (newTheme is null) return;
|
||||
if (TargetScope is not null)
|
||||
{
|
||||
TargetScope.RequestedThemeVariant = newTheme;
|
||||
return;
|
||||
}
|
||||
if (_scope is not null)
|
||||
{
|
||||
_scope.RequestedThemeVariant = newTheme;
|
||||
return;
|
||||
}
|
||||
if (_application is not null)
|
||||
{
|
||||
_application.RequestedThemeVariant = newTheme;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
49
src/Ursa/Controls/ThemeSelector/ThemeToggleButton.cs
Normal file
49
src/Ursa/Controls/ThemeSelector/ThemeToggleButton.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Styling;
|
||||
using Ursa.Common;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
[TemplatePart(PART_ThemeToggleButton, typeof(ToggleButton))]
|
||||
public class ThemeToggleButton: ThemeSelectorBase
|
||||
{
|
||||
public const string PART_ThemeToggleButton = "PART_ThemeToggleButton";
|
||||
|
||||
/// <summary>
|
||||
/// This button IsChecked=true means ThemeVariant.Light, IsChecked=false means ThemeVariant.Dark.
|
||||
/// </summary>
|
||||
private ToggleButton? _button;
|
||||
private ThemeVariant? _currentTheme;
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_currentTheme = this.ActualThemeVariant;
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||
{
|
||||
base.OnApplyTemplate(e);
|
||||
EventHelper.UnregisterEvent(Button.ClickEvent, OnButtonClickedChanged, _button);
|
||||
_button = e.NameScope.Get<ToggleButton>(PART_ThemeToggleButton);
|
||||
EventHelper.RegisterEvent(Button.ClickEvent, OnButtonClickedChanged, _button);
|
||||
PropertyHelper.SetValue(ToggleButton.IsCheckedProperty, _currentTheme == ThemeVariant.Light, _button);
|
||||
}
|
||||
|
||||
private void OnButtonClickedChanged(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var newTheme = (sender as ToggleButton)!.IsChecked;
|
||||
if (newTheme is null) return;
|
||||
SetCurrentValue(SelectedThemeProperty, newTheme.Value ? ThemeVariant.Light : ThemeVariant.Dark);
|
||||
}
|
||||
|
||||
protected override void SyncThemeFromScope(ThemeVariant? theme)
|
||||
{
|
||||
base.SyncThemeFromScope(theme);
|
||||
PropertyHelper.SetValue(ToggleButton.IsCheckedProperty, theme == ThemeVariant.Light, _button);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user