diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 319cadd..a41fcec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -44,7 +44,7 @@ jobs:
shell: pwsh
run: >-
dotnet test tests/MarkdownViewr.Windows.UiTests --no-build
- --filter 'FullyQualifiedName~SourceHighlighting|FullyQualifiedName~Settings_NewWindowPanelDefaults|FullyQualifiedName~ThemeTransfer_NativePickerCancels|FullyQualifiedName~Print_UserWidthAndBorders|FullyQualifiedName~ReadingKeys_ScrollCommands|FullyQualifiedName~Source_ShowReloadAndHide|FullyQualifiedName~ThemeEditor_AllTypography|FullyQualifiedName~ThemeEditor_NewCustomTheme|FullyQualifiedName~Appearance_WidthCssAndPrint|FullyQualifiedName~Toolbar_ReadingCommandsAndMenus|FullyQualifiedName~ThemeTransfer_BatchFutureInvalidAndDisabledExport|FullyQualifiedName~ReadingKeys_HeadingSequences|FullyQualifiedName~Source_BidirectionalScroll|FullyQualifiedName~Source_RawHtmlHeadingProvenance|FullyQualifiedName~ActivationInboxTests|FullyQualifiedName~Outline_NestedCollapse'
+ --filter 'FullyQualifiedName~SourceHighlighting|FullyQualifiedName~Settings_NewWindowPanelDefaults|FullyQualifiedName~Settings_ContentsPresentation|FullyQualifiedName~ThemeTransfer_NativePickerCancels|FullyQualifiedName~Print_UserWidthAndBorders|FullyQualifiedName~ReadingKeys_ScrollCommands|FullyQualifiedName~Source_ShowReloadAndHide|FullyQualifiedName~ThemeEditor_AllTypography|FullyQualifiedName~ThemeEditor_NewCustomTheme|FullyQualifiedName~Appearance_WidthCssAndPrint|FullyQualifiedName~Toolbar_ReadingCommandsAndMenus|FullyQualifiedName~ThemeTransfer_BatchFutureInvalidAndDisabledExport|FullyQualifiedName~ReadingKeys_HeadingSequences|FullyQualifiedName~Source_BidirectionalScroll|FullyQualifiedName~Source_RawHtmlHeadingProvenance|FullyQualifiedName~ActivationInboxTests|FullyQualifiedName~Outline_NestedCollapse'
--logger 'trx;LogFileName=reader-regressions.trx'
--results-directory artifacts/reader-regressions
- name: Retain native regression evidence
diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md
index d0b775f..1e808dc 100644
--- a/docs/PRODUCT.md
+++ b/docs/PRODUCT.md
@@ -26,7 +26,7 @@ Reference source inspected at commit `6d08bc9d27d7d7b2c2fa95bb282da330b8cf9021`.
| Rendering | GitHub Flavored Markdown, tables, tasks, strikethrough, fenced code, local images/links; independently configurable highlight, superscript, subscript and underline extensions |
| Reload | Watch saves, including atomic replacements; debounce changes, preserve scroll, report deletion/recovery |
| Themes | 20 built-ins; editable body/heading/code fonts, sizes and palette; theme/global CSS; content width; theme order, enable/disable/delete/restore and cycling; JSON import/export with overwrite/keep-both/skip conflicts; persistent settings |
-| Navigation | Resizable table of contents, depth control, heading jumps, collapsible sections and per-window font zoom (30–500%, Ctrl+plus/minus/0) |
+| Navigation | Resizable table of contents, depth control, configurable wrapping or ellipsis/full-title tooltip, optional heading-depth markers, heading jumps, collapsible sections and per-window font zoom (30–500%, Ctrl+plus/minus/0) |
| Source | Optional source pane, synchronized headings/scroll and collapse state; theme-aware syntax highlighting; draggable split opening at50/50 (#16/#17) |
| Find | Match counts, next/previous matches, search in both panes and vim-style shortcuts |
| Keys | j/k, Ctrl+d/u, gg/G, ]]/[[, /, n/N; Windows Ctrl-based equivalents for native commands |
diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md
index ebd80bb..d59f672 100644
--- a/docs/SESSION-STATE.md
+++ b/docs/SESSION-STATE.md
@@ -2,6 +2,28 @@
Updated: 2026-10-11
+## Native General and Contents options prepared - 2026-10-11
+
+Move new-window Contents/Source defaults and Default Apps guidance into General;
+splash/startup update controls remain Advanced. Add persisted Contents wrapping
+and optional depth markers using native TextBlock/ListView/ToggleSwitch controls.
+Unwrapped titles use one line/ellipsis and full-title tooltips; wrapped titles have
+no two-line cap. Preserve heading identities, selected row, per-window depth and
+visibility. Checked persistence precedes sibling broadcasts and baseline updates.
+Category switches reset ordinary scroll position without discarding CSS drafts.
+
+All593 core tests pass with100% measured line coverage; zero-warning Release build
+passes. A new actual native case checks long-title geometry, live sibling markers,
+unrelated-save persistence, Settings reopening and restoration; focused/full/
+distributed suites retain every previous case and deadline. It has compiled but
+not executed. Retain actual Settings-window screenshots in the hosted case.
+
+Signed local5803fc1 inline-heading fix and this preparation remain unpushed while
+exact53755ab installed114596717826 is verified live; focused21/native99 on537
+pass. Do not cancel the current full job or claim new installed/chooser/signing
+results. Issue21 remains open for full Settings redesign/default status/Printing/
+theme previews and owner acceptance. No local desktop input or production release.
+
## Mac screenshot comparison and inline-heading correction - 2026-10-11
Reviewed all eight owner-provided Mac screenshots; issue #21 records the concrete
diff --git a/docs/TODO.md b/docs/TODO.md
index 123b513..a6726f8 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -1,5 +1,8 @@
# Development Tasks
+- [x] Prepare native General Contents wrapping/depth-marker controls with checked sibling-safe persistence;593 core/100% measured lines and zero-warning Release pass.
+- [ ] Execute the actual long-title/sibling/persistence/restoration test and inspect Settings screenshots on its exact pushed head.
+
- [x] Review all eight Mac screenshots and record verified Settings gaps in issue #21 without private content.
- [ ] Implement/accept the native Windows Settings organization, General default-viewer status, Contents options, theme previews/defaults and Printing page.
- [x] Reproduce/correct multiline inline raw-heading fold loss;592 core tests/100% measured lines and zero-warning Release pass.
diff --git a/src/MarkdownViewr.Windows.Core/AppSettings.cs b/src/MarkdownViewr.Windows.Core/AppSettings.cs
index e31def0..2e0b17b 100644
--- a/src/MarkdownViewr.Windows.Core/AppSettings.cs
+++ b/src/MarkdownViewr.Windows.Core/AppSettings.cs
@@ -114,6 +114,16 @@ public sealed class AppSettings : ShellSettings
+ /// Long Contents entries wrap in full; otherwise they use one line with an ellipsis and full-title tooltip.
+ public bool WrapOutlineEntries { get; set; }
+
+
+
+ /// Optional native depth markers distinguish heading levels without changing titles or identities.
+ public bool ShowOutlineDepthMarkers { get; set; }
+
+
+
/// Last native sidebar width in device-independent pixels.
public double OutlineWidth { get; set; } = 220;
diff --git a/src/MarkdownViewr.Windows/App.Startup.cs b/src/MarkdownViewr.Windows/App.Startup.cs
index 906be46..b1a1fc5 100644
--- a/src/MarkdownViewr.Windows/App.Startup.cs
+++ b/src/MarkdownViewr.Windows/App.Startup.cs
@@ -9,6 +9,17 @@ namespace MarkdownViewr.Windows;
/// Startup preferences are shared by independent reader windows without changing an in-progress launch.
public partial class App
{
+ /// Updates native Contents and saved baselines only after the shared presentation preferences were persisted.
+ internal void ApplyContentsSettings(Core.AppSettings settings)
+ {
+ foreach(MainWindow window in _windows.ToArray())
+ {
+ window.ApplyContentsSettings(settings);
+ }
+ }
+
+
+
/// Updates each native reader and its saved baseline only after startup preferences were persisted successfully.
internal void ApplyStartupSettings(Core.AppSettings settings)
{
diff --git a/src/MarkdownViewr.Windows/MainWindow.Navigation.cs b/src/MarkdownViewr.Windows/MainWindow.Navigation.cs
index 682de04..f3a132b 100644
--- a/src/MarkdownViewr.Windows/MainWindow.Navigation.cs
+++ b/src/MarkdownViewr.Windows/MainWindow.Navigation.cs
@@ -76,8 +76,30 @@ public sealed partial class MainWindow
{
foreach(Core.Documents.DocumentHeading heading in _outline.Headings.Where(heading => heading.Level <= _settings.OutlineDepth))
{
- var label = new TextBlock { Tag = heading, Text = heading.Title, TextWrapping = TextWrapping.Wrap, MaxLines = 2, Margin = new Thickness((heading.Level - 1) * 12, 0, 0, 0) };
- var item = new ListViewItem { Content = label, Tag = heading };
+ var label = new TextBlock
+ {
+ Tag = heading,
+ Text = heading.Title,
+ TextWrapping = _settings.WrapOutlineEntries ? TextWrapping.Wrap : TextWrapping.NoWrap,
+ TextTrimming = _settings.WrapOutlineEntries ? TextTrimming.None : TextTrimming.CharacterEllipsis,
+ MaxLines = _settings.WrapOutlineEntries ? 0 : 1,
+ };
+ Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(label, "OutlineTitle:" + heading.Index.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ var row = new Grid { Margin = new Thickness((heading.Level - 1) * 12, 0, 0, 0), ColumnSpacing = 6 };
+ row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ row.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ if(_settings.ShowOutlineDepthMarkers)
+ {
+ var marker = new TextBlock { Text = "●○▪▫–·"[(heading.Level - 1)..heading.Level], VerticalAlignment = VerticalAlignment.Top, Width = 16 };
+ Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(marker, "OutlineDepthMarker:" + heading.Index.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(marker, "Heading level " + heading.Level.ToString(System.Globalization.CultureInfo.InvariantCulture));
+ row.Children.Add(marker);
+ }
+ Grid.SetColumn(label, 1);
+ row.Children.Add(label);
+ var item = new ListViewItem { Content = row, Tag = heading, HorizontalContentAlignment = HorizontalAlignment.Stretch };
+ Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(item, heading.Title);
+ ToolTipService.SetToolTip(item, new TextBlock { Text = heading.Title, TextWrapping = TextWrapping.Wrap, MaxWidth = 320 });
Microsoft.UI.Xaml.Automation.AutomationProperties.SetAutomationId(item, "OutlineHeading:" + heading.Index.ToString(System.Globalization.CultureInfo.InvariantCulture));
OutlineList.Items.Add(item);
if(heading == selected)
diff --git a/src/MarkdownViewr.Windows/MainWindow.Settings.cs b/src/MarkdownViewr.Windows/MainWindow.Settings.cs
index be8d2ae..ed7823d 100644
--- a/src/MarkdownViewr.Windows/MainWindow.Settings.cs
+++ b/src/MarkdownViewr.Windows/MainWindow.Settings.cs
@@ -40,7 +40,7 @@ public sealed partial class MainWindow
}
if(_settingsWindow is null)
{
- _settingsWindow = new SettingsWindow(_settings, SaveReadingAppearanceAsync, SaveStartupSettings, RunSettingsActionAsync);
+ _settingsWindow = new SettingsWindow(_settings, SaveReadingAppearanceAsync, SaveStartupSettings, SaveContentsSettings, RunSettingsActionAsync);
WinUIShell.ApplyTheme((FrameworkElement)_settingsWindow.Content, _settings.ThemeKind);
_settingsWindow.Closed += (_, _) => _settingsWindow = null;
}
@@ -113,6 +113,41 @@ public sealed partial class MainWindow
+ /// Only Contents presentation fields change in the latest store, preserving sibling reader and CSS preferences.
+ private bool SaveContentsSettings(bool wrap, bool depthMarkers)
+ {
+ if(_closed)
+ {
+ return false;
+ }
+ Core.AppSettings settings = AppIdentity.SettingsStore.Load();
+ settings.WrapOutlineEntries = wrap;
+ settings.ShowOutlineDepthMarkers = depthMarkers;
+ if(!AppIdentity.SettingsStore.Save(settings))
+ {
+ return false;
+ }
+ ((App)Application.Current).ApplyContentsSettings(settings);
+ return true;
+ }
+
+
+
+ /// Committed Contents presentation refreshes live headings and the baseline without changing visibility or depth selection.
+ internal void ApplyContentsSettings(Core.AppSettings settings)
+ {
+ if(_closed)
+ {
+ return;
+ }
+ _settings.WrapOutlineEntries = _lastSavedSettings.WrapOutlineEntries = settings.WrapOutlineEntries;
+ _settings.ShowOutlineDepthMarkers = _lastSavedSettings.ShowOutlineDepthMarkers = settings.ShowOutlineDepthMarkers;
+ RefreshOutline();
+ _settingsWindow?.UpdateContents(settings);
+ }
+
+
+
/// Only fixed native Settings controls launch configuration; document content cannot request these operations.
private Task RunSettingsActionAsync(string action) => action switch
{
diff --git a/src/MarkdownViewr.Windows/SettingsWindow.Contents.cs b/src/MarkdownViewr.Windows/SettingsWindow.Contents.cs
new file mode 100644
index 0000000..60517ff
--- /dev/null
+++ b/src/MarkdownViewr.Windows/SettingsWindow.Contents.cs
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2026 James Maes
+ * SPDX-License-Identifier: MIT
+ */
+
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Automation;
+using Microsoft.UI.Xaml.Controls;
+
+
+namespace MarkdownViewr.Windows;
+
+/// General settings expose native Contents presentation and independent new-window defaults.
+internal sealed partial class SettingsWindow
+{
+ private readonly ToggleSwitch _wrapOutlineEntries = new() { Header = "Wrap long entries in Contents" };
+ private readonly ToggleSwitch _showOutlineDepthMarkers = new() { Header = "Show heading-depth markers in Contents" };
+ private readonly Func _saveContents;
+
+
+
+ /// Related controls share one page; presentation updates live while panel defaults apply to new windows.
+ private void AddContentsPreferences(StackPanel page, Core.AppSettings current)
+ {
+ page.Children.Add(new TextBlock { Text = "New document windows", FontSize = 20, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold });
+ page.Children.Add(new TextBlock { Text = "Contents and Source start hidden by default. Toolbar changes affect only the current window.", TextWrapping = TextWrapping.Wrap });
+ AutomationProperties.SetAutomationId(_showOutlineByDefault, "SettingsDefaultContentsToggle");
+ AutomationProperties.SetAutomationId(_showSourceByDefault, "SettingsDefaultSourceToggle");
+ _showOutlineByDefault.Toggled += (_, _) => SaveStartup();
+ _showSourceByDefault.Toggled += (_, _) => SaveStartup();
+ page.Children.Add(_showOutlineByDefault);
+ page.Children.Add(_showSourceByDefault);
+ page.Children.Add(new TextBlock { Text = "Contents", FontSize = 20, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold });
+ AutomationProperties.SetAutomationId(_wrapOutlineEntries, "SettingsContentsWrapToggle");
+ AutomationProperties.SetAutomationId(_showOutlineDepthMarkers, "SettingsContentsDepthMarkersToggle");
+ UpdateContents(current);
+ _wrapOutlineEntries.Toggled += (_, _) => SaveContents();
+ _showOutlineDepthMarkers.Toggled += (_, _) => SaveContents();
+ page.Children.Add(_wrapOutlineEntries);
+ page.Children.Add(new TextBlock { Text = "When off, long headings end with an ellipsis. Hover over an entry to see its full title.", TextWrapping = TextWrapping.Wrap, Opacity = 0.7 });
+ page.Children.Add(_showOutlineDepthMarkers);
+ page.Children.Add(new TextBlock { Text = "A different shape marks each heading level. These changes apply to all open windows.", TextWrapping = TextWrapping.Wrap, Opacity = 0.7 });
+ }
+
+
+
+ /// Saved sibling changes refresh the toggles without creating recursive saves.
+ internal void UpdateContents(Core.AppSettings settings)
+ {
+ if(_closed)
+ {
+ return;
+ }
+ bool previous = _updating;
+ _updating = true;
+ try
+ {
+ _wrapOutlineEntries.IsOn = settings.WrapOutlineEntries;
+ _showOutlineDepthMarkers.IsOn = settings.ShowOutlineDepthMarkers;
+ }
+ finally
+ {
+ _updating = previous;
+ }
+ }
+
+
+
+ /// Persistence precedes visible changes; failure stays visible in the native status area.
+ private void SaveContents()
+ {
+ if(_closed || _updating)
+ {
+ return;
+ }
+ try
+ {
+ _status.Text = _saveContents(_wrapOutlineEntries.IsOn, _showOutlineDepthMarkers.IsOn) ? "Saved. Applies to all open windows." : "Could not save the setting. Please try again.";
+ }
+ catch(Exception exception)
+ {
+ ReportError(exception);
+ }
+ }
+}
diff --git a/src/MarkdownViewr.Windows/SettingsWindow.Startup.cs b/src/MarkdownViewr.Windows/SettingsWindow.Startup.cs
index a7cd224..501c20f 100644
--- a/src/MarkdownViewr.Windows/SettingsWindow.Startup.cs
+++ b/src/MarkdownViewr.Windows/SettingsWindow.Startup.cs
@@ -40,15 +40,6 @@ internal sealed partial class SettingsWindow
page.Children.Add(_showSplash);
page.Children.Add(_splashDuration);
page.Children.Add(_checkStartupUpdates);
- page.Children.Add(new TextBlock { Text = "New document windows", FontSize = 20, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold });
- page.Children.Add(new TextBlock { Text = "Contents and Source start hidden by default. Toolbar changes affect only the current window.", TextWrapping = TextWrapping.Wrap });
- AutomationProperties.SetAutomationId(_showOutlineByDefault, "SettingsDefaultContentsToggle");
- AutomationProperties.SetAutomationId(_showSourceByDefault, "SettingsDefaultSourceToggle");
- _showOutlineByDefault.Toggled += (_, _) => SaveStartup();
- _showSourceByDefault.Toggled += (_, _) => SaveStartup();
- page.Children.Add(_showOutlineByDefault);
- page.Children.Add(_showSourceByDefault);
- AddDefaultFileTypes(page);
return page;
}
diff --git a/src/MarkdownViewr.Windows/SettingsWindow.cs b/src/MarkdownViewr.Windows/SettingsWindow.cs
index b3237a8..9e107ff 100644
--- a/src/MarkdownViewr.Windows/SettingsWindow.cs
+++ b/src/MarkdownViewr.Windows/SettingsWindow.cs
@@ -33,13 +33,14 @@ internal sealed partial class SettingsWindow : Window
/// Settings is a separate window; routine layout saves immediately and advanced CSS has an explicit Apply action.
- public SettingsWindow(Core.AppSettings current, Func> saveAppearance, Func saveStartup, Func runAction)
+ public SettingsWindow(Core.AppSettings current, Func> saveAppearance, Func saveStartup, Func saveContents, Func runAction)
{
Title = AppIdentity.Info.DisplayName + " — Settings";
SystemBackdrop = new MicaBackdrop();
_appearance = new ReadingAppearanceEditor(current.ReadingAppearance);
_savedCss = current.GlobalCss;
_saveStartup = saveStartup;
+ _saveContents = saveContents;
_saveAppearance = saveAppearance;
_runAction = runAction;
_root.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
@@ -55,7 +56,7 @@ internal sealed partial class SettingsWindow : Window
AutomationProperties.SetLiveSetting(_status, AutomationLiveSetting.Polite);
_root.Children.Add(_status);
Content = _root;
- AddPage("SettingsAppearanceTab", "Appearance", Symbol.Font, AppearancePage());
+ AddPage("SettingsAppearanceTab", "General", Symbol.Home, AppearancePage(current));
AddPage("SettingsThemesTab", "Document themes", Symbol.Library, ThemesPage());
AddPage("SettingsEditorsTab", "Editors", Symbol.Edit, EditorsPage());
AddPage("SettingsAdvancedTab", "Advanced", Symbol.Setting, AdvancedPage(current));
@@ -139,14 +140,17 @@ internal sealed partial class SettingsWindow : Window
_navigation.Header = title;
_pageScroll.Content = null;
_pageScroll.Content = page;
+ _pageScroll.ChangeView(null, 0, null, disableAnimation: true);
}
/// Native layout controls save their validated changes; unsubmitted CSS stays a separate draft.
- private StackPanel AppearancePage()
+ private StackPanel AppearancePage(Core.AppSettings current)
{
var page = new StackPanel { Spacing = 16, MaxWidth = 720, HorizontalAlignment = HorizontalAlignment.Stretch };
+ AddDefaultFileTypes(page);
+ AddContentsPreferences(page, current);
page.Children.Add(_appearance.Fields);
var apply = new Button { Content = "Apply custom CSS", HorizontalAlignment = HorizontalAlignment.Left };
AutomationProperties.SetAutomationId(apply, "SettingsApplyCss");
diff --git a/tests/MarkdownViewr.Windows.Core.Tests/SettingsChangesTests.cs b/tests/MarkdownViewr.Windows.Core.Tests/SettingsChangesTests.cs
index d08dba1..bb11e74 100644
--- a/tests/MarkdownViewr.Windows.Core.Tests/SettingsChangesTests.cs
+++ b/tests/MarkdownViewr.Windows.Core.Tests/SettingsChangesTests.cs
@@ -49,6 +49,31 @@ public sealed class SettingsChangesTests
+ /// Legacy settings use truncated unmarked Contents; saved presentation choices survive an unrelated stale-reader edit.
+ [Fact]
+ public void Merge_ContentsPresentation_PersistsWithoutChangingPanelDefaults()
+ {
+ var before = new AppSettings();
+ Assert.False(before.WrapOutlineEntries);
+ Assert.False(before.ShowOutlineDepthMarkers);
+ AppSettings edited = SettingsChanges.Copy(before);
+ edited.OutlineDepth = 5;
+ edited.ShowOutline = true;
+ var current = new AppSettings { WrapOutlineEntries = true, ShowOutlineDepthMarkers = true, DocumentTheme = "Nord" };
+ AppSettings result = SettingsChanges.Copy(SettingsChanges.Merge(current, before, edited));
+ Assert.True(result.WrapOutlineEntries);
+ Assert.True(result.ShowOutlineDepthMarkers);
+ Assert.True(result.ShowOutline);
+ Assert.Equal(5, result.OutlineDepth);
+ Assert.Equal("Nord", result.DocumentTheme);
+ Assert.False(result.ShowOutlineByDefault);
+ Assert.False(result.ShowSourceByDefault);
+ Assert.False(before.WrapOutlineEntries);
+ Assert.False(before.ShowOutlineDepthMarkers);
+ }
+
+
+
/// A window's source change preserves newer history, theme, editor and print choices from its sibling.
[Fact]
public void Merge_StaleWindow_PreservesUnchangedSiblingPreferences()
diff --git a/tests/MarkdownViewr.Windows.UiTests/AppTests.Settings.cs b/tests/MarkdownViewr.Windows.UiTests/AppTests.Settings.cs
index 8c948d7..8efedd8 100644
--- a/tests/MarkdownViewr.Windows.UiTests/AppTests.Settings.cs
+++ b/tests/MarkdownViewr.Windows.UiTests/AppTests.Settings.cs
@@ -25,7 +25,7 @@ public partial class AppTests
Assert.Null(session.FindInApp(cf => cf.ByAutomationId("OutlineDepth")));
Assert.Null(session.FindInApp(cf => cf.ByAutomationId("SourceText")));
session.InvokeMenuItem("File", "SettingsMenuItem");
- session.WaitForElement("SettingsAdvancedTab").Patterns.SelectionItem.Pattern.Select();
+ session.WaitForElement("SettingsAppearanceTab").Patterns.SelectionItem.Pattern.Select();
AutomationElement contents = session.WaitForElement("SettingsDefaultContentsToggle");
AutomationElement source = session.WaitForElement("SettingsDefaultSourceToggle");
Assert.Equal(ToggleState.Off, contents.Patterns.Toggle.Pattern.ToggleState.Value);
@@ -56,6 +56,55 @@ public partial class AppTests
+ /// Native long titles wrap without truncation, depth markers update sibling windows and unrelated saves retain both choices.
+ [Fact]
+ public void Settings_ContentsPresentation_UpdatesLiveSiblingsAndPersists()
+ {
+ const string Title = "A deliberately long heading that must wrap across several lines in a narrow Contents panel without losing its full title";
+ const string Document = "# Contents display fixture\n\n## " + Title + "\nBody";
+ using var session = new AppSession(document: Document, documentFileName: "contents.md",
+ additionalDocuments: new Dictionary(StringComparer.Ordinal) { ["sibling.md"] = Document });
+ Window primary = WaitForDocumentWindow(session, "contents.md");
+ Window sibling = WaitForDocumentWindow(session, "sibling.md");
+ WaitForWindowHeading(session, sibling, "Contents display fixture");
+ session.WaitForElement("mv-heading-0");
+ InvokeDocumentMenu(session, primary, "OutlineToggle", menuName: "View");
+ InvokeDocumentMenu(session, sibling, "OutlineToggle", menuName: "View");
+ AutomationElement title = WaitForWindowElement(session, primary, "OutlineTitle:1");
+ double oneLineHeight = title.BoundingRectangle.Height;
+ Assert.True(oneLineHeight > 0);
+ Assert.Equal(Title, WaitForWindowElement(session, primary, "OutlineHeading:1").Name);
+ Assert.Null(session.FindInApp(cf => cf.ByAutomationId("OutlineDepthMarker:1")));
+ InvokeDocumentMenu(session, primary, "SettingsMenuItem");
+ AutomationElement wrap = session.WaitForElement("SettingsContentsWrapToggle");
+ AutomationElement markers = session.WaitForElement("SettingsContentsDepthMarkersToggle");
+ Assert.Equal(ToggleState.Off, wrap.Patterns.Toggle.Pattern.ToggleState.Value);
+ Assert.Equal(ToggleState.Off, markers.Patterns.Toggle.Pattern.ToggleState.Value);
+ wrap.Patterns.Toggle.Pattern.Toggle();
+ markers.Patterns.Toggle.Pattern.Toggle();
+ RetainDocumentWindow(session, session.TopLevelWindows().Single(element => string.Equals(element.Name, session.DisplayName + " — Settings", StringComparison.Ordinal)).AsWindow(), "general-contents-settings");
+ CloseSettings(session);
+ session.WaitFor(() => primary.FindFirstDescendant(cf => cf.ByAutomationId("OutlineTitle:1")) is { } label && label.BoundingRectangle.Height > oneLineHeight * 1.8 ? label : null, "the fully wrapped Contents title");
+ Assert.Equal("Heading level 2", WaitForWindowElement(session, primary, "OutlineDepthMarker:1").Name);
+ Assert.Equal("Heading level 2", WaitForWindowElement(session, sibling, "OutlineDepthMarker:1").Name);
+ Assert.True(WaitForWindowElement(session, sibling, "OutlineTitle:1").BoundingRectangle.Height > oneLineHeight * 1.8);
+ InvokeDocumentMenu(session, sibling, "SourceToggle", menuName: "View");
+ AssertSavedContentsPreferences(session, enabled: true);
+ InvokeDocumentMenu(session, primary, "SettingsMenuItem");
+ Assert.Equal(ToggleState.On, session.WaitForElement("SettingsContentsWrapToggle").Patterns.Toggle.Pattern.ToggleState.Value);
+ Assert.Equal(ToggleState.On, session.WaitForElement("SettingsContentsDepthMarkersToggle").Patterns.Toggle.Pattern.ToggleState.Value);
+ session.WaitForElement("SettingsContentsWrapToggle").Patterns.Toggle.Pattern.Toggle();
+ session.WaitForElement("SettingsContentsDepthMarkersToggle").Patterns.Toggle.Pattern.Toggle();
+ CloseSettings(session);
+ Assert.Null(session.FindInApp(cf => cf.ByAutomationId("OutlineDepthMarker:1")));
+ session.WaitFor(() => primary.FindFirstDescendant(cf => cf.ByAutomationId("OutlineTitle:1")) is { } label && label.BoundingRectangle.Height <= oneLineHeight * 1.2 ? label : null, "the restored single-line Contents title");
+ Assert.Equal(Title, WaitForWindowElement(session, primary, "OutlineHeading:1").Name);
+ AssertSavedContentsPreferences(session, enabled: false);
+ RetainDocumentWindow(session, primary, "contents-truncation-and-markers");
+ }
+
+
+
/// The appearance draft survives a cancelled secondary theme editor without saving either draft.
[Fact]
public void Settings_ThemeEditorCancel_RetainsCssDraftUntilWindowClose()
@@ -170,7 +219,7 @@ public partial class AppTests
using var session = new AppSession(document: "# Default file settings fixture");
session.WaitFor(() => session.FindInApp(cf => cf.ByName("Default file settings fixture")), "the default file settings fixture", TimeSpan.FromSeconds(30));
session.InvokeMenuItem("File", "SettingsMenuItem");
- session.WaitForElement("SettingsAdvancedTab").Patterns.SelectionItem.Pattern.Select();
+ session.WaitForElement("SettingsAppearanceTab").Patterns.SelectionItem.Pattern.Select();
AutomationElement guidance = session.WaitForElement("SettingsDefaultFilesGuidance");
Assert.Contains(".md", guidance.Name, StringComparison.Ordinal);
Assert.Contains(".txt) is optional", guidance.Name, StringComparison.Ordinal);
@@ -201,6 +250,18 @@ public partial class AppTests
+ /// Reads the actual saved presentation choices and proves they did not opt new windows into either pane.
+ private static void AssertSavedContentsPreferences(AppSession session, bool enabled)
+ {
+ using var saved = JsonDocument.Parse(session.ReadSettings());
+ Assert.Equal(enabled, saved.RootElement.GetProperty("WrapOutlineEntries").GetBoolean());
+ Assert.Equal(enabled, saved.RootElement.GetProperty("ShowOutlineDepthMarkers").GetBoolean());
+ Assert.False(saved.RootElement.GetProperty("ShowOutlineByDefault").GetBoolean());
+ Assert.False(saved.RootElement.GetProperty("ShowSourceByDefault").GetBoolean());
+ }
+
+
+
/// All configuration tests enter through the actual File Settings command and native category items.
private static void OpenSettingsAction(AppSession session, string identity, string tab = "SettingsThemesTab")
{