diff --git a/README.md b/README.md index 6df03ae..428742e 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,12 @@ its build targets) and copies its brand assets (`Assets/Brand/app-icon.svg`, ## Usage +The default sample/starter Help menus include **View on GitHub**, +**Submit an issue…** and **Community support…**, matching gclo. Derive the issue +chooser (`/issues/new/choose`) and Discussions (`/discussions`) from the app's +`AppInfo.RepositoryUrl`; enable GitHub Discussions during repository setup. +Both WinUI and WPF samples show the wiring without requiring a new AppKit API. + New apps should start from the KofTwentyTwo Windows app template, which has everything below in place; the design is the [Windows desktop reference architecture](https://github.com/KofTwentyTwo/standards/blob/main/architecture/windows-desktop-app.md). diff --git a/samples/AppKit.Sample.WinUI/MainWindow.Help.cs b/samples/AppKit.Sample.WinUI/MainWindow.Help.cs new file mode 100644 index 0000000..701ae7c --- /dev/null +++ b/samples/AppKit.Sample.WinUI/MainWindow.Help.cs @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2026 James Maes + * SPDX-License-Identifier: MIT + */ + +using Microsoft.UI.Xaml; + + +namespace AppKit.Sample.WinUI; + +/// Opens only the app's fixed public Help destinations after deliberate native menu actions. +public sealed partial class MainWindow +{ + /// Shows this Windows app's repository in the user's default browser. + private async void GitHub_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(SampleApp.Info.RepositoryUrl!); + + + + /// Offers the app's issue templates without sending private document data. + private async void SubmitIssue_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(SampleApp.IssuesUrl); + + + + /// Opens community questions and tips in this app's repository. + private async void CommunitySupport_Click(object sender, RoutedEventArgs args) => await OpenHelpPageAsync(SampleApp.SupportUrl); + + + + /// A missing browser or OS launch failure is visible; no document path/content is included. + private async Task OpenHelpPageAsync(Uri target) + { + try + { + if(!await global::Windows.System.Launcher.LaunchUriAsync(target)) + { + StatusText.Text = "The link could not be opened. Check your default browser."; + } + } + catch(Exception exception) when(exception is System.Runtime.InteropServices.COMException or InvalidOperationException or ArgumentException) + { + StatusText.Text = "The link could not be opened. Check your default browser."; + App.Log.Warning(FormattableString.Invariant($"A Help link could not be opened ({exception.GetType().Name}, 0x{exception.HResult:X8}).")); + } + } +} diff --git a/samples/AppKit.Sample.WinUI/MainWindow.xaml b/samples/AppKit.Sample.WinUI/MainWindow.xaml index efe302b..8e48950 100644 --- a/samples/AppKit.Sample.WinUI/MainWindow.xaml +++ b/samples/AppKit.Sample.WinUI/MainWindow.xaml @@ -25,7 +25,11 @@ - + + + + + diff --git a/samples/AppKit.Sample.Wpf/MainWindow.Help.cs b/samples/AppKit.Sample.Wpf/MainWindow.Help.cs new file mode 100644 index 0000000..09b1b25 --- /dev/null +++ b/samples/AppKit.Sample.Wpf/MainWindow.Help.cs @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 James Maes (KofTwentyTwo) + * SPDX-License-Identifier: MIT + */ + +using System.Diagnostics; +using System.Windows; + + +namespace AppKit.Sample.Wpf; + +/// Opens the sample's fixed public Help destinations using the Windows default browser. +public partial class MainWindow +{ + /// Shows the sample's source repository. + private void GitHub_Click(object sender, RoutedEventArgs args) => OpenHelpPage(SampleApp.Info.RepositoryUrl!); + + + + /// Offers the repository's bug/feature templates. + private void SubmitIssue_Click(object sender, RoutedEventArgs args) => OpenHelpPage(SampleApp.IssuesUrl); + + + + /// Opens the sample's community support destination. + private void CommunitySupport_Click(object sender, RoutedEventArgs args) => OpenHelpPage(SampleApp.SupportUrl); + + + + /// Surfaces OS/browser launch failure without passing private app data to the shell. + private void OpenHelpPage(Uri target) + { + try + { + using var process = Process.Start(new ProcessStartInfo(target.AbsoluteUri) { UseShellExecute = true }); + } + catch(Exception exception) when(exception is System.ComponentModel.Win32Exception or InvalidOperationException) + { + StatusText.Text = "The link could not be opened. Check your default browser."; + Program.Log.Warning(FormattableString.Invariant($"A Help link could not be opened ({exception.GetType().Name}, 0x{exception.HResult:X8}).")); + } + } +} diff --git a/samples/AppKit.Sample.Wpf/MainWindow.xaml b/samples/AppKit.Sample.Wpf/MainWindow.xaml index adfd523..c67f3af 100644 --- a/samples/AppKit.Sample.Wpf/MainWindow.xaml +++ b/samples/AppKit.Sample.Wpf/MainWindow.xaml @@ -25,6 +25,10 @@ + + + + diff --git a/samples/Shared/SampleApp.cs b/samples/Shared/SampleApp.cs index bcf393c..71fb008 100644 --- a/samples/Shared/SampleApp.cs +++ b/samples/Shared/SampleApp.cs @@ -36,5 +36,17 @@ internal static class SampleApp public static AppPaths Paths { get; } = new(Info); + + + /// The app's bug/feature templates, independent of any upstream project credited in About. + public static Uri IssuesUrl { get; } = new(Info.RepositoryUrl!.AbsoluteUri.TrimEnd('/') + "/issues/new/choose"); + + + + /// Community questions and tips in the app's own repository. + public static Uri SupportUrl { get; } = new(Info.RepositoryUrl!.AbsoluteUri.TrimEnd('/') + "/discussions"); + + + public static SettingsStore SettingsStore { get; } = new(Paths.SettingsFile, SampleSettingsContext.Default.SampleSettings); } diff --git a/tests/KofTwentyTwo.AppKit.UiTests/AppSession.cs b/tests/KofTwentyTwo.AppKit.UiTests/AppSession.cs index 5b41f4b..b54d475 100644 --- a/tests/KofTwentyTwo.AppKit.UiTests/AppSession.cs +++ b/tests/KofTwentyTwo.AppKit.UiTests/AppSession.cs @@ -152,7 +152,12 @@ public sealed class AppSession : IDisposable /// expand (its flyout opens before the menu is ready), so the menu is re-opened /// until the item appears. /// - public void InvokeMenuItem(string menuName, string itemAutomationId) + public void InvokeMenuItem(string menuName, string itemAutomationId) => Invoke(OpenMenuItem(menuName, itemAutomationId)); + + + + /// Opens an owned native menu and returns its item without launching an external destination. + public AutomationElement OpenMenuItem(string menuName, string itemAutomationId) { AutomationElement menu = WaitFor( () => MainWindow.FindFirstDescendant(cf => cf.ByControlType(ControlType.MenuItem).And(cf.ByName(menuName))), @@ -171,7 +176,7 @@ public sealed class AppSession : IDisposable Wait.UntilInputIsProcessed(); item = Retry.WhileNull(() => FindInApp(cf => cf.ByAutomationId(itemAutomationId)), timeout: TimeSpan.FromSeconds(3), interval: s_pollInterval, ignoreException: true).Result; } - Invoke(item ?? throw new TimeoutException($"Menu item '{itemAutomationId}' never appeared under '{menuName}'. (app exited: {App.HasExited})")); + return item ?? throw new TimeoutException($"Menu item '{itemAutomationId}' never appeared under '{menuName}'. (app exited: {App.HasExited})"); } diff --git a/tests/KofTwentyTwo.AppKit.UiTests/SampleAppTests.cs b/tests/KofTwentyTwo.AppKit.UiTests/SampleAppTests.cs index c3d5d13..94cb990 100644 --- a/tests/KofTwentyTwo.AppKit.UiTests/SampleAppTests.cs +++ b/tests/KofTwentyTwo.AppKit.UiTests/SampleAppTests.cs @@ -28,6 +28,25 @@ public class SampleAppTests + /// The real Help menu exposes the app's repository, issue reporting and community commands without launching a browser. + [Theory] + [InlineData(SampleKind.WinUI)] + [InlineData(SampleKind.Wpf)] + public void Help_PublicDestinations_AreAvailableInTheNativeMenu(SampleKind kind) + { + using var session = new AppSession(kind); + _ = session.OpenMenuItem("Help", "MenuGitHub"); + foreach((string identity, string label) in new[] { ("MenuGitHub", "GitHub"), ("MenuSubmitIssue", "issue"), ("MenuCommunitySupport", "support") }) + { + AutomationElement item = session.WaitForElement(identity); + Assert.True(item.IsEnabled); + Assert.False(item.IsOffscreen); + Assert.Contains(label, item.Name, StringComparison.OrdinalIgnoreCase); + } + } + + + /// About: shows the build version. [Theory] [InlineData(SampleKind.WinUI)]