diff --git a/src/Eto.Mac/Forms/ApplicationHandler.cs b/src/Eto.Mac/Forms/ApplicationHandler.cs index f3792c38d..8067179a1 100644 --- a/src/Eto.Mac/Forms/ApplicationHandler.cs +++ b/src/Eto.Mac/Forms/ApplicationHandler.cs @@ -3,7 +3,7 @@ using System.Runtime.CompilerServices; namespace Eto.Mac.Forms { - public class ApplicationHandler : WidgetHandler, Application.IHandler + public class ApplicationHandler : MacObject, Application.IHandler { bool attached; @@ -115,7 +115,7 @@ static void restart_WillTerminate(object sender, EventArgs e) var args = new string[] { "-c", - "open \"$1\"", + "open \"$1\"", string.Empty, NSBundle.MainBundle.BundlePath }; @@ -129,7 +129,7 @@ public void Invoke(Action action) else Control.InvokeOnMainThread(action); } - + public void AsyncInvoke(Action action) { Control.BeginInvokeOnMainThread(action); @@ -150,23 +150,23 @@ public void Restart() Control.Delegate = oldDelegate; } - static readonly IntPtr selNextEventMatchingMaskUntilDateInModeDequeue_Handle = Selector.GetHandle ("nextEventMatchingMask:untilDate:inMode:dequeue:"); - static readonly IntPtr selSendEvent_Handle = Selector.GetHandle ("sendEvent:"); - + static readonly IntPtr selNextEventMatchingMaskUntilDateInModeDequeue_Handle = Selector.GetHandle("nextEventMatchingMask:untilDate:inMode:dequeue:"); + static readonly IntPtr selSendEvent_Handle = Selector.GetHandle("sendEvent:"); + public void RunIteration() { MacView.InMouseTrackingLoop = false; // drain the event queue only for a short period of time so it doesn't lock up var date = NSDate.FromTimeIntervalSinceNow(0.001); - for (;;) + for (; ; ) { // dequeue the event var evt = Control.NextEvent(NSEventMask.AnyEvent, date, NSRunLoopMode.Default, true); - + // no event? cool, let's get out of here if (evt == null) break; - + // dispatch the event Control.SendEvent(evt); } @@ -196,7 +196,7 @@ public void Run() EtoBundle.Init(); - + EtoFontManager.Install(); if (Control.Delegate == null) @@ -227,7 +227,7 @@ public void Open(string url) #if Mac64 delegate void UncaughtExceptionHandlerDelegate(IntPtr nsexceptionPtr); - + [DllImport(Constants.FoundationLibrary)] static extern void NSSetUncaughtExceptionHandler(UncaughtExceptionHandlerDelegate handler); @@ -275,6 +275,12 @@ public override void AttachEvent(string id) NSNotificationCenter.DefaultCenter.AddObserver(NSApplication.DidBecomeActiveNotification, SharedApplication_ActiveChanged); NSNotificationCenter.DefaultCenter.AddObserver(NSApplication.DidResignActiveNotification, SharedApplication_ActiveChanged); break; + case Application.CurrentThemeChangedEvent: + AddControlObserver(new NSString("effectiveAppearance"), e => + { + Callback.OnThemeChanged(Widget, EventArgs.Empty); + }); + break; default: base.AttachEvent(id); break; @@ -325,5 +331,18 @@ public void EnableFullScreen() public Keys AlternateModifier => Keys.Alt; public bool IsActive => NSApplication.SharedApplication.Active; + + public Theme CurrentTheme + { + get + { + var appearance = NSApplication.SharedApplication.Appearance; + return new Theme(new ThemeHandler(appearance)); + } + set + { + NSApplication.SharedApplication.Appearance = ThemeHandler.GetControl(value); + } + } } } diff --git a/src/Eto.Mac/Forms/ThemeHandler.cs b/src/Eto.Mac/Forms/ThemeHandler.cs new file mode 100644 index 000000000..6667e1ea3 --- /dev/null +++ b/src/Eto.Mac/Forms/ThemeHandler.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Eto.Mac.Forms; + +public class ThemeHandler : WidgetHandler, Theme.IHandler +{ + public ThemeHandler(NSAppearance appearance) + { + Control = appearance; + } + + public string Name + { + get + { + var name = Control?.Name.ToString() ?? Application.Instance.Localize(Widget, "System"); + if (name.StartsWith("NSAppearanceName")) + return name.Substring("NSAppearanceName".Length); + return name; + } + } + + public ThemeStyle Style => Control.Name.ToString().ToLowerInvariant().Contains("dark") ? ThemeStyle.Dark : ThemeStyle.Light; + + public override bool Equals(object obj) + { + if (obj is ThemeHandler themeHandler) + { + if (Control == null && themeHandler.Control == null) + return true; + return Control.Equals(themeHandler.Control); + } + return base.Equals(obj); + } + + public override int GetHashCode() + { + return Control?.GetHashCode() ?? 0; + } +} + +public class ThemesHandler : Themes.IHandler +{ + public Theme System => new Theme(new ThemeHandler(null)); + public Theme Light => new Theme(new ThemeHandler(NSAppearance.GetAppearance(NSAppearance.NameAqua))); + public Theme Dark => new Theme(new ThemeHandler(NSAppearance.GetAppearance(NSAppearance.NameDarkAqua))); + public Theme None => System; + + public IEnumerable GetThemes() + { + yield return new Theme(new ThemeHandler(null)); + yield return new Theme(new ThemeHandler(NSAppearance.GetAppearance(NSAppearance.NameAqua))); + yield return new Theme(new ThemeHandler(NSAppearance.GetAppearance(NSAppearance.NameDarkAqua))); + yield return new Theme(new ThemeHandler(NSAppearance.GetAppearance(NSAppearance.NameVibrantLight))); + yield return new Theme(new ThemeHandler(NSAppearance.GetAppearance(NSAppearance.NameVibrantDark))); + } +} diff --git a/src/Eto.Mac/Platform.cs b/src/Eto.Mac/Platform.cs index 94364ddbc..6949fa2a0 100644 --- a/src/Eto.Mac/Platform.cs +++ b/src/Eto.Mac/Platform.cs @@ -258,6 +258,7 @@ public static void AddTo(Eto.Platform p) p.Add(() => new DataFormatsHandler()); p.Add(() => new TaskbarHandler()); p.Add(() => new WindowHandler()); + p.Add(() => new ThemesHandler()); // IO p.Add(() => new SystemIconsHandler()); diff --git a/src/Eto.Wpf/Eto.Wpf.csproj b/src/Eto.Wpf/Eto.Wpf.csproj index 011c4634e..5f04bf638 100755 --- a/src/Eto.Wpf/Eto.Wpf.csproj +++ b/src/Eto.Wpf/Eto.Wpf.csproj @@ -12,7 +12,7 @@ true true - NU1701;MSB4011;$(NoWarn) + NU1701;MSB4011;WPF0001;$(NoWarn) diff --git a/src/Eto.Wpf/Forms/ApplicationHandler.cs b/src/Eto.Wpf/Forms/ApplicationHandler.cs index d8dce0d12..45aafb1fc 100755 --- a/src/Eto.Wpf/Forms/ApplicationHandler.cs +++ b/src/Eto.Wpf/Forms/ApplicationHandler.cs @@ -235,6 +235,25 @@ public Keys AlternateModifier get { return Keys.Alt; } } + public Theme CurrentTheme + { + get + { +#if NET9_0_OR_GREATER + return new Theme(new ThemeHandler(Control.ThemeMode)); +#else + return null; +#endif + } + set + { +#if NET9_0_OR_GREATER + Control.ThemeMode = ThemeHandler.GetControl(value); +#endif + } + } + + public void Open(string url) { Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); @@ -242,6 +261,9 @@ public void Open(string url) public void Run() { +#if NET9_0_OR_GREATER +// Control.ThemeMode = sw.ThemeMode.System; +#endif Callback.OnInitialized(Widget, EventArgs.Empty); if (!_attached) { diff --git a/src/Eto.Wpf/Forms/ThemeHandler.cs b/src/Eto.Wpf/Forms/ThemeHandler.cs new file mode 100755 index 000000000..730a4b3be --- /dev/null +++ b/src/Eto.Wpf/Forms/ThemeHandler.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Eto.Wpf.Forms; + +#if NET9_0_OR_GREATER + +public class ThemeHandler : WidgetHandler, Theme.IHandler +{ + public ThemeHandler(sw.ThemeMode mode) + { + Control = mode; + } + + public string Name => Control.ToString(); + + public ThemeStyle Style + { + get + { + if (Control == sw.ThemeMode.System) + return ThemeStyle.System; + if (Control == sw.ThemeMode.Light) + return ThemeStyle.Light; + if (Control == sw.ThemeMode.Dark) + return ThemeStyle.Dark; + return ThemeStyle.Light; + } + } +} + +public class ThemesHandler : Themes.IHandler +{ + public Theme Light => new Theme(new ThemeHandler(sw.ThemeMode.Light)); + public Theme Dark => new Theme(new ThemeHandler(sw.ThemeMode.Dark)); + public Theme System => new Theme(new ThemeHandler(sw.ThemeMode.System)); + public Theme None => new Theme(new ThemeHandler(sw.ThemeMode.None)); + + public IEnumerable GetThemes() + { + yield return new Theme(new ThemeHandler(sw.ThemeMode.System)); + yield return new Theme(new ThemeHandler(sw.ThemeMode.Light)); + yield return new Theme(new ThemeHandler(sw.ThemeMode.Dark)); + yield return new Theme(new ThemeHandler(sw.ThemeMode.None)); + } +} + +#endif \ No newline at end of file diff --git a/src/Eto.Wpf/Platform.cs b/src/Eto.Wpf/Platform.cs index dcdb2c47c..8807dbb0e 100755 --- a/src/Eto.Wpf/Platform.cs +++ b/src/Eto.Wpf/Platform.cs @@ -209,6 +209,9 @@ public static void AddTo(Eto.Platform p) p.Add(() => new DataFormatsHandler()); p.Add(() => new TaskbarHandler()); p.Add(() => new WindowHandler()); +#if NET9_0_OR_GREATER + p.Add(() => new ThemesHandler()); +#endif // IO p.Add(() => new SystemIconsHandler()); diff --git a/src/Eto.Wpf/themes/controls/DataGrid.xaml b/src/Eto.Wpf/themes/controls/DataGrid.xaml index 3bca28434..6bfa69046 100755 --- a/src/Eto.Wpf/themes/controls/DataGrid.xaml +++ b/src/Eto.Wpf/themes/controls/DataGrid.xaml @@ -15,7 +15,7 @@ \ No newline at end of file diff --git a/src/Eto.Wpf/themes/controls/Window.xaml b/src/Eto.Wpf/themes/controls/Window.xaml index 81f785690..8c1a2bf50 100755 --- a/src/Eto.Wpf/themes/controls/Window.xaml +++ b/src/Eto.Wpf/themes/controls/Window.xaml @@ -9,7 +9,7 @@ xmlns:local="clr-namespace:Xceed.Wpf.Toolkit;assembly=Xceed.Wpf.Toolkit"> \ No newline at end of file diff --git a/src/Eto/Forms/Application.cs b/src/Eto/Forms/Application.cs index 272591321..4e9ebe704 100644 --- a/src/Eto/Forms/Application.cs +++ b/src/Eto/Forms/Application.cs @@ -272,6 +272,22 @@ void HandleClosed(object sender, EventArgs e) /// /// The name. public string Name { get; set; } + + public Theme CurrentTheme + { + get => Handler.CurrentTheme; + set => Handler.CurrentTheme = value; + } + + public const string CurrentThemeChangedEvent = "Application.CurrentThemeChanged"; + + public event EventHandler CurrentThemeChanged + { + add { Properties.AddHandlerEvent(CurrentThemeChangedEvent, value); } + remove { Properties.RemoveEvent(CurrentThemeChangedEvent, value); } + } + + protected virtual void OnCurrentThemeChanged(EventArgs e) => Properties.TriggerEvent(CurrentThemeChangedEvent, this, e); static Application() { @@ -279,6 +295,7 @@ static Application() EventLookup.Register(c => c.OnUnhandledException(null), Application.UnhandledExceptionEvent); EventLookup.Register(c => c.OnNotificationActivated(null), Application.NotificationActivatedEvent); EventLookup.Register(c => c.OnIsActiveChanged(null), Application.IsActiveChangedEvent); + EventLookup.Register(c => c.OnCurrentThemeChanged(null), Application.CurrentThemeChangedEvent); } /// @@ -609,6 +626,7 @@ public string Localize(object source, string text) /// Raises the IsActiveChanged event. /// void OnIsActiveChanged(Application wiget, EventArgs e); + void OnThemeChanged(Application wiget, EventArgs e); } /// @@ -659,6 +677,12 @@ public void OnIsActiveChanged(Application widget, EventArgs e) using (widget.Platform.Context) widget.OnIsActiveChanged(e); } + + public void OnThemeChanged(Application widget, EventArgs e) + { + using (widget.Platform.Context) + widget.OnCurrentThemeChanged(e); + } } /// @@ -767,5 +791,6 @@ public void OnIsActiveChanged(Application widget, EventArgs e) /// Gets a value indicating that the application is currently the active application /// bool IsActive { get; } + Theme CurrentTheme { get; set; } } } \ No newline at end of file diff --git a/src/Eto/Forms/Theme.cs b/src/Eto/Forms/Theme.cs new file mode 100644 index 000000000..372d79edf --- /dev/null +++ b/src/Eto/Forms/Theme.cs @@ -0,0 +1,60 @@ +namespace Eto.Forms; + +[Handler(typeof(IHandler))] +public class Theme : Widget +{ + new IHandler Handler => (IHandler)base.Handler; + public string Name => Handler.Name; + + public ThemeStyle Style => Handler.Style; + + public Theme(IHandler handler) : base(handler) + { + } + + public interface IHandler : Widget.IHandler + { + string Name { get; } + ThemeStyle Style { get; } + } + + public override string ToString() => Name; +} + +public enum ThemeStyle +{ + /// + /// Specifies the theme follows the system light or dark style + /// + System, + /// + /// Specifies the theme is considered a light style + /// + Light, + /// + /// Specifies the theme is considered a dark style + /// + Dark +} + +[Handler(typeof(IHandler))] +public static class Themes +{ + static IHandler Handler => Platform.Instance.CreateShared(); + + public static IEnumerable AllThemes => Handler.GetThemes(); + + public static Theme Light => Handler.Light; + public static Theme Dark => Handler.Dark; + public static Theme None => Handler.None; + + public interface IHandler + { + Theme Light { get; } + Theme Dark { get; } + Theme None { get; } + + IEnumerable GetThemes(); + } +} + diff --git a/src/Eto/Forms/ThemedControls/ThemedFontPickerHandler.cs b/src/Eto/Forms/ThemedControls/ThemedFontPickerHandler.cs index 5a240b5df..53c1cb14a 100644 --- a/src/Eto/Forms/ThemedControls/ThemedFontPickerHandler.cs +++ b/src/Eto/Forms/ThemedControls/ThemedFontPickerHandler.cs @@ -30,6 +30,8 @@ public Font Value font = value; Callback.OnValueChanged(Widget, EventArgs.Empty); Refresh(); + if (dialog != null) + dialog.Font = value; } } @@ -64,7 +66,11 @@ private void Control_Click(object sender, EventArgs e) dialog.Font = font; dialog.FontChanged += Dialog_FontChanged; - dialog.ShowDialog(Widget); + var result = dialog.ShowDialog(Widget); + if (result != DialogResult.None) + { + dialog = null; + } } void Dialog_FontChanged(object sender, EventArgs e) diff --git a/test/Eto.Test.Wpf/Eto.Test.Wpf.csproj b/test/Eto.Test.Wpf/Eto.Test.Wpf.csproj index 5b1a6bd97..435b189ed 100644 --- a/test/Eto.Test.Wpf/Eto.Test.Wpf.csproj +++ b/test/Eto.Test.Wpf/Eto.Test.Wpf.csproj @@ -1,7 +1,7 @@  - net7.0-windows;net48 + net9.0-windows;net7.0-windows;net48 WinExe Properties TestIcon.ico diff --git a/test/Eto.Test/Sections/Behaviors/ThemeSection.cs b/test/Eto.Test/Sections/Behaviors/ThemeSection.cs new file mode 100644 index 000000000..ac8e22362 --- /dev/null +++ b/test/Eto.Test/Sections/Behaviors/ThemeSection.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Eto.Test.Sections.Behaviors +{ + [Section("Behaviors", typeof(Themes))] + public class ThemeSection : Panel + { + public ThemeSection() + { + var themes = Themes.AllThemes.ToList(); + // themes.Insert(0, null); + var themeDropDown = new DropDown + { + DataStore = themes, + ItemTextBinding = Binding.Delegate((Theme t) => t.Name, defaultGetValue: "System") + }; + themeDropDown.SelectedValueBinding.Bind(Application.Instance, t => t.CurrentTheme); + + + var currentThemeLabel = new Label(); + currentThemeLabel.TextBinding.Bind(Application.Instance, t => t.CurrentTheme.Name); + + var layout = new DynamicLayout(); + layout.AddAutoSized(themeDropDown); + layout.AddAutoSized(currentThemeLabel); + + Content = layout; + } + + protected override void OnUnLoad(EventArgs e) + { + base.OnUnLoad(e); + Unbind(); + } + + } +} \ No newline at end of file